diff --git a/mistralclient/api/v2/executions.py b/mistralclient/api/v2/executions.py index b4421d0e..b62e1b25 100644 --- a/mistralclient/api/v2/executions.py +++ b/mistralclient/api/v2/executions.py @@ -31,12 +31,11 @@ class ExecutionManager(base.ResourceManager): def create(self, workflow_identifier='', namespace='', workflow_input=None, description='', source_execution_id=None, **params): - ident = workflow_identifier or source_execution_id - self._ensure_not_empty(workflow_identifier=ident) + self._ensure_not_empty( + workflow_identifier=workflow_identifier or source_execution_id + ) - data = { - 'description': description, - } + data = {'description': description} if uuidutils.is_uuid_like(source_execution_id): data.update({'source_execution_id': source_execution_id}) @@ -101,10 +100,31 @@ class ExecutionManager(base.ResourceManager): def delete(self, id, force=None): self._ensure_not_empty(id=id) - qparams = {} - if force: - qparams['force'] = True - query_string = self._build_query_params(filters=qparams) + query_params = {} + + if force: + query_params['force'] = True + + query_string = self._build_query_params(filters=query_params) self._delete('/executions/%s%s' % (id, query_string)) + + def get_report(self, id, errors_only=True, max_depth=None): + self._ensure_not_empty(id=id) + + query_params = {} + + if errors_only: + query_params['errors_only'] = True + + if max_depth is not None: + query_params['max_depth'] = max_depth + + query_string = self._build_query_params(filters=query_params) + + resp = self.http_client.get( + '/executions/%s/report%s' % (id, query_string) + ) + + return resp.json() diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py index 9e4b4e27..f6cfb7be 100644 --- a/mistralclient/commands/v2/executions.py +++ b/mistralclient/commands/v2/executions.py @@ -215,7 +215,9 @@ class Delete(command.Command): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + force = parsed_args.force + utils.do_action_on_many( lambda s: mistral_client.executions.delete(s, force=force), parsed_args.execution, @@ -290,6 +292,7 @@ class GetInput(command.Command): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + ex_input = mistral_client.executions.get(parsed_args.id).input try: @@ -313,6 +316,7 @@ class GetOutput(command.Command): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + output = mistral_client.executions.get(parsed_args.id).output try: @@ -322,3 +326,139 @@ class GetOutput(command.Command): LOG.debug("Execution output is not JSON.") self.app.stdout.write(output or "\n") + + +REPORT_ENTRY_INDENT = 4 + + +class GetReport(command.Command): + """Print execution report.""" + + def get_parser(self, prog_name): + parser = super(GetReport, self).get_parser(prog_name) + + parser.add_argument('id', help='Execution ID') + parser.add_argument( + '--errors-only', + dest='errors_only', + action='store_true', + help='Only error paths will be included.' + ) + parser.add_argument( + '--no-errors-only', + dest='errors_only', + action='store_false', + help='Not only error paths will be included.' + ) + parser.set_defaults(errors_only=True) + + 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 print_line(self, line, level=0): + self.app.stdout.write( + "%s%s\n" % (' ' * (level * REPORT_ENTRY_INDENT), line) + ) + + def print_workflow_execution_entry(self, wf_ex, level): + self.print_line( + "workflow '%s' [%s] %s" % + (wf_ex['name'], wf_ex['state'], wf_ex['id']), + level + ) + + if 'task_executions' in wf_ex: + for t_ex in wf_ex['task_executions']: + self.print_task_execution_entry(t_ex, level + 1) + + def print_task_execution_entry(self, t_ex, level): + self.print_line( + "task '%s' [%s] %s" % + (t_ex['name'], t_ex['state'], t_ex['id']), + level + ) + + if t_ex['state'] == 'ERROR': + state_info = t_ex['state_info'] + state_info = state_info[0:200] + '...' + + self.print_line('(error info: %s)' % state_info, level) + + if 'action_executions' in t_ex: + for a_ex in t_ex['action_executions']: + self.print_action_execution_entry(a_ex, level + 1) + + if 'workflow_executions' in t_ex: + for wf_ex in t_ex['workflow_executions']: + self.print_workflow_execution_entry(wf_ex, level + 1) + + def print_action_execution_entry(self, a_ex, level): + self.print_line( + "action '%s' [%s] %s" % + (a_ex['name'], a_ex['state'], a_ex['id']), + level + ) + + def print_statistics(self, stat): + self.print_line( + 'Number of tasks in SUCCESS state: %s' % + stat['success_tasks_count'] + ) + self.print_line( + 'Number of tasks in ERROR state: %s' % stat['error_tasks_count'] + ) + self.print_line( + 'Number of tasks in RUNNING state: %s' % + stat['running_tasks_count'] + ) + self.print_line( + 'Number of tasks in IDLE state: %s' % stat['idle_tasks_count'] + ) + self.print_line( + 'Number of tasks in PAUSED state: %s\n' % + stat['paused_tasks_count'] + ) + + def print_report(self, report_json): + self.print_line( + "\nTo get more details on a task failure " + "run: mistral task-get -c 'State info'\n" + ) + + frame_line = '=' * 30 + + self.print_line( + '%s General Statistics %s\n' % + (frame_line, frame_line) + ) + self.print_statistics(report_json['statistics']) + + self.print_line( + '%s Workflow Execution Tree %s\n' % + (frame_line, frame_line) + ) + self.print_workflow_execution_entry( + report_json['root_workflow_execution'], + 0 + ) + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + report_json = mistral_client.executions.get_report( + parsed_args.id, + errors_only=parsed_args.errors_only, + max_depth=parsed_args.max_depth + ) + + self.print_report(report_json) diff --git a/mistralclient/shell.py b/mistralclient/shell.py index b5ce7f65..623c0513 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -728,6 +728,8 @@ class MistralShell(app.App): mistralclient.commands.v2.executions.GetInput, 'execution-get-output': mistralclient.commands.v2.executions.GetOutput, + 'execution-get-report': + mistralclient.commands.v2.executions.GetReport, 'task-list': mistralclient.commands.v2.tasks.List, 'task-get': mistralclient.commands.v2.tasks.Get, 'task-get-published': mistralclient.commands.v2.tasks.GetPublished, diff --git a/mistralclient/tests/unit/v2/test_executions.py b/mistralclient/tests/unit/v2/test_executions.py index 54689c57..2c7c3417 100644 --- a/mistralclient/tests/unit/v2/test_executions.py +++ b/mistralclient/tests/unit/v2/test_executions.py @@ -265,3 +265,17 @@ class TestExecutionsV2(base.BaseClientV2Test): self.requests_mock.delete(url, status_code=204) self.executions.delete(EXEC['id']) + + def test_report(self): + url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] + '/report' + + expected_json = { + 'root_workflow_execution': {}, + 'statistics': {} + } + + self.requests_mock.get(url, json=expected_json) + + report = self.executions.get_report(EXEC['id']) + + self.assertDictEqual(expected_json, report)