Add new CLI commands for sub-executions new API endpoints

*2 new CLI commands were added:

   - execution-get-sub-executions
       returns the sub-executions of a given execution id

   - task-get-sub-executions
       returns the sub-executions of a given task-execution id

 both commands have the options
  --errors_only: returns only the error routes
               - default is False
  --max_depth: the max depth for the returned executions
             - if a negative value is given, then the API will return
               all sub-executions
             - default is -1

Change-Id: Ifcd25cfdbfb99613ff1bdccf8b94b3929f02a71d
Implements: blueprint mistral-execution-origin
Signed-off-by: ali <ali.abdelal@nokia.com>
This commit is contained in:
ali 2020-01-26 10:58:30 +00:00
parent e1e75d61eb
commit 23270d272a
11 changed files with 322 additions and 4 deletions

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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]

View File

@ -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
)

View File

@ -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()
)

View File

@ -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()
)

View File

@ -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.