| # 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. |
| |
| """ |
| Utility for cleaning up environment after Tempest test run |
| |
| **Usage:** ``tempest cleanup [--help] [OPTIONS]`` |
| |
| If run with no arguments, ``tempest cleanup`` will query your OpenStack |
| deployment and build a list of resources to delete and destroy them. This list |
| will exclude the resources from ``saved_state.json`` and will include the |
| configured admin account if the ``--delete-tempest-conf-objects`` flag is |
| specified. By default the admin project is not deleted and the admin user |
| specified in ``tempest.conf`` is never deleted. |
| |
| Example Run |
| ----------- |
| |
| .. warning:: |
| |
| We advice not to run tempest cleanup on production environments. |
| |
| .. warning:: |
| |
| If step 1 is skipped in the example below, the cleanup procedure |
| may delete resources that existed in the cloud before the test run. This |
| may cause an unwanted destruction of cloud resources, so use caution with |
| this command. |
| |
| Examples:: |
| |
| $ tempest cleanup --init-saved-state |
| $ # Actual running of Tempest tests |
| $ tempest cleanup |
| |
| Runtime Arguments |
| ----------------- |
| |
| * ``--init-saved-state``: Initializes the saved state of the OpenStack |
| deployment and will output a ``saved_state.json`` file containing resources |
| from your deployment that will be preserved from the cleanup command. This |
| should be done prior to running Tempest tests. Note, that if other users of |
| your cloud could have created resources after running ``--init-saved-state``, |
| it would not protect those resources as they wouldn't be present in the |
| saved_state.json file. |
| |
| * ``--delete-tempest-conf-objects``: If option is present, then the command |
| will delete the admin project in addition to the resources associated with |
| them on clean up. If option is not present, the command will delete the |
| resources associated with the Tempest and alternate Tempest users and |
| projects but will not delete the projects themselves. |
| |
| * ``--dry-run``: Creates a report (``./dry_run.json``) of the projects that |
| will be cleaned up (in the ``_projects_to_clean`` dictionary [1]_) and the |
| global objects that will be removed (domains, flavors, images, roles, |
| projects, and users). Once the cleanup command is executed (e.g. run without |
| parameters), running it again with ``--dry-run`` should yield an empty |
| report. We STRONGLY ENCOURAGE to run ``tempest cleanup`` with ``--dry-run`` |
| first and then verify that the resources listed in the ``dry_run.json`` file |
| are meant to be deleted. |
| |
| * ``--prefix``: Only resources that match the prefix will be deleted. When this |
| option is used, ``saved_state.json`` file is not needed (no need to run with |
| ``--init-saved-state`` first). |
| |
| All tempest resources are created with the prefix value from the config |
| option ``resource_name_prefix`` in tempest.conf. To cleanup only the |
| resources created by tempest, you should use the prefix set in your |
| tempest.conf (the default value of ``resource_name_prefix`` is ``tempest``. |
| |
| Note, that some resources are not named thus they will not be deleted when |
| filtering based on the prefix. This option will be ignored when |
| ``--init-saved-state`` is used so that it can capture the true init state - |
| all resources present at that moment. If there is any ``saved_state.json`` |
| file present (e.g. if you ran the tempest cleanup with ``--init-saved-state`` |
| before) and you run the tempest cleanup with ``--prefix``, the |
| ``saved_state.json`` file will be ignored and cleanup will be done based on |
| the passed prefix only. |
| |
| * ``--help``: Print the help text for the command and parameters. |
| |
| .. [1] The ``_projects_to_clean`` dictionary in ``dry_run.json`` lists the |
| projects that ``tempest cleanup`` will loop through to delete child |
| objects, but the command will, by default, not delete the projects |
| themselves. This may differ from the ``projects`` list as you can clean |
| the Tempest and alternate Tempest users and projects but they will not be |
| deleted unless the ``--delete-tempest-conf-objects`` flag is used to |
| force their deletion. |
| |
| .. note:: |
| |
| If during execution of ``tempest cleanup`` NotImplemented exception |
| occurres, ``tempest cleanup`` won't fail on that, it will be logged only. |
| NotImplemented errors are ignored because they are an outcome of some |
| extensions being disabled and ``tempest cleanup`` is not checking their |
| availability as it tries to clean up as much as possible without any |
| complicated logic. |
| |
| """ |
| import sys |
| import traceback |
| |
| from cliff import command |
| from oslo_log import log as logging |
| from oslo_serialization import jsonutils as json |
| |
| from tempest import clients |
| from tempest.cmd import cleanup_service |
| from tempest.common import credentials_factory as credentials |
| from tempest import config |
| from tempest.lib import exceptions |
| |
| SAVED_STATE_JSON = "saved_state.json" |
| DRY_RUN_JSON = "dry_run.json" |
| LOG = logging.getLogger(__name__) |
| CONF = config.CONF |
| |
| |
| class TempestCleanup(command.Command): |
| |
| GOT_EXCEPTIONS = [] |
| |
| def take_action(self, parsed_args): |
| try: |
| self.init(parsed_args) |
| if not parsed_args.init_saved_state: |
| self._cleanup() |
| except Exception: |
| LOG.exception("Failure during cleanup") |
| traceback.print_exc() |
| raise |
| # ignore NotImplemented errors as those are an outcome of some |
| # extensions being disabled and cleanup is not checking their |
| # availability as it tries to clean up as much as possible without |
| # any complicated logic |
| critical_exceptions = [ex for ex in self.GOT_EXCEPTIONS if |
| not isinstance(ex, exceptions.NotImplemented)] |
| if critical_exceptions: |
| raise Exception(self.GOT_EXCEPTIONS) |
| |
| def init(self, parsed_args): |
| # set new handler for logging to stdout, by default only INFO messages |
| # are logged to stdout |
| stdout_handler = logging.logging.StreamHandler() |
| # debug argument is defined in cliff already |
| if self.app_args.debug: |
| stdout_handler.level = logging.DEBUG |
| else: |
| stdout_handler.level = logging.INFO |
| LOG.handlers.append(stdout_handler) |
| |
| cleanup_service.init_conf() |
| self.options = parsed_args |
| self.admin_mgr = clients.Manager( |
| credentials.get_configured_admin_credentials()) |
| self.dry_run_data = {} |
| self.json_data = {} |
| |
| # available services |
| self.project_associated_services = ( |
| cleanup_service.get_project_associated_cleanup_services()) |
| self.resource_cleanup_services = ( |
| cleanup_service.get_resource_cleanup_services()) |
| self.global_services = cleanup_service.get_global_cleanup_services() |
| |
| if parsed_args.init_saved_state: |
| self._init_state() |
| return |
| |
| self._load_json() |
| |
| def _cleanup(self): |
| LOG.info("Begin cleanup") |
| is_dry_run = self.options.dry_run |
| is_preserve = not self.options.delete_tempest_conf_objects |
| is_save_state = False |
| cleanup_prefix = self.options.prefix |
| |
| if is_dry_run: |
| self.dry_run_data["_projects_to_clean"] = {} |
| |
| admin_mgr = self.admin_mgr |
| # Always cleanup tempest and alt tempest projects 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, |
| 'prefix': cleanup_prefix} |
| project_service = cleanup_service.ProjectService(admin_mgr, **kwargs) |
| projects = project_service.list() |
| LOG.info("Processing %s projects", len(projects)) |
| |
| # Loop through list of projects and clean them up. |
| for project in projects: |
| self._clean_project(project) |
| |
| 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, |
| 'prefix': cleanup_prefix, |
| 'got_exceptions': self.GOT_EXCEPTIONS} |
| LOG.info("Processing global services") |
| for service in self.global_services: |
| svc = service(admin_mgr, **kwargs) |
| svc.run() |
| |
| LOG.info("Processing services") |
| for service in self.resource_cleanup_services: |
| svc = service(self.admin_mgr, **kwargs) |
| svc.run() |
| |
| if is_dry_run: |
| with open(DRY_RUN_JSON, 'w+') as f: |
| f.write(json.dumps(self.dry_run_data, sort_keys=True, |
| indent=2, separators=(',', ': '))) |
| |
| def _clean_project(self, project): |
| LOG.debug("Cleaning project: %s ", project['name']) |
| is_dry_run = self.options.dry_run |
| dry_run_data = self.dry_run_data |
| is_preserve = not self.options.delete_tempest_conf_objects |
| project_id = project['id'] |
| project_name = project['name'] |
| project_data = None |
| cleanup_prefix = self.options.prefix |
| if is_dry_run: |
| project_data = dry_run_data["_projects_to_clean"][project_id] = {} |
| project_data['name'] = project_name |
| |
| kwargs = {'data': project_data, |
| 'is_dry_run': is_dry_run, |
| 'saved_state_json': self.json_data, |
| 'is_preserve': is_preserve, |
| 'is_save_state': False, |
| 'project_id': project_id, |
| 'prefix': cleanup_prefix, |
| 'got_exceptions': self.GOT_EXCEPTIONS} |
| for service in self.project_associated_services: |
| svc = service(self.admin_mgr, **kwargs) |
| svc.run() |
| |
| def get_parser(self, prog_name): |
| parser = super(TempestCleanup, self).get_parser(prog_name) |
| 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 object types " |
| "tempest creates and destroys during a run. " |
| "You must run with this flag prior to " |
| "executing cleanup in normal mode, which is with " |
| "no arguments.") |
| parser.add_argument('--delete-tempest-conf-objects', |
| action="store_true", |
| dest='delete_tempest_conf_objects', |
| default=False, |
| help="Force deletion of the tempest and " |
| "alternate tempest users and projects.") |
| 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.") |
| parser.add_argument('--prefix', dest='prefix', default=None, |
| help="Only resources that match the prefix will " |
| "be deleted (resources in saved_state.json are " |
| "not taken into account). All tempest resources " |
| "are created with the prefix value set by " |
| "resource_name_prefix in tempest.conf, default " |
| "prefix is tempest. Note that some resources are " |
| "not named thus they will not be deleted when " |
| "filtering based on the prefix. This opt will be " |
| "ignored when --init-saved-state is used so that " |
| "it can capture the true init state - all " |
| "resources present at that moment.") |
| return parser |
| |
| def get_description(self): |
| return ('tempest cleanup tool, read the full documentation before ' |
| 'using this tool. We advice not to run it on production ' |
| 'environments. On environments where also other users may ' |
| 'create resources, we strongly advice using --dry-run ' |
| 'argument first and verify the content of dry_run.json file.') |
| |
| def _init_state(self): |
| LOG.info("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, |
| # must be None as we want to capture true init state |
| # (all resources present) thus no filtering based |
| # on the prefix |
| 'prefix': None, |
| 'got_exceptions': self.GOT_EXCEPTIONS} |
| for service in self.global_services: |
| svc = service(admin_mgr, **kwargs) |
| svc.run() |
| |
| for service in self.project_associated_services: |
| svc = service(admin_mgr, **kwargs) |
| svc.run() |
| |
| for service in self.resource_cleanup_services: |
| svc = service(admin_mgr, **kwargs) |
| svc.run() |
| |
| with open(SAVED_STATE_JSON, 'w+') as f: |
| f.write(json.dumps(data, sort_keys=True, |
| indent=2, separators=(',', ': '))) |
| |
| def _load_json(self, saved_state_json=SAVED_STATE_JSON): |
| try: |
| with open(saved_state_json, 'rb') as json_file: |
| self.json_data = json.load(json_file) |
| |
| 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) |