diff --git a/mistralclient/api/base.py b/mistralclient/api/base.py index 7a71c100..c1f88299 100644 --- a/mistralclient/api/base.py +++ b/mistralclient/api/base.py @@ -173,7 +173,9 @@ class ResourceManager(object): for resource_data in resource] return self.resource_class(self, resource) - def _list(self, url, response_key=None, headers=None): + def _list(self, url, response_key=None, headers=None, + returned_res_cls=None): + try: resp = self.http_client.get(url, headers) except exceptions.HttpError as ex: @@ -182,7 +184,9 @@ class ResourceManager(object): if resp.status_code != 200: self._raise_api_exception(resp) - return [self.resource_class(self, resource_data) + resource_class = returned_res_cls or self.resource_class + + return [resource_class(self, resource_data) for resource_data in extract_json(resp, response_key)] def _get(self, url, response_key=None, headers=None): diff --git a/mistralclient/api/v2/executions.py b/mistralclient/api/v2/executions.py index c65bd62c..6ea9563d 100644 --- a/mistralclient/api/v2/executions.py +++ b/mistralclient/api/v2/executions.py @@ -98,6 +98,15 @@ class ExecutionManager(base.ResourceManager): return self._get('/executions/%s' % id) + def get_ex_sub_executions(self, id, errors_only='', max_depth=-1): + ex_sub_execs_path = '/executions/%s/executions%s' + params = '?max_depth=%s&errors_only=%s' % (max_depth, errors_only) + + return self._list( + ex_sub_execs_path % (id, params), + response_key='executions' + ) + def delete(self, id, force=None): self._ensure_not_empty(id=id) diff --git a/mistralclient/api/v2/tasks.py b/mistralclient/api/v2/tasks.py index 780eb8c4..0c037089 100644 --- a/mistralclient/api/v2/tasks.py +++ b/mistralclient/api/v2/tasks.py @@ -16,6 +16,7 @@ from oslo_serialization import jsonutils from mistralclient.api import base +from mistralclient.api.v2 import executions class Task(base.Resource): @@ -50,6 +51,16 @@ class TaskManager(base.ResourceManager): return self._get('/tasks/%s' % id) + def get_task_sub_executions(self, id, errors_only='', max_depth=-1): + task_sub_execs_path = '/tasks/%s/executions%s' + params = '?max_depth=%s&errors_only=%s' % (max_depth, errors_only) + + return self._list( + task_sub_execs_path % (id, params), + response_key='executions', + returned_res_cls=executions.Execution + ) + def rerun(self, task_ex_id, reset=True, env=None): url = '/tasks/%s' % task_ex_id diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py index 93ac74e6..d9c1f190 100644 --- a/mistralclient/commands/v2/executions.py +++ b/mistralclient/commands/v2/executions.py @@ -24,6 +24,7 @@ from oslo_serialization import jsonutils from osc_lib.command import command +from cliff.lister import Lister as cliff_lister from mistralclient.commands.v2 import base from mistralclient import utils @@ -510,3 +511,71 @@ class GetPublished(command.Command): LOG.debug("Task result is not JSON.") self.app.stdout.write(published or "\n") + + +class SubExecutionsBaseLister(cliff_lister): + + def _get_format_function(self): + raise NotImplementedError + + def _get_resources_function(self): + raise NotImplementedError + + def get_parser(self, prog_name): + parser = super(SubExecutionsBaseLister, self).get_parser(prog_name) + + parser.add_argument( + 'id', + metavar='ID', + help='origin id' + ) + parser.add_argument( + '--errors-only', + dest='errors_only', + action='store_true', + help='Only error paths will be included.' + ) + parser.add_argument( + '--max-depth', + dest='max_depth', + nargs='?', + type=int, + default=-1, + help='Maximum depth of the workflow execution tree. ' + 'If 0, only the root workflow execution and its ' + 'tasks will be included' + ) + + return parser + + def _get_resources(self, parsed_args): + resource_function = self._get_resources_function() + errors_only = parsed_args.errors_only or '' + + return resource_function( + parsed_args.id, + errors_only=errors_only, + max_depth=parsed_args.max_depth, + ) + + def take_action(self, parsed_args): + format_func = self._get_format_function() + execs_list = self._get_resources(parsed_args) + + if not isinstance(execs_list, list): + execs_list = [execs_list] + + data = [format_func(r)[1] for r in execs_list] + + return (format_func()[0], data) if data else format_func() + + +class SubExecutionsLister(SubExecutionsBaseLister): + + def _get_format_function(self): + return ExecutionFormatter.format_list + + def _get_resources_function(self): + mistral_client = self.app.client_manager.workflow_engine + + return mistral_client.executions.get_ex_sub_executions diff --git a/mistralclient/commands/v2/tasks.py b/mistralclient/commands/v2/tasks.py index 1d7ab5eb..655f2716 100644 --- a/mistralclient/commands/v2/tasks.py +++ b/mistralclient/commands/v2/tasks.py @@ -23,6 +23,7 @@ from oslo_serialization import jsonutils from osc_lib.command import command from mistralclient.commands.v2 import base +from mistralclient.commands.v2 import executions from mistralclient import utils LOG = logging.getLogger(__name__) @@ -211,3 +212,14 @@ class Rerun(command.ShowOne): ) return TaskFormatter.format(execution) + + +class SubExecutionsLister(executions.SubExecutionsBaseLister): + + def _get_format_function(self): + return executions.ExecutionFormatter.format_list + + def _get_resources_function(self): + mistral_client = self.app.client_manager.workflow_engine + + return mistral_client.tasks.get_task_sub_executions diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 432a4252..e1fe93e6 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -732,10 +732,14 @@ class MistralShell(app.App): mistralclient.commands.v2.executions.GetReport, 'execution-get-published': mistralclient.commands.v2.executions.GetPublished, + 'execution-get-sub-executions': + mistralclient.commands.v2.executions.SubExecutionsLister, 'task-list': mistralclient.commands.v2.tasks.List, 'task-get': mistralclient.commands.v2.tasks.Get, 'task-get-published': mistralclient.commands.v2.tasks.GetPublished, 'task-get-result': mistralclient.commands.v2.tasks.GetResult, + 'task-get-sub-executions': + mistralclient.commands.v2.tasks.SubExecutionsLister, 'task-rerun': mistralclient.commands.v2.tasks.Rerun, 'action-list': mistralclient.commands.v2.actions.List, 'action-get': mistralclient.commands.v2.actions.Get, diff --git a/mistralclient/tests/unit/v2/test_cli_executions.py b/mistralclient/tests/unit/v2/test_cli_executions.py index 0f0896bd..869d2b14 100644 --- a/mistralclient/tests/unit/v2/test_cli_executions.py +++ b/mistralclient/tests/unit/v2/test_cli_executions.py @@ -53,7 +53,7 @@ SUB_WF_EXEC = executions.Execution( 'workflow_namespace': '', 'root_execution_id': 'ROOT_EXECUTION_ID', 'description': '', - 'state': 'RUNNING', + 'state': 'ERROR', 'state_info': None, 'created_at': '1', 'updated_at': '1', @@ -83,12 +83,13 @@ SUB_WF_EX_RESULT = ( '', 'abc', 'ROOT_EXECUTION_ID', - 'RUNNING', + 'ERROR', None, '1', '1' ) +EXECS_LIST = [EXEC, SUB_WF_EXEC] EXEC_PUBLISHED = {"bar1": "val1", "var2": 2} EXEC_WITH_PUBLISHED_DICT = EXEC_DICT.copy() EXEC_WITH_PUBLISHED_DICT.update( @@ -241,6 +242,61 @@ class TestCLIExecutionsV2(base.BaseCommandTest): result[1] ) + def test_sub_executions(self): + self.client.executions.get_ex_sub_executions.return_value = \ + EXECS_LIST + + result = self.call( + execution_cmd.SubExecutionsLister, + app_args=[EXEC_DICT['id']] + ) + + self.assertEqual([EX_RESULT, SUB_WF_EX_RESULT], result[1]) + self.assertEqual( + 1, + self.client.executions.get_ex_sub_executions.call_count + ) + self.assertEqual( + [mock.call(EXEC_DICT['id'], errors_only='', max_depth=-1)], + self.client.executions.get_ex_sub_executions.call_args_list + ) + + def test_sub_executions_errors_only(self): + self.client.executions.get_ex_sub_executions.return_value = \ + EXECS_LIST + + self.call( + execution_cmd.SubExecutionsLister, + app_args=[EXEC_DICT['id'], '--errors-only'] + ) + + self.assertEqual( + 1, + self.client.executions.get_ex_sub_executions.call_count + ) + self.assertEqual( + [mock.call(EXEC_DICT['id'], errors_only=True, max_depth=-1)], + self.client.executions.get_ex_sub_executions.call_args_list + ) + + def test_sub_executions_with_max_depth(self): + self.client.executions.get_ex_sub_executions.return_value = \ + EXECS_LIST + + self.call( + execution_cmd.SubExecutionsLister, + app_args=[EXEC_DICT['id'], '--max-depth', '3'] + ) + + self.assertEqual( + 1, + self.client.executions.get_ex_sub_executions.call_count + ) + self.assertEqual( + [mock.call(EXEC_DICT['id'], errors_only='', max_depth=3)], + self.client.executions.get_ex_sub_executions.call_args_list + ) + def test_list_with_pagination(self): self.client.executions.list.return_value = [EXEC] diff --git a/mistralclient/tests/unit/v2/test_cli_tasks.py b/mistralclient/tests/unit/v2/test_cli_tasks.py index 9075f72b..91bc2f69 100644 --- a/mistralclient/tests/unit/v2/test_cli_tasks.py +++ b/mistralclient/tests/unit/v2/test_cli_tasks.py @@ -19,6 +19,7 @@ from oslo_serialization import jsonutils import mock +from mistralclient.api.v2.executions import Execution from mistralclient.api.v2 import tasks from mistralclient.commands.v2 import tasks as task_cmd from mistralclient.tests.unit import base @@ -35,6 +36,37 @@ TASK_DICT = { 'updated_at': '1', } +TASK_SUB_WF_EXEC = Execution( + mock, + { + 'id': '456', + 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', + 'workflow_name': 'some_sub_wf', + 'workflow_namespace': '', + 'root_execution_id': 'ROOT_EXECUTION_ID', + 'description': '', + 'state': 'ERROR', + 'state_info': None, + 'created_at': '1', + 'updated_at': '1', + 'task_execution_id': '123' + } +) + +TASK_SUB_WF_EX_RESULT = ( + '456', + '123e4567-e89b-12d3-a456-426655440000', + 'some_sub_wf', + '', + '', + '123', + 'ROOT_EXECUTION_ID', + 'ERROR', + None, + '1', + '1' +) + TASK_RESULT = {"test": "is", "passed": "successfully"} TASK_PUBLISHED = {"bar1": "val1", "var2": 2} @@ -131,3 +163,58 @@ class TestCLITasksV2(base.BaseCommandTest): ) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) + + def test_sub_executions(self): + self.client.tasks.get_task_sub_executions.return_value = \ + TASK_SUB_WF_EXEC + + result = self.call( + task_cmd.SubExecutionsLister, + app_args=[TASK_DICT['id']] + ) + + self.assertEqual([TASK_SUB_WF_EX_RESULT], result[1]) + self.assertEqual( + 1, + self.client.tasks.get_task_sub_executions.call_count + ) + self.assertEqual( + [mock.call(TASK_DICT['id'], errors_only='', max_depth=-1)], + self.client.tasks.get_task_sub_executions.call_args_list + ) + + def test_sub_executions_errors_only(self): + self.client.tasks.get_task_sub_executions.return_value = \ + TASK_SUB_WF_EXEC + + self.call( + task_cmd.SubExecutionsLister, + app_args=[TASK_DICT['id'], '--errors-only'] + ) + + self.assertEqual( + 1, + self.client.tasks.get_task_sub_executions.call_count + ) + self.assertEqual( + [mock.call(TASK_DICT['id'], errors_only=True, max_depth=-1)], + self.client.tasks.get_task_sub_executions.call_args_list + ) + + def test_sub_executions_with_max_depth(self): + self.client.tasks.get_task_sub_executions.return_value = \ + TASK_SUB_WF_EXEC + + self.call( + task_cmd.SubExecutionsLister, + app_args=[TASK_DICT['id'], '--max-depth', '3'] + ) + + self.assertEqual( + 1, + self.client.tasks.get_task_sub_executions.call_count + ) + self.assertEqual( + [mock.call(TASK_DICT['id'], errors_only='', max_depth=3)], + self.client.tasks.get_task_sub_executions.call_args_list + ) diff --git a/mistralclient/tests/unit/v2/test_executions.py b/mistralclient/tests/unit/v2/test_executions.py index 9725939c..a20b48c5 100644 --- a/mistralclient/tests/unit/v2/test_executions.py +++ b/mistralclient/tests/unit/v2/test_executions.py @@ -52,10 +52,23 @@ SUB_WF_EXEC = { } } +ERROR_SUB_WF_EXEC = { + 'id': "456", + 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', + 'workflow_name': 'my_sub_wf', + 'workflow_namespace': '', + 'task_execution_id': "abc", + 'description': '', + 'state': 'ERROR', + 'input': {} +} + + SOURCE_EXEC = EXEC SOURCE_EXEC['source_execution_id'] = EXEC['workflow_id'] URL_TEMPLATE = '/executions' URL_TEMPLATE_ID = '/executions/%s' +URL_TEMPLATE_SUB_EXECUTIONS = '/executions/%s/executions%s' class TestExecutionsV2(base.BaseClientV2Test): @@ -279,3 +292,21 @@ class TestExecutionsV2(base.BaseClientV2Test): report = self.executions.get_report(EXEC['id']) self.assertDictEqual(expected_json, report) + + def test_get_sub_executions(self): + url = self.TEST_URL + URL_TEMPLATE_SUB_EXECUTIONS \ + % (EXEC['id'], '?max_depth=-1&errors_only=') + + self.requests_mock.get(url, json={'executions': [EXEC, SUB_WF_EXEC]}) + + sub_execution_list = self.executions.get_ex_sub_executions(EXEC['id']) + + self.assertEqual(2, len(sub_execution_list)) + self.assertDictEqual( + executions.Execution(self.executions, EXEC).to_dict(), + sub_execution_list[0].to_dict() + ) + self.assertDictEqual( + executions.Execution(self.executions, SUB_WF_EXEC).to_dict(), + sub_execution_list[1].to_dict() + ) diff --git a/mistralclient/tests/unit/v2/test_tasks.py b/mistralclient/tests/unit/v2/test_tasks.py index 92299e5e..acb41ff2 100644 --- a/mistralclient/tests/unit/v2/test_tasks.py +++ b/mistralclient/tests/unit/v2/test_tasks.py @@ -15,6 +15,7 @@ from oslo_serialization import jsonutils +from mistralclient.api.v2.executions import Execution from mistralclient.api.v2 import tasks from mistralclient.tests.unit.v2 import base @@ -30,9 +31,20 @@ TASK = { 'result': {'some': 'result'} } +SUB_WF_EXEC = { + 'id': "456", + 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', + 'workflow_name': 'my_sub_wf', + 'workflow_namespace': '', + 'task_execution_id': "1", + 'description': '', + 'state': 'RUNNING', + 'input': {} +} URL_TEMPLATE = '/tasks' URL_TEMPLATE_ID = '/tasks/%s' +URL_TEMPLATE_SUB_EXECUTIONS = '/tasks/%s/executions%s' class TestTasksV2(base.BaseClientV2Test): @@ -136,3 +148,17 @@ class TestTasksV2(base.BaseClientV2Test): 'env': jsonutils.dumps({'k1': 'foobar'}) } self.assertDictEqual(body, self.requests_mock.last_request.json()) + + def test_get_sub_executions(self): + url = self.TEST_URL + URL_TEMPLATE_SUB_EXECUTIONS \ + % (TASK['id'], '?max_depth=-1&errors_only=') + + self.requests_mock.get(url, json={'executions': [SUB_WF_EXEC]}) + + sub_execution_list = self.tasks.get_task_sub_executions(TASK['id']) + + self.assertEqual(1, len(sub_execution_list)) + self.assertDictEqual( + Execution(self.executions, SUB_WF_EXEC).to_dict(), + sub_execution_list[0].to_dict() + ) diff --git a/releasenotes/notes/add_sub_executions_new_commands.yaml b/releasenotes/notes/add_sub_executions_new_commands.yaml new file mode 100644 index 00000000..dd461858 --- /dev/null +++ b/releasenotes/notes/add_sub_executions_new_commands.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added 2 new CLI commands, "execution-get-sub-executions" returns + sub-executions of a given execution id and "task-get-sub-executions" + returns the sub-executions of a given task execution id. + Both commands have the options "--max-depth" which is the max depth of a + sub-execution, and "--errors-only" that allows to find only ERROR paths of + the execution tree.