#!/usr/bin/env python # Copyright 2017 Cisco Systems, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # ############################################################################### # # # This is a helper script which will delete all resources created by # # NFVbench. # # # # Normally, NFVbench will clean up automatically when it is done. However, # # sometimes errors or timeouts happen during the resource creation stage, # # which will cause NFVbench out of sync with the real environment. If that # # happens, a force cleanup may be needed. # # # # It is safe to use the script with the resource list generated by # # NFVbench, usage: # # $ python nfvbench_cleanup.py -r /path/to/openrc # # # # Note: If running under single-tenant or tenant/user reusing mode, you have # # to cleanup the server resources first, then client resources. # # # # When there is no resource list provided, the script will simply grep the # # resource name with "nfvbench" and delete them. If running on a production # # network, please double and triple check all resources names are *NOT* # # starting with "nfvbench", otherwise they will be deleted by the script. # # # ############################################################################### # ====================================================== # WARNING # ====================================================== # IMPORTANT FOR RUNNING NFVbench ON PRODUCTION CLOUDS # # DOUBLE CHECK THE NAMES OF ALL RESOURCES THAT DO NOT # BELONG TO NFVbench ARE *NOT* STARTING WITH "nfvbench". # ====================================================== from abc import ABCMeta from abc import abstractmethod import argparse import re import sys import time import traceback # openstack python clients import cinderclient from keystoneclient import client as keystoneclient import neutronclient from novaclient.exceptions import NotFound from tabulate import tabulate from nfvbench import credentials resource_name_re = None def prompt_to_run(): print "Warning: You didn't specify a resource list file as the input. "\ "The script will delete all resources shown above." answer = raw_input("Are you sure? (y/n) ") if answer.lower() != 'y': sys.exit(0) def fetch_resources(fetcher, options=None): try: if options: res_list = fetcher(search_opts=options) else: res_list = fetcher() except Exception as e: res_list = [] traceback.print_exc() print "Warning exception while listing resources:" + str(e) resources = {} for res in res_list: # some objects provide direct access some # require access by key try: resid = res.id resname = res.name except AttributeError: resid = res['id'] resname = res['name'] if resname and resource_name_re.match(resname): resources[resid] = resname return resources class AbstractCleaner(object): __metaclass__ = ABCMeta def __init__(self, res_category, res_desc, resources, dryrun): self.dryrun = dryrun self.category = res_category self.resources = {} if not resources: print 'Discovering %s resources...' % (res_category) for rtype, fetch_args in res_desc.iteritems(): if resources: if rtype in resources: self.resources[rtype] = resources[rtype] else: self.resources[rtype] = fetch_resources(*fetch_args) def report_deletion(self, rtype, name): if self.dryrun: print ' + ' + rtype + ' ' + name + ' should be deleted (but is not deleted: dry run)' else: print ' + ' + rtype + ' ' + name + ' is successfully deleted' def report_not_found(self, rtype, name): print ' ? ' + rtype + ' ' + name + ' not found (already deleted?)' def report_error(self, rtype, name, reason): print ' - ' + rtype + ' ' + name + ' ERROR:' + reason def get_resource_list(self): result = [] for rtype, rdict in self.resources.iteritems(): for resid, resname in rdict.iteritems(): result.append([rtype, resname, resid]) return result @abstractmethod def clean(self): pass class StorageCleaner(AbstractCleaner): def __init__(self, sess, resources, dryrun): from cinderclient import client as cclient from novaclient import client as nclient self.nova = nclient.Client('2', endpoint_type='publicURL', session=sess) self.cinder = cclient.Client('2', endpoint_type='publicURL', session=sess) res_desc = {'volumes': [self.cinder.volumes.list, {"all_tenants": 1}]} super(StorageCleaner, self).__init__('Storage', res_desc, resources, dryrun) def clean(self): print '*** STORAGE cleanup' try: volumes = [] detaching_volumes = [] for id, name in self.resources['volumes'].iteritems(): try: vol = self.cinder.volumes.get(id) if vol.attachments: # detach the volume try: if not self.dryrun: ins_id = vol.attachments[0]['server_id'] self.nova.volumes.delete_server_volume(ins_id, id) print ' . VOLUME ' + vol.name + ' detaching...' else: print ' . VOLUME ' + vol.name + ' to be detached...' detaching_volumes.append(vol) except NotFound: print 'WARNING: Volume %s attached to an instance that no longer '\ 'exists (will require manual cleanup of the database)' % (id) except Exception as e: print str(e) else: # no attachments volumes.append(vol) except cinderclient.exceptions.NotFound: self.report_not_found('VOLUME', name) # check that the volumes are no longer attached if detaching_volumes: if not self.dryrun: print ' . Waiting for %d volumes to be fully detached...' % \ (len(detaching_volumes)) retry_count = 5 + len(detaching_volumes) while True: retry_count -= 1 for vol in list(detaching_volumes): if not self.dryrun: latest_vol = self.cinder.volumes.get(detaching_volumes[0].id) if self.dryrun or not latest_vol.attachments: if not self.dryrun: print ' + VOLUME ' + vol.name + ' detach complete' detaching_volumes.remove(vol) volumes.append(vol) if detaching_volumes and not self.dryrun: if retry_count: print ' . VOLUME %d left to be detached, retries left=%d...' % \ (len(detaching_volumes), retry_count) time.sleep(2) else: print ' - VOLUME detach timeout, %d volumes left:' % \ (len(detaching_volumes)) for vol in detaching_volumes: print ' ', vol.name, vol.status, vol.id, vol.attachments break else: break # finally delete the volumes for vol in volumes: if not self.dryrun: try: vol.force_delete() except cinderclient.exceptions.BadRequest as exc: print str(exc) self.report_deletion('VOLUME', vol.name) except KeyError: pass class ComputeCleaner(AbstractCleaner): def __init__(self, sess, resources, dryrun): from neutronclient.neutron import client as nclient from novaclient import client as novaclient self.neutron_client = nclient.Client('2.0', endpoint_type='publicURL', session=sess) self.nova_client = novaclient.Client('2', endpoint_type='publicURL', session=sess) res_desc = { 'instances': [self.nova_client.servers.list, {"all_tenants": 1}], 'flavors': [self.nova_client.flavors.list], 'keypairs': [self.nova_client.keypairs.list] } super(ComputeCleaner, self).__init__('Compute', res_desc, resources, dryrun) def clean(self): print '*** COMPUTE cleanup' try: # Get a list of floating IPs fip_lst = self.neutron_client.list_floatingips()['floatingips'] deleting_instances = self.resources['instances'] for id, name in self.resources['instances'].iteritems(): try: if self.nova_client.servers.get(id).addresses.values(): ins_addr = self.nova_client.servers.get(id).addresses.values()[0] fips = [x['addr'] for x in ins_addr if x['OS-EXT-IPS:type'] == 'floating'] else: fips = [] if self.dryrun: self.nova_client.servers.get(id) for fip in fips: self.report_deletion('FLOATING IP', fip) self.report_deletion('INSTANCE', name) else: for fip in fips: fip_id = [x['id'] for x in fip_lst if x['floating_ip_address'] == fip] self.neutron_client.delete_floatingip(fip_id[0]) self.report_deletion('FLOATING IP', fip) self.nova_client.servers.delete(id) except NotFound: deleting_instances.remove(id) self.report_not_found('INSTANCE', name) if not self.dryrun and len(deleting_instances): print ' . Waiting for %d instances to be fully deleted...' % \ (len(deleting_instances)) retry_count = 5 + len(deleting_instances) while True: retry_count -= 1 for ins_id in deleting_instances.keys(): try: self.nova_client.servers.get(ins_id) except NotFound: self.report_deletion('INSTANCE', deleting_instances[ins_id]) deleting_instances.pop(ins_id) if not len(deleting_instances): break if retry_count: print ' . INSTANCE %d left to be deleted, retries left=%d...' % \ (len(deleting_instances), retry_count) time.sleep(2) else: print ' - INSTANCE deletion timeout, %d instances left:' % \ (len(deleting_instances)) for ins_id in deleting_instances.keys(): try: ins = self.nova_client.servers.get(ins_id) print ' ', ins.name, ins.status, ins.id except NotFound: print(' ', deleting_instances[ins_id], '(just deleted)', ins_id) break except KeyError: pass try: for id, name in self.resources['flavors'].iteritems(): try: flavor = self.nova_client.flavors.find(name=name) if not self.dryrun: flavor.delete() self.report_deletion('FLAVOR', name) except NotFound: self.report_not_found('FLAVOR', name) except KeyError: pass try: for id, name in self.resources['keypairs'].iteritems(): try: if self.dryrun: self.nova_client.keypairs.get(name) else: self.nova_client.keypairs.delete(name) self.report_deletion('KEY PAIR', name) except NotFound: self.report_not_found('KEY PAIR', name) except KeyError: pass class NetworkCleaner(AbstractCleaner): def __init__(self, sess, resources, dryrun): from neutronclient.neutron import client as nclient self.neutron = nclient.Client('2.0', endpoint_type='publicURL', session=sess) # because the response has an extra level of indirection # we need to extract it to present the list of network or router objects def networks_fetcher(): return self.neutron.list_networks()['networks'] def routers_fetcher(): return self.neutron.list_routers()['routers'] def secgroup_fetcher(): return self.neutron.list_security_groups()['security_groups'] res_desc = { 'sec_groups': [secgroup_fetcher], 'networks': [networks_fetcher], 'routers': [routers_fetcher] } super(NetworkCleaner, self).__init__('Network', res_desc, resources, dryrun) def remove_router_interface(self, router_id, port): """ Remove the network interface from router """ body = { # 'port_id': port['id'] 'subnet_id': port['fixed_ips'][0]['subnet_id'] } try: self.neutron.remove_interface_router(router_id, body) self.report_deletion('Router Interface', port['fixed_ips'][0]['ip_address']) except neutronclient.common.exceptions.NotFound: pass def remove_network_ports(self, net): """ Remove ports belonging to network """ for port in filter(lambda p: p['network_id'] == net, self.neutron.list_ports()['ports']): try: self.neutron.delete_port(port['id']) self.report_deletion('Network port', port['id']) except neutronclient.common.exceptions.NotFound: pass def clean(self): print '*** NETWORK cleanup' try: for id, name in self.resources['sec_groups'].iteritems(): try: if self.dryrun: self.neutron.show_security_group(id) else: self.neutron.delete_security_group(id) self.report_deletion('SECURITY GROUP', name) except NotFound: self.report_not_found('SECURITY GROUP', name) except KeyError: pass try: for id, name in self.resources['floating_ips'].iteritems(): try: if self.dryrun: self.neutron.show_floatingip(id) else: self.neutron.delete_floatingip(id) self.report_deletion('FLOATING IP', name) except neutronclient.common.exceptions.NotFound: self.report_not_found('FLOATING IP', name) except KeyError: pass try: for id, name in self.resources['routers'].iteritems(): try: if self.dryrun: self.neutron.show_router(id) self.report_deletion('Router Gateway', name) port_list = self.neutron.list_ports(id)['ports'] for port in port_list: if 'fixed_ips' in port: self.report_deletion('Router Interface', port['fixed_ips'][0]['ip_address']) else: self.neutron.remove_gateway_router(id) self.report_deletion('Router Gateway', name) # need to delete each interface before deleting the router port_list = self.neutron.list_ports(id)['ports'] for port in port_list: self.remove_router_interface(id, port) self.neutron.delete_router(id) self.report_deletion('ROUTER', name) except neutronclient.common.exceptions.NotFound: self.report_not_found('ROUTER', name) except neutronclient.common.exceptions.Conflict as exc: self.report_error('ROUTER', name, str(exc)) except KeyError: pass try: for id, name in self.resources['networks'].iteritems(): try: if self.dryrun: self.neutron.show_network(id) else: self.remove_network_ports(id) self.neutron.delete_network(id) self.report_deletion('NETWORK', name) except neutronclient.common.exceptions.NetworkNotFoundClient: self.report_not_found('NETWORK', name) except neutronclient.common.exceptions.NetworkInUseClient as exc: self.report_error('NETWORK', name, str(exc)) except KeyError: pass class KeystoneCleaner(AbstractCleaner): def __init__(self, sess, resources, dryrun): self.keystone = keystoneclient.Client(endpoint_type='publicURL', session=sess) self.tenant_api = self.keystone.tenants \ if self.keystone.version == 'v2.0' else self.keystone.projects res_desc = { 'users': [self.keystone.users.list], 'tenants': [self.tenant_api.list] } super(KeystoneCleaner, self).__init__('Keystone', res_desc, resources, dryrun) def clean(self): print '*** KEYSTONE cleanup' try: for id, name in self.resources['users'].iteritems(): try: if self.dryrun: self.keystone.users.get(id) else: self.keystone.users.delete(id) self.report_deletion('USER', name) except keystoneclient.auth.exceptions.http.NotFound: self.report_not_found('USER', name) except KeyError: pass try: for id, name in self.resources['tenants'].iteritems(): try: if self.dryrun: self.tenant_api.get(id) else: self.tenant_api.delete(id) self.report_deletion('TENANT', name) except keystoneclient.auth.exceptions.http.NotFound: self.report_not_found('TENANT', name) except KeyError: pass class Cleaners(object): def __init__(self, creds_obj, resources, dryrun): self.cleaners = [] sess = creds_obj.get_session() for cleaner_type in [StorageCleaner, ComputeCleaner, NetworkCleaner, KeystoneCleaner]: self.cleaners.append(cleaner_type(sess, resources, dryrun)) def show_resources(self): table = [["Type", "Name", "UUID"]] for cleaner in self.cleaners: table.extend(cleaner.get_resource_list()) count = len(table) - 1 print if count: print 'SELECTED RESOURCES:' print tabulate(table, headers="firstrow", tablefmt="psql") else: print 'There are no resources to delete.' print return count def clean(self): for cleaner in self.cleaners: cleaner.clean() # A dictionary of resources to cleanup # First level keys are: # flavors, keypairs, users, routers, floating_ips, instances, volumes, sec_groups, tenants, networks # second level keys are the resource IDs # values are the resource name (e.g. 'nfvbench-net0') def get_resources_from_cleanup_log(logfile): '''Load triplets separated by '|' into a 2 level dictionary ''' resources = {} with open(logfile) as ff: content = ff.readlines() for line in content: tokens = line.strip().split('|') restype = tokens[0] resname = tokens[1] resid = tokens[2] if not resid: # normally only the keypairs have no ID if restype != "keypairs": print 'Error: resource type %s has no ID - ignored!!!' % (restype) else: resid = '0' if restype not in resources: resources[restype] = {} tres = resources[restype] tres[resid] = resname return resources def main(): parser = argparse.ArgumentParser(description='NFVbench Force Cleanup') parser.add_argument('-r', '--rc', dest='rc', action='store', required=False, help='openrc file', metavar='') parser.add_argument('-f', '--file', dest='file', action='store', required=False, help='get resources to delete from cleanup log file ' '(default:discover from OpenStack)', metavar='') parser.add_argument('-d', '--dryrun', dest='dryrun', action='store_true', default=False, help='check resources only - do not delete anything') parser.add_argument('--filter', dest='filter', action='store', required=False, help='resource name regular expression filter (default:"nfvbench")' ' - OpenStack discovery only', metavar='') opts = parser.parse_args() cred = credentials.Credentials(openrc_file=opts.rc) if opts.file: resources = get_resources_from_cleanup_log(opts.file) else: # None means try to find the resources from openstack directly by name resources = None global resource_name_re if opts.filter: try: resource_name_re = re.compile(opts.filter) except Exception as exc: print 'Provided filter is not a valid python regular expression: ' + opts.filter print str(exc) sys.exit(1) else: resource_name_re = re.compile('nfvbench') cleaners = Cleaners(cred, resources, opts.dryrun) if opts.dryrun: print print('!!! DRY RUN - RESOURCES WILL BE CHECKED BUT WILL NOT BE DELETED !!!') print # Display resources to be deleted count = cleaners.show_resources() if not count: sys.exit(0) if not opts.file and not opts.dryrun: prompt_to_run() cleaners.clean() if __name__ == '__main__': main()