[NFVBENCH-83] Add option to display status and to cleanup 81/55281/3
authorahothan <ahothan@cisco.com>
Mon, 9 Apr 2018 23:57:25 +0000 (16:57 -0700)
committerahothan <ahothan@cisco.com>
Tue, 10 Apr 2018 00:11:15 +0000 (17:11 -0700)
Change-Id: If135fedee4e5ee9226a262800917c4c35bc83bc7
Signed-off-by: ahothan <ahothan@cisco.com>
cleanup/__init__.py [deleted file]
cleanup/nfvbench_cleanup.py [deleted file]
docs/testing/user/userguide/advanced.rst
nfvbench/cleanup.py [new file with mode: 0644]
nfvbench/nfvbench.py
setup.cfg
tox.ini

diff --git a/cleanup/__init__.py b/cleanup/__init__.py
deleted file mode 100644 (file)
index 04924ec..0000000
+++ /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 (file)
index 1520647..0000000
+++ /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='<file>')
-    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='<file>')
-    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='<any-python-regex>')
-    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()
index 252cbc9..02c7fce 100644 (file)
@@ -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 (file)
index 0000000..246be3f
--- /dev/null
@@ -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()
index 5e2de76..90b16d4 100644 (file)
@@ -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)
index 0938968..e1b1ddf 100644 (file)
--- 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 (file)
--- 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