diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 692f245878..c0067c8ff1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -20,6 +20,11 @@ Changelog [Unreleased] ------------ +Added +~~~~~ + +* Introducing ``rally env cleanup`` command for performing disaster cleanup. + Changed ~~~~~~~ @@ -30,10 +35,11 @@ Changed allows us to show only those workloads which we are interested in (see the examples below). Examples: - 1. show only failed workloads - ``rally task detailed --filter-by sla-failures`` - 2. show only those workloads which include the next scenario plugin(s) - ``rally task detailed --filter-by scenarios=scenario1[,scenarios2...]`` + + 1. show only failed workloads + ``rally task detailed --filter-by sla-failures`` + 2. show only those workloads which include the next scenario plugin(s) + ``rally task detailed --filter-by scenarios=scenario1[,scenarios2...]`` Removed ~~~~~~~ diff --git a/etc/rally.bash_completion b/etc/rally.bash_completion index bd64fd00fc..96946f061f 100644 --- a/etc/rally.bash_completion +++ b/etc/rally.bash_completion @@ -33,6 +33,7 @@ _rally() OPTS["deployment_show"]="--deployment" OPTS["deployment_use"]="--deployment" OPTS["env_check"]="--env --json --detailed" + OPTS["env_cleanup"]="--json --env" OPTS["env_create"]="--name --description --extras --from-sysenv --spec --json --no-use" OPTS["env_delete"]="--env --force" OPTS["env_destroy"]="--env --skip-cleanup --json --detailed" diff --git a/rally/cli/commands/env.py b/rally/cli/commands/env.py index ed74ba8139..628807e8d4 100644 --- a/rally/cli/commands/env.py +++ b/rally/cli/commands/env.py @@ -101,6 +101,48 @@ class EnvCommands(object): self._show(env.data, to_json=to_json, only_spec=False) return 0 + @cliutils.args("--json", action="store_true", dest="to_json", + help="Format output as JSON.") + @cliutils.args("--env", dest="env", type=str, + metavar="", required=False, + help="UUID or name of the env.") + @envutils.with_default_env() + def cleanup(self, api, env=None, to_json=False): + """Perform disaster cleanup for specified environment. + + Cases when Rally can leave undeleted resources after performing + workload: + + - Rally execution was interrupted and cleanup was not performed + - The environment or a particular platform became unreachable which + fail Rally execution of cleanup + """ + env = env_mgr.EnvManager.get(env) + _print("Cleaning up resources for %s" % env, to_json) + result = env.cleanup() + + if to_json: + print(json.dumps(result, indent=2)) + return int(any([p["errors"] for p in result.values()])) + + print("Cleaning is finished. See the results bellow.") + + return_code = 0 + for platform in sorted(result): + cleanup_info = result[platform] + print("\nInformation for %s platform." % platform) + print("=" * 80) + print("Status: %s" % cleanup_info["message"]) + for key in ("discovered", "deleted", "failed"): + print("Total %s: %s" % (key, cleanup_info[key])) + if cleanup_info["errors"]: + return_code = 1 + errors = "\t- ".join(e["message"] + for e in cleanup_info["errors"]) + print("Errors:\n\t- %s" % errors) + + return return_code + @cliutils.args("--env", dest="env", type=str, metavar="", required=False, help="UUID or name of the env.") @@ -124,6 +166,7 @@ class EnvCommands(object): % (NO, env, result["destroy_info"]["message"]), to_json) else: _print("%s Successfully destroyed env %s" % (YES, env), to_json) + if detailed or to_json: print(json.dumps(result, indent=2)) @@ -136,7 +179,7 @@ class EnvCommands(object): help="Delete DB records even if env is not destroyed.") @envutils.with_default_env() def delete(self, api, env=None, force=False): - """Deletes all records related to Env from db.""" + """Deletes all records related to the environment from db.""" env_mgr.EnvManager.get(env).delete(force=force) # TODO(boris-42): clear env variables if default one is deleted @@ -196,6 +239,7 @@ class EnvCommands(object): @cliutils.suppress_warnings @envutils.with_default_env() def show(self, api, env=None, to_json=False, only_spec=False): + """Show base information about the environment record.""" env_data = env_mgr.EnvManager.get(env).data self._show(env_data, to_json=to_json, only_spec=only_spec) @@ -206,7 +250,7 @@ class EnvCommands(object): help="Format output as JSON.") @envutils.with_default_env() def info(self, api, env=None, to_json=False): - """Show environment information.""" + """Retrieve and show environment information.""" env = env_mgr.EnvManager.get(env) env_info = env.get_info() return_code = int(any(v.get("error") for v in env_info.values())) diff --git a/tests/unit/cli/commands/test_env.py b/tests/unit/cli/commands/test_env.py index ffaabb684a..fe7dac27e2 100644 --- a/tests/unit/cli/commands/test_env.py +++ b/tests/unit/cli/commands/test_env.py @@ -153,6 +153,86 @@ class EnvCommandsTestCase(test.TestCase): mock.call(mock.ANY) ]) + @mock.patch("rally.env.env_mgr.EnvManager.get") + @mock.patch("rally.cli.commands.env.print") + def test_cleanup(self, mock_print, mock_env_manager_get): + env_ = mock.Mock() + env_inst = mock_env_manager_get.return_value + env_inst.cleanup.return_value = { + "existing@docker": { + "message": "Success", + "discovered": 5, + "deleted": 5, + "failed": 0, + "errors": [] + }, + "existing@openstack": { + "message": "It is OpenStack. several failures are ok :)", + "discovered": 10, + "deleted": 8, + "failed": 2, + "errors": [ + {"message": "Port disappeared", + "traceback": "traceback"} + ] + } + } + self.assertEqual(1, self.env.cleanup(self.api, env_)) + mock_env_manager_get.assert_called_once_with(env_) + env_inst.cleanup.assert_called_once_with() + + actual_print = "\n".join( + [call_args[0] + for call_args, _call_kwargs in mock_print.call_args_list]) + expected_print = ( + "Cleaning up resources for %(env)s\n" + "Cleaning is finished. See the results bellow.\n" + "\n" + "Information for existing@docker platform.\n" + "%(hr)s\n" + "Status: Success\n" + "Total discovered: 5\n" + "Total deleted: 5\n" + "Total failed: 0\n" + "\n" + "Information for existing@openstack platform.\n" + "%(hr)s\n" + "Status: It is OpenStack. several failures are ok :)\n" + "Total discovered: 10\n" + "Total deleted: 8\n" + "Total failed: 2\n" + "Errors:\n" + "\t- Port disappeared" % {"env": env_inst, "hr": "=" * 80}) + self.assertEqual(expected_print, actual_print) + + @mock.patch("rally.env.env_mgr.EnvManager.get") + @mock.patch("rally.cli.commands.env.print") + def test_cleanup_to_json(self, mock_print, mock_env_manager_get): + env_ = mock.Mock() + env_inst = mock_env_manager_get.return_value + env_inst.cleanup.return_value = { + "existing@docker": { + "message": "Success", + "discovered": 5, + "deleted": 5, + "failed": 0, + "errors": [] + }, + "existing@openstack": { + "message": "It is OpenStack. several failures are ok :)", + "discovered": 10, + "deleted": 8, + "failed": 2, + "errors": [ + {"message": "Port disappeared", + "traceback": "traceback"} + ] + } + } + self.assertEqual(1, self.env.cleanup(self.api, env_, to_json=True)) + mock_print.assert_called_once_with( + json.dumps(env_inst.cleanup.return_value, indent=2)) + @mock.patch("rally.env.env_mgr.EnvManager.get") @mock.patch("rally.cli.commands.env.print") def test_destroy(self, mock_print, mock_env_manager_get):