Add tempest post-run cleanup script
Implemented all features defined in blueprint.
Change-Id: If3c9b82f85095eeb22bbf32841be534024213567
Implements: blueprint post-run-cleanup
diff --git a/tempest/cmd/cleanup.py b/tempest/cmd/cleanup.py
new file mode 100644
index 0000000..9ae3dfb
--- /dev/null
+++ b/tempest/cmd/cleanup.py
@@ -0,0 +1,301 @@
+#!/usr/bin/env python
+#
+# Copyright 2014 Dell Inc.
+# 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.
+# @author: David Paterson
+
+"""
+Utility for cleaning up environment after Tempest run
+
+Runtime Arguments
+-----------------
+
+--init-saved-state: Before you can execute cleanup you must initialize
+the saved state by running it with the --init-saved-state flag
+(creating ./saved_state.json), which protects your deployment from
+cleanup deleting objects you want to keep. Typically you would run
+cleanup with --init-saved-state prior to a tempest run. If this is not
+the case saved_state.json must be edited, removing objects you want
+cleanup to delete.
+
+--dry-run: Creates a report (dry_run.json) of the tenants that will be
+cleaned up (in the "_tenants_to_clean" array), and the global objects
+that will be removed (tenants, users, flavors and images). Once
+cleanup is executed in normal mode, running it again with --dry-run
+should yield an empty report.
+
+**NOTE**: The _tenants_to_clean array in dry-run.json lists the
+tenants that cleanup will loop through and delete child objects, not
+delete the tenant itself. This may differ from the tenants array as you
+can clean the tempest and alternate tempest tenants but not delete the
+tenants themselves. This is actually the default behavior.
+
+**Normal mode**: running with no arguments, will query your deployment and
+build a list of objects to delete after filtering out out the objects
+found in saved_state.json and based on the
+--preserve-tempest-conf-objects and
+--delete-tempest-conf-objects flags.
+
+By default the tempest and alternate tempest users and tenants are not
+deleted and the admin user specified in tempest.conf is never deleted.
+
+Please run with --help to see full list of options.
+"""
+import argparse
+import json
+import sys
+
+from tempest import auth
+from tempest import clients
+from tempest.cmd import cleanup_service
+from tempest import config
+from tempest.openstack.common import log as logging
+
+SAVED_STATE_JSON = "saved_state.json"
+DRY_RUN_JSON = "dry_run.json"
+LOG = logging.getLogger(__name__)
+CONF = config.CONF
+
+
+class Cleanup(object):
+
+ def __init__(self):
+ self.admin_mgr = clients.AdminManager()
+ self.dry_run_data = {}
+ self.json_data = {}
+ self._init_options()
+
+ self.admin_id = ""
+ self.admin_role_id = ""
+ self.admin_tenant_id = ""
+ self._init_admin_ids()
+
+ self.admin_role_added = []
+
+ # available services
+ self.tenant_services = cleanup_service.get_tenant_cleanup_services()
+ self.global_services = cleanup_service.get_global_cleanup_services()
+ cleanup_service.init_conf()
+
+ def run(self):
+ opts = self.options
+ if opts.init_saved_state:
+ self._init_state()
+ return
+
+ self._load_json()
+ self._cleanup()
+
+ def _cleanup(self):
+ LOG.debug("Begin cleanup")
+ is_dry_run = self.options.dry_run
+ is_preserve = self.options.preserve_tempest_conf_objects
+ is_save_state = False
+
+ if is_dry_run:
+ self.dry_run_data["_tenants_to_clean"] = {}
+ f = open(DRY_RUN_JSON, 'w+')
+
+ admin_mgr = self.admin_mgr
+ # Always cleanup tempest and alt tempest tenants unless
+ # they are in saved state json. Therefore is_preserve is False
+ kwargs = {'data': self.dry_run_data,
+ 'is_dry_run': is_dry_run,
+ 'saved_state_json': self.json_data,
+ 'is_preserve': False,
+ 'is_save_state': is_save_state}
+ tenant_service = cleanup_service.TenantService(admin_mgr, **kwargs)
+ tenants = tenant_service.list()
+ LOG.debug("Process %s tenants" % len(tenants))
+
+ # Loop through list of tenants and clean them up.
+ for tenant in tenants:
+ self._add_admin(tenant['id'])
+ self._clean_tenant(tenant)
+
+ kwargs = {'data': self.dry_run_data,
+ 'is_dry_run': is_dry_run,
+ 'saved_state_json': self.json_data,
+ 'is_preserve': is_preserve,
+ 'is_save_state': is_save_state}
+ for service in self.global_services:
+ svc = service(admin_mgr, **kwargs)
+ svc.run()
+
+ if is_dry_run:
+ f.write(json.dumps(self.dry_run_data, sort_keys=True,
+ indent=2, separators=(',', ': ')))
+ f.close()
+
+ self._remove_admin_user_roles()
+
+ def _remove_admin_user_roles(self):
+ tenant_ids = self.admin_role_added
+ LOG.debug("Removing admin user roles where needed for tenants: %s"
+ % tenant_ids)
+ for tenant_id in tenant_ids:
+ self._remove_admin_role(tenant_id)
+
+ def _clean_tenant(self, tenant):
+ LOG.debug("Cleaning tenant: %s " % tenant['name'])
+ is_dry_run = self.options.dry_run
+ dry_run_data = self.dry_run_data
+ is_preserve = self.options.preserve_tempest_conf_objects
+ tenant_id = tenant['id']
+ tenant_name = tenant['name']
+ tenant_data = None
+ if is_dry_run:
+ tenant_data = dry_run_data["_tenants_to_clean"][tenant_id] = {}
+ tenant_data['name'] = tenant_name
+
+ kwargs = {"username": CONF.identity.admin_username,
+ "password": CONF.identity.admin_password,
+ "tenant_name": tenant['name']}
+ mgr = clients.Manager(credentials=auth.get_credentials(**kwargs))
+ kwargs = {'data': tenant_data,
+ 'is_dry_run': is_dry_run,
+ 'saved_state_json': None,
+ 'is_preserve': is_preserve,
+ 'is_save_state': False,
+ 'tenant_id': tenant_id}
+ for service in self.tenant_services:
+ svc = service(mgr, **kwargs)
+ svc.run()
+
+ def _init_admin_ids(self):
+ id_cl = self.admin_mgr.identity_client
+
+ tenant = id_cl.get_tenant_by_name(CONF.identity.admin_tenant_name)
+ self.admin_tenant_id = tenant['id']
+
+ user = id_cl.get_user_by_username(self.admin_tenant_id,
+ CONF.identity.admin_username)
+ self.admin_id = user['id']
+
+ _, roles = id_cl.list_roles()
+ for role in roles:
+ if role['name'] == CONF.identity.admin_role:
+ self.admin_role_id = role['id']
+ break
+
+ def _init_options(self):
+ parser = argparse.ArgumentParser(
+ description='Cleanup after tempest run')
+ parser.add_argument('--init-saved-state', action="store_true",
+ dest='init_saved_state', default=False,
+ help="Creates JSON file: " + SAVED_STATE_JSON +
+ ", representing the current state of your "
+ "deployment, specifically objects types "
+ "Tempest creates and destroys during a run. "
+ "You must run with this flag prior to "
+ "executing cleanup.")
+ parser.add_argument('--preserve-tempest-conf-objects',
+ action="store_true",
+ dest='preserve_tempest_conf_objects',
+ default=True, help="Do not delete the "
+ "tempest and alternate tempest users and "
+ "tenants, so they may be used for future "
+ "tempest runs. By default this is argument "
+ "is true.")
+ parser.add_argument('--delete-tempest-conf-objects',
+ action="store_false",
+ dest='preserve_tempest_conf_objects',
+ default=False,
+ help="Delete the tempest and "
+ "alternate tempest users and tenants.")
+ parser.add_argument('--dry-run', action="store_true",
+ dest='dry_run', default=False,
+ help="Generate JSON file:" + DRY_RUN_JSON +
+ ", that reports the objects that would have "
+ "been deleted had a full cleanup been run.")
+
+ self.options = parser.parse_args()
+
+ def _add_admin(self, tenant_id):
+ id_cl = self.admin_mgr.identity_client
+ needs_role = True
+ _, roles = id_cl.list_user_roles(tenant_id, self.admin_id)
+ for role in roles:
+ if role['id'] == self.admin_role_id:
+ needs_role = False
+ LOG.debug("User already had admin privilege for this tenant")
+ if needs_role:
+ LOG.debug("Adding admin priviledge for : %s" % tenant_id)
+ id_cl.assign_user_role(tenant_id, self.admin_id,
+ self.admin_role_id)
+ self.admin_role_added.append(tenant_id)
+
+ def _remove_admin_role(self, tenant_id):
+ LOG.debug("Remove admin user role for tenant: %s" % tenant_id)
+ # Must initialize AdminManager for each user role
+ # Otherwise authentication exception is thrown, weird
+ id_cl = clients.AdminManager().identity_client
+ if (self._tenant_exists(tenant_id)):
+ try:
+ id_cl.remove_user_role(tenant_id, self.admin_id,
+ self.admin_role_id)
+ except Exception as ex:
+ LOG.exception("Failed removing role from tenant which still"
+ "exists, exception: %s" % ex)
+
+ def _tenant_exists(self, tenant_id):
+ id_cl = self.admin_mgr.identity_client
+ try:
+ t = id_cl.get_tenant(tenant_id)
+ LOG.debug("Tenant is: %s" % str(t))
+ return True
+ except Exception as ex:
+ LOG.debug("Tenant no longer exists? %s" % ex)
+ return False
+
+ def _init_state(self):
+ LOG.debug("Initializing saved state.")
+ data = {}
+ admin_mgr = self.admin_mgr
+ kwargs = {'data': data,
+ 'is_dry_run': False,
+ 'saved_state_json': data,
+ 'is_preserve': False,
+ 'is_save_state': True}
+ for service in self.global_services:
+ svc = service(admin_mgr, **kwargs)
+ svc.run()
+
+ f = open(SAVED_STATE_JSON, 'w+')
+ f.write(json.dumps(data,
+ sort_keys=True, indent=2, separators=(',', ': ')))
+ f.close()
+
+ def _load_json(self):
+ try:
+ json_file = open(SAVED_STATE_JSON)
+ self.json_data = json.load(json_file)
+ json_file.close()
+ except IOError as ex:
+ LOG.exception("Failed loading saved state, please be sure you"
+ " have first run cleanup with --init-saved-state "
+ "flag prior to running tempest. Exception: %s" % ex)
+ sys.exit(ex)
+ except Exception as ex:
+ LOG.exception("Exception parsing saved state json : %s" % ex)
+ sys.exit(ex)
+
+
+def main():
+ cleanup = Cleanup()
+ cleanup.run()
+ LOG.info('Cleanup finished!')
+ return 0
+
+if __name__ == "__main__":
+ sys.exit(main())