From dc42abd840d4bb6519062da794a817ce51655f47 Mon Sep 17 00:00:00 2001 From: ahothan Date: Mon, 9 Apr 2018 16:57:25 -0700 Subject: [NFVBENCH-83] Add option to display status and to cleanup Change-Id: If135fedee4e5ee9226a262800917c4c35bc83bc7 Signed-off-by: ahothan (cherry picked from commit 6226413ca6e1b4c3a52a3deeb66f8f487c2704ae) --- cleanup/__init__.py | 13 - cleanup/nfvbench_cleanup.py | 594 ------------------------------- docs/testing/user/userguide/advanced.rst | 86 +++-- nfvbench/cleanup.py | 179 ++++++++++ nfvbench/nfvbench.py | 58 ++- setup.cfg | 1 - tox.ini | 2 +- 7 files changed, 277 insertions(+), 656 deletions(-) delete mode 100644 cleanup/__init__.py delete mode 100644 cleanup/nfvbench_cleanup.py create mode 100644 nfvbench/cleanup.py diff --git a/cleanup/__init__.py b/cleanup/__init__.py deleted file mode 100644 index 04924ec..0000000 --- a/cleanup/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# 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. diff --git a/cleanup/nfvbench_cleanup.py b/cleanup/nfvbench_cleanup.py deleted file mode 100644 index 1520647..0000000 --- a/cleanup/nfvbench_cleanup.py +++ /dev/null @@ -1,594 +0,0 @@ -#!/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() diff --git a/docs/testing/user/userguide/advanced.rst b/docs/testing/user/userguide/advanced.rst index 252cbc9..02c7fce 100644 --- a/docs/testing/user/userguide/advanced.rst +++ b/docs/testing/user/userguide/advanced.rst @@ -314,46 +314,60 @@ NFVbench will dicover the MAC addresses to use for generated frames using: - either OpenStack discovery (find the MAC of an existing VM) in the case of PVP and PVVP service chains - or using dynamic ARP discovery (find MAC from IP) in the case of external chains. -Cleanup Script --------------- +Status and Cleanup of NFVbench Resources +---------------------------------------- + +The --status option will display the status of NFVbench and list any NFVbench resources. You need to pass the OpenStack RC +file in order to connect to OpenStack. + +.. code-block:: none -The nfvbench_cleanup script will cleanup resources created by NFVbench. You need to pass the OpenStack RC file in order to connect to -OpenStack. + # nfvbench --status -r /tmp/nfvbench/openrc + 2018-04-09 17:05:48,682 INFO Version: 1.3.2.dev1 + 2018-04-09 17:05:48,683 INFO Status: idle + 2018-04-09 17:05:48,757 INFO Discovering instances nfvbench-loop-vm... + 2018-04-09 17:05:49,252 INFO Discovering flavor nfvbench.medium... + 2018-04-09 17:05:49,281 INFO Discovering networks... + 2018-04-09 17:05:49,365 INFO No matching NFVbench resources found + # + +The Status can be either "idle" or "busy (run pending)". + +The --cleanup option will first discover resources created by NFVbench and prompt if you want to proceed with cleaning them up. Example of run: .. code-block:: none - # nfvbench_cleanup -r /tmp/nfvbench/openrc - Discovering Storage resources... - Discovering Compute resources... - Discovering Network resources... - Discovering Keystone resources... - - SELECTED RESOURCES: - +-----------+-------------------+--------------------------------------+ - | Type | Name | UUID | - |-----------+-------------------+--------------------------------------| - | flavors | nfvbench.medium | 362b2215-89d1-4f46-8b89-8e58165ff5bc | - | instances | nfvbench-loop-vm0 | f78dfb74-1b8e-4c5c-8d83-652a7571da95 | - | networks | nfvbench-net0 | 57d7e6c9-325f-4c13-9b1b-929344cc9c39 | - | networks | nfvbench-net1 | 2d429bcd-33fa-4aa4-9f2e-299a735177c9 | - +-----------+-------------------+--------------------------------------+ - - Warning: You didn't specify a resource list file as the input. The script will delete all resources shown above. + # nfvbench --cleanup -r /tmp/nfvbench/openrc + 2018-04-09 16:58:00,204 INFO Version: 1.3.2.dev1 + 2018-04-09 16:58:00,205 INFO Status: idle + 2018-04-09 16:58:00,279 INFO Discovering instances nfvbench-loop-vm... + 2018-04-09 16:58:00,829 INFO Discovering flavor nfvbench.medium... + 2018-04-09 16:58:00,876 INFO Discovering networks... + 2018-04-09 16:58:00,960 INFO Discovering ports... + 2018-04-09 16:58:01,012 INFO Discovered 6 NFVbench resources: + +----------+-------------------+--------------------------------------+ + | Type | Name | UUID | + |----------+-------------------+--------------------------------------| + | Instance | nfvbench-loop-vm0 | b039b858-777e-467e-99fb-362f856f4a94 | + | Flavor | nfvbench.medium | a027003c-ad86-4f24-b676-2b05bb06adc0 | + | Network | nfvbench-net0 | bca8d183-538e-4965-880e-fd92d48bfe0d | + | Network | nfvbench-net1 | c582a201-8279-4309-8084-7edd6511092c | + | Port | | 67740862-80ac-4371-b04e-58a0b0f05085 | + | Port | | b5db95b9-e419-4725-951a-9a8f7841e66a | + +----------+-------------------+--------------------------------------+ + 2018-04-09 16:58:01,013 INFO NFVbench will delete all resources shown... Are you sure? (y/n) y - *** STORAGE cleanup - *** COMPUTE cleanup - . Waiting for 1 instances to be fully deleted... - . INSTANCE 1 left to be deleted, retries left=5... - . INSTANCE 1 left to be deleted, retries left=4... - + INSTANCE nfvbench-loop-vm0 is successfully deleted - + FLAVOR nfvbench.medium is successfully deleted - *** NETWORK cleanup - + Network port 075d91f3-fa6a-428c-bd3f-ebd40cd935e1 is successfully deleted - + Network port 3a7ccd8c-53a6-43d0-a823-4b5ca762d06e is successfully deleted - + NETWORK nfvbench-net0 is successfully deleted - + Network port 5b5a75bd-e0b5-4f81-91b9-9e216d194f48 is successfully deleted - + Network port cc2d8f1b-49fe-491e-9e44-6990fc57e891 is successfully deleted - + NETWORK nfvbench-net1 is successfully deleted - *** KEYSTONE cleanup + 2018-04-09 16:58:01,865 INFO Deleting instance nfvbench-loop-vm0... + 2018-04-09 16:58:02,058 INFO Waiting for 1 instances to be fully deleted... + 2018-04-09 16:58:02,182 INFO 1 yet to be deleted by Nova, retries left=6... + 2018-04-09 16:58:04,506 INFO 1 yet to be deleted by Nova, retries left=5... + 2018-04-09 16:58:06,636 INFO 1 yet to be deleted by Nova, retries left=4... + 2018-04-09 16:58:08,701 INFO Deleting flavor nfvbench.medium... + 2018-04-09 16:58:08,729 INFO Deleting port 67740862-80ac-4371-b04e-58a0b0f05085... + 2018-04-09 16:58:09,102 INFO Deleting port b5db95b9-e419-4725-951a-9a8f7841e66a... + 2018-04-09 16:58:09,620 INFO Deleting network nfvbench-net0... + 2018-04-09 16:58:10,357 INFO Deleting network nfvbench-net1... # + +The --force-cleanup option will do the same but without prompting for confirmation. diff --git a/nfvbench/cleanup.py b/nfvbench/cleanup.py new file mode 100644 index 0000000..246be3f --- /dev/null +++ b/nfvbench/cleanup.py @@ -0,0 +1,179 @@ +#!/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. +# + +import sys +import time + +from neutronclient.neutron import client as nclient +from novaclient.client import Client +from novaclient.exceptions import NotFound +from tabulate import tabulate + +import credentials as credentials +from log import LOG + +class ComputeCleaner(object): + """A cleaner for compute resources.""" + + def __init__(self, nova_client, instance_prefix): + self.nova_client = nova_client + LOG.info('Discovering instances %s...', instance_prefix) + all_servers = self.nova_client.servers.list() + self.servers = [server for server in all_servers + if server.name.startswith(instance_prefix)] + + def instance_exists(self, server): + try: + self.nova_client.servers.get(server.id) + except NotFound: + return False + return True + + def get_resource_list(self): + return [["Instance", server.name, server.id] for server in self.servers] + + def clean(self): + if self.servers: + for server in self.servers: + try: + LOG.info('Deleting instance %s...', server.name) + self.nova_client.servers.delete(server.id) + except Exception: + LOG.exception("Instance %s deletion failed", server.name) + LOG.info(' Waiting for %d instances to be fully deleted...', len(self.servers)) + retry_count = 5 + len(self.servers) * 2 + while True: + retry_count -= 1 + self.servers = [server for server in self.servers if self.instance_exists(server)] + if not self.servers: + break + + if retry_count: + LOG.info(' %d yet to be deleted by Nova, retries left=%d...', + len(self.servers), retry_count) + time.sleep(2) + else: + LOG.warning(' instance deletion verification timed out: %d not removed', + len(self.servers)) + break + + +class NetworkCleaner(object): + """A cleaner for network resources.""" + + def __init__(self, neutron_client, network_names): + self.neutron_client = neutron_client + LOG.info('Discovering networks...') + all_networks = self.neutron_client.list_networks()['networks'] + self.networks = [] + for net in all_networks: + try: + network_names.remove(net['name']) + self.networks.append(net) + except ValueError: + pass + if not network_names: + break + net_ids = [net['id'] for net in self.networks] + if net_ids: + LOG.info('Discovering ports...') + all_ports = self.neutron_client.list_ports()['ports'] + self.ports = [port for port in all_ports if port['network_id'] in net_ids] + else: + self.ports = [] + + def get_resource_list(self): + res_list = [["Network", net['name'], net['id']] for net in self.networks] + res_list.extend([["Port", port['name'], port['id']] for port in self.ports]) + return res_list + + def clean(self): + for port in self.ports: + LOG.info("Deleting port %s...", port['id']) + try: + self.neutron_client.delete_port(port['id']) + except Exception: + LOG.exception("Port deletion failed") + + for net in self.networks: + LOG.info("Deleting network %s...", net['name']) + try: + self.neutron_client.delete_network(net['id']) + except Exception: + LOG.exception("Network deletion failed") + +class FlavorCleaner(object): + """Cleaner for NFVbench flavor.""" + + def __init__(self, nova_client, name): + self.name = name + LOG.info('Discovering flavor %s...', name) + try: + self.flavor = nova_client.flavors.find(name=name) + except NotFound: + self.flavor = None + + def get_resource_list(self): + if self.flavor: + return [['Flavor', self.name, self.flavor.id]] + return None + + def clean(self): + if self.flavor: + LOG.info("Deleting flavor %s...", self.flavor.name) + try: + self.flavor.delete() + except Exception: + LOG.exception("Flavor deletion failed") + +class Cleaner(object): + """Cleaner for all NFVbench resources.""" + + def __init__(self, config): + cred = credentials.Credentials(config.openrc_file, None, False) + session = cred.get_session() + self.neutron_client = nclient.Client('2.0', session=session) + self.nova_client = Client(2, session=session) + network_names = [inet['name'] for inet in config.internal_networks.values()] + self.cleaners = [ComputeCleaner(self.nova_client, config.loop_vm_name), + FlavorCleaner(self.nova_client, config.flavor_type), + NetworkCleaner(self.neutron_client, network_names)] + + def show_resources(self): + """Show all NFVbench resources.""" + table = [["Type", "Name", "UUID"]] + for cleaner in self.cleaners: + res_list = cleaner.get_resource_list() + if res_list: + table.extend(res_list) + count = len(table) - 1 + if count: + LOG.info('Discovered %d NFVbench resources:', count) + print tabulate(table, headers="firstrow", tablefmt="psql") + else: + LOG.info('No matching NFVbench resources found') + return count + + def clean(self, prompt): + """Clean all resources.""" + LOG.info("NFVbench will delete all resources shown...") + if prompt: + answer = raw_input("Are you sure? (y/n) ") + if answer.lower() != 'y': + LOG.info("Exiting without deleting any resource") + sys.exit(0) + for cleaner in self.cleaners: + cleaner.clean() diff --git a/nfvbench/nfvbench.py b/nfvbench/nfvbench.py index 5e2de76..90b16d4 100644 --- a/nfvbench/nfvbench.py +++ b/nfvbench/nfvbench.py @@ -30,6 +30,7 @@ from pkg_resources import resource_string from __init__ import __version__ from chain_runner import ChainRunner +from cleanup import Cleaner from config import config_load from config import config_loads import credentials as credentials @@ -49,6 +50,7 @@ fluent_logger = None class NFVBench(object): """Main class of NFV benchmarking tool.""" + STATUS_OK = 'OK' STATUS_ERROR = 'ERROR' @@ -97,8 +99,8 @@ class NFVBench(object): try: if int(frame_size) < int(min_packet_size): new_frame_sizes.append(min_packet_size) - LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to " - + "traffic generator restriction", frame_size, min_packet_size) + LOG.info("Adjusting frame size %s Bytes to minimum size %s Bytes due to " + + "traffic generator restriction", frame_size, min_packet_size) else: new_frame_sizes.append(frame_size) except ValueError: @@ -141,7 +143,7 @@ class NFVBench(object): } def prepare_summary(self, result): - """Prepares summary of the result to print and send it to logger (eg: fluentd)""" + """Prepare summary of the result to print and send it to logger (eg: fluentd).""" global fluent_logger summary = NFVBenchSummarizer(result, fluent_logger) LOG.info(str(summary)) @@ -223,12 +225,12 @@ class NFVBench(object): if self.config.openrc_file: self.config.openrc_file = os.path.expanduser(self.config.openrc_file) - self.config.ndr_run = (not self.config.no_traffic - and 'ndr' in self.config.rate.strip().lower().split('_')) - self.config.pdr_run = (not self.config.no_traffic - and 'pdr' in self.config.rate.strip().lower().split('_')) - self.config.single_run = (not self.config.no_traffic - and not (self.config.ndr_run or self.config.pdr_run)) + self.config.ndr_run = (not self.config.no_traffic and + 'ndr' in self.config.rate.strip().lower().split('_')) + self.config.pdr_run = (not self.config.no_traffic and + 'pdr' in self.config.rate.strip().lower().split('_')) + self.config.single_run = (not self.config.no_traffic and + not (self.config.ndr_run or self.config.pdr_run)) if self.config.vlans and len(self.config.vlans) != 2: raise Exception('Number of configured VLAN IDs for VLAN tagging must be exactly 2.') @@ -252,6 +254,11 @@ class NFVBench(object): def parse_opts_from_cli(): parser = argparse.ArgumentParser() + parser.add_argument('--status', dest='status', + action='store_true', + default=None, + help='Provide NFVbench status') + parser.add_argument('-c', '--config', dest='config', action='store', help='Override default values with a config file or ' @@ -366,6 +373,16 @@ def parse_opts_from_cli(): action='store_true', help='no cleanup after run') + parser.add_argument('--cleanup', dest='cleanup', + default=None, + action='store_true', + help='Cleanup NFVbench resources (prompt to confirm)') + + parser.add_argument('--force-cleanup', dest='force_cleanup', + default=None, + action='store_true', + help='Cleanup NFVbench resources (do not prompt)') + parser.add_argument('--json', dest='json', action='store', help='store results in json format file', @@ -429,8 +446,7 @@ def load_default_config(): def override_custom_traffic(config, frame_sizes, unidir): - """Override the traffic profiles with a custom one - """ + """Override the traffic profiles with a custom one.""" if frame_sizes is not None: traffic_profile_name = "custom_traffic_profile" config.traffic_profile = [ @@ -457,6 +473,23 @@ def check_physnet(name, netattrs): raise Exception("SRIOV requires segmentation_id to be specified for the {n} network" .format(n=name)) +def status_cleanup(config, cleanup, force_cleanup): + LOG.info('Version: %s', pbr.version.VersionInfo('nfvbench').version_string_with_vcs()) + # check if another run is pending + ret_code = 0 + try: + with utils.RunLock(): + LOG.info('Status: idle') + except Exception: + LOG.info('Status: busy (run pending)') + ret_code = 1 + # check nfvbench resources + if config.openrc_file and config.service_chain != ChainType.EXT: + cleaner = Cleaner(config) + count = cleaner.show_resources() + if count and (cleanup or force_cleanup): + cleaner.clean(not force_cleanup) + sys.exit(ret_code) def main(): global fluent_logger @@ -566,6 +599,9 @@ def main(): # in a copy of the dict (config plugin still holds the original dict) config_plugin.set_config(config) + if opts.status or opts.cleanup or opts.force_cleanup: + status_cleanup(config, opts.cleanup, opts.force_cleanup) + # add file log if requested if config.log_file: log.add_file_logger(config.log_file) diff --git a/setup.cfg b/setup.cfg index 0938968..e1b1ddf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,6 @@ packages = console_scripts = nfvbench = nfvbench.nfvbench:main nfvbench_client = client.nfvbench_client:main - nfvbench_cleanup = cleanup.nfvbench_cleanup:main [compile_catalog] directory = nfvbench/locale diff --git a/tox.ini b/tox.ini index 5aa8997..354740f 100644 --- a/tox.ini +++ b/tox.ini @@ -40,6 +40,6 @@ show-source = True #H404: multi line docstring should start without a leading new line #H405: multi line docstring summary not separated with an empty line #H904: Wrap long lines in parentheses instead of a backslash -ignore = E123,E125,H803,E302,E303,H104,H233,H236,H302,H404,H405,H904 +ignore = E123,E125,H803,E302,E303,H104,H233,H236,H302,H404,H405,H904,D102,D100,D107 builtins = _ exclude=venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,dib-venv -- cgit 1.2.3-korg