From f07d3ee6ea7bf3432700fc127c88ca33cba8a84d Mon Sep 17 00:00:00 2001 From: Nikolay Mahotkin Date: Wed, 11 Mar 2015 16:08:23 +0300 Subject: [PATCH] Add action execution client lib and CLI Partially implements blueprint mistral-refactor-task-output Change-Id: I18c8e7e531ddf8164891bcf69e7497ef41cb21f8 --- mistralclient/api/v2/action_executions.py | 54 +++++ mistralclient/api/v2/client.py | 2 + .../commands/v2/action_executions.py | 186 ++++++++++++++++++ mistralclient/shell.py | 11 ++ mistralclient/tests/unit/v2/base.py | 1 + .../tests/unit/v2/test_action_executions.py | 80 ++++++++ .../tests/unit/v2/test_cli_action_execs.py | 113 +++++++++++ mistralclient/tests/unit/v2/test_cli_tasks.py | 2 +- 8 files changed, 448 insertions(+), 1 deletion(-) create mode 100644 mistralclient/api/v2/action_executions.py create mode 100644 mistralclient/commands/v2/action_executions.py create mode 100644 mistralclient/tests/unit/v2/test_action_executions.py create mode 100644 mistralclient/tests/unit/v2/test_cli_action_execs.py diff --git a/mistralclient/api/v2/action_executions.py b/mistralclient/api/v2/action_executions.py new file mode 100644 index 00000000..b50d66d1 --- /dev/null +++ b/mistralclient/api/v2/action_executions.py @@ -0,0 +1,54 @@ +# Copyright 2014 - Mirantis, 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. + +from mistralclient.api import base + + +class ActionExecution(base.Resource): + resource_name = 'ActionExecution' + + +class ActionExecutionManager(base.ResourceManager): + resource_class = ActionExecution + + def update(self, id, state=None, output=None): + self._ensure_not_empty(id=id) + + if not (state or output): + raise base.APIException( + 400, + "Please provide either state or output for action execution." + ) + + data = {} + if state: + data['state'] = state + + if output: + data['output'] = output + + return self._update('/action_executions/%s' % id, data) + + def list(self, task_execution_id=None): + url = '/action_executions' + + if task_execution_id: + url = '/tasks/%s/action_executions' % task_execution_id + + return self._list(url, response_key='action_executions') + + def get(self, id): + self._ensure_not_empty(id=id) + + return self._get('/action_executions/%s' % id) diff --git a/mistralclient/api/v2/client.py b/mistralclient/api/v2/client.py index 2c2d1e28..69fb50dd 100644 --- a/mistralclient/api/v2/client.py +++ b/mistralclient/api/v2/client.py @@ -16,6 +16,7 @@ import six from mistralclient.api import httpclient +from mistralclient.api.v2 import action_executions from mistralclient.api.v2 import actions from mistralclient.api.v2 import cron_triggers from mistralclient.api.v2 import environments @@ -56,6 +57,7 @@ class Client(object): self.workflows = workflows.WorkflowManager(self) self.cron_triggers = cron_triggers.CronTriggerManager(self) self.environments = environments.EnvironmentManager(self) + self.action_executions = action_executions.ActionExecutionManager(self) def authenticate(self, mistral_url=None, username=None, api_key=None, project_name=None, auth_url=None, project_id=None, diff --git a/mistralclient/commands/v2/action_executions.py b/mistralclient/commands/v2/action_executions.py new file mode 100644 index 00000000..12492bc7 --- /dev/null +++ b/mistralclient/commands/v2/action_executions.py @@ -0,0 +1,186 @@ +# Copyright 2014 - Mirantis, Inc. +# All Rights Reserved +# +# 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. +# + +import json +import logging + +from cliff import command +from cliff import show + +from mistralclient.api.v2 import action_executions +from mistralclient.commands.v2 import base + +LOG = logging.getLogger(__name__) + + +def format(action_ex=None): + columns = ( + 'ID', + 'Name', + 'Workflow name', + 'State', + 'State info', + 'Is accepted', + ) + + if action_ex: + data = ( + action_ex.id, + action_ex.name, + action_ex.workflow_name, + action_ex.state, + action_ex.state_info, + action_ex.accepted, + ) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class List(base.MistralLister): + """List all Action executions.""" + + def _get_format_function(self): + return format + + def get_parser(self, prog_name): + parser = super(List, self).get_parser(prog_name) + + parser.add_argument( + 'task_execution_id', + nargs='?', + help='Task execution ID.') + return parser + + def _get_resources(self, parsed_args): + return action_executions.ActionExecutionManager( + self.app.client + ).list(parsed_args.task_execution_id) + + +class Get(show.ShowOne): + """Show specific Action execution.""" + + def get_parser(self, prog_name): + parser = super(Get, self).get_parser(prog_name) + + parser.add_argument( + 'id', + help='Action execution ID.') + return parser + + def take_action(self, parsed_args): + execution = action_executions.ActionExecutionManager( + self.app.client + ).get(parsed_args.id) + + return format(execution) + + +class Update(show.ShowOne): + """Update specific Action execution.""" + + def get_parser(self, prog_name): + parser = super(Update, self).get_parser(prog_name) + + parser.add_argument( + 'id', + help='Action execution ID.') + parser.add_argument( + '--state', + dest='state', + choices=['IDLE', 'RUNNING', 'SUCCESS', 'ERROR'], + help='Action execution state') + parser.add_argument( + '--output', + dest='output', + help='Action execution output') + + return parser + + def take_action(self, parsed_args): + output = None + if parsed_args.output: + try: + output = json.loads(parsed_args.output) + except: + output = json.load(open(parsed_args.output)) + + execution = action_executions.ActionExecutionManager( + self.app.client + ).update( + parsed_args.id, + parsed_args.state, + output + ) + + return format(execution) + + +class GetOutput(command.Command): + """Show Action execution output data.""" + + def get_parser(self, prog_name): + parser = super(GetOutput, self).get_parser(prog_name) + parser.add_argument( + 'id', + help='Action execution ID.') + + return parser + + def take_action(self, parsed_args): + output = action_executions.ActionExecutionManager( + self.app.client + ).get( + parsed_args.id + ).output + + try: + output = json.loads(output) + output = json.dumps(output, indent=4) + "\n" + except: + LOG.debug("Task result is not JSON.") + + self.app.stdout.write(output or "\n") + + +class GetInput(command.Command): + """Show Action execution input data.""" + + def get_parser(self, prog_name): + parser = super(GetInput, self).get_parser(prog_name) + parser.add_argument( + 'id', + help='Action execution ID.' + ) + + return parser + + def take_action(self, parsed_args): + result = action_executions.ActionExecutionManager( + self.app.client + ).get( + parsed_args.id + ).input + + try: + result = json.loads(result) + result = json.dumps(result, indent=4) + "\n" + except: + LOG.debug("Task result is not JSON.") + + self.app.stdout.write(result or "\n") diff --git a/mistralclient/shell.py b/mistralclient/shell.py index 797b9d5e..db1baf22 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -25,6 +25,7 @@ from mistralclient.api import client import mistralclient.commands.v1.executions import mistralclient.commands.v1.tasks import mistralclient.commands.v1.workbooks +import mistralclient.commands.v2.action_executions import mistralclient.commands.v2.actions import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.environments @@ -261,6 +262,16 @@ class MistralShell(app.App): mistralclient.commands.v2.environments.Update, 'environment-list': mistralclient.commands.v2.environments.List, 'environment-get': mistralclient.commands.v2.environments.Get, + 'action-execution-list': + mistralclient.commands.v2.action_executions.List, + 'action-execution-get': + mistralclient.commands.v2.action_executions.Get, + 'action-execution-get-input': + mistralclient.commands.v2.action_executions.GetInput, + 'action-execution-get-output': + mistralclient.commands.v2.action_executions.GetOutput, + 'action-execution-update': + mistralclient.commands.v2.action_executions.Update, 'execution-create': mistralclient.commands.v2.executions.Create, 'execution-delete': mistralclient.commands.v2.executions.Delete, 'execution-update': mistralclient.commands.v2.executions.Update, diff --git a/mistralclient/tests/unit/v2/base.py b/mistralclient/tests/unit/v2/base.py index ed31df42..1e36fe79 100644 --- a/mistralclient/tests/unit/v2/base.py +++ b/mistralclient/tests/unit/v2/base.py @@ -28,3 +28,4 @@ class BaseClientV2Test(base.BaseClientTest): self.tasks = self._client.tasks self.workflows = self._client.workflows self.environments = self._client.environments + self.action_executions = self._client.action_executions diff --git a/mistralclient/tests/unit/v2/test_action_executions.py b/mistralclient/tests/unit/v2/test_action_executions.py new file mode 100644 index 00000000..33081678 --- /dev/null +++ b/mistralclient/tests/unit/v2/test_action_executions.py @@ -0,0 +1,80 @@ +# Copyright 2014 - Mirantis, 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. + +import json + +from mistralclient.api.v2 import action_executions +from mistralclient.tests.unit.v2 import base + +# TODO(everyone): later we need additional tests verifying all the errors etc. + +ACTION_EXEC = { + 'id': "1", + 'name': 'my_action_execution', + 'workflow_name': 'my_wf', + 'state': 'RUNNING', +} + + +URL_TEMPLATE = '/action_executions' +URL_TEMPLATE_ID = '/action_executions/%s' + + +class TestActionExecutions(base.BaseClientV2Test): + def test_update(self): + mock = self.mock_http_put(content=ACTION_EXEC) + body = { + 'state': ACTION_EXEC['state'] + } + + action_execution = self.action_executions.update( + ACTION_EXEC['id'], + ACTION_EXEC['state'] + ) + + self.assertIsNotNone(action_execution) + self.assertEqual(action_executions.ActionExecution( + self.action_executions, ACTION_EXEC + ).__dict__, action_execution.__dict__) + + mock.assert_called_once_with( + URL_TEMPLATE_ID % ACTION_EXEC['id'], json.dumps(body)) + + def test_list(self): + mock = self.mock_http_get( + content={'action_executions': [ACTION_EXEC]} + ) + + action_execution_list = self.action_executions.list() + + self.assertEqual(1, len(action_execution_list)) + action_execution = action_execution_list[0] + + self.assertEqual(action_executions.ActionExecution( + self.action_executions, ACTION_EXEC + ).__dict__, action_execution.__dict__) + + mock.assert_called_once_with(URL_TEMPLATE) + + def test_get(self): + mock = self.mock_http_get(content=ACTION_EXEC) + + action_execution = self.action_executions.get(ACTION_EXEC['id']) + + self.assertEqual(action_executions.ActionExecution( + self.action_executions, ACTION_EXEC + ).__dict__, action_execution.__dict__) + + mock.assert_called_once_with( + URL_TEMPLATE_ID % ACTION_EXEC['id']) diff --git a/mistralclient/tests/unit/v2/test_cli_action_execs.py b/mistralclient/tests/unit/v2/test_cli_action_execs.py new file mode 100644 index 00000000..317ac0f4 --- /dev/null +++ b/mistralclient/tests/unit/v2/test_cli_action_execs.py @@ -0,0 +1,113 @@ +# Copyright 2014 Mirantis, Inc. +# All Rights Reserved +# +# 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. +# + +import json + +import mock + +from mistralclient.api.v2 import action_executions as action_ex +from mistralclient.commands.v2 import action_executions as action_ex_cmd +from mistralclient.tests.unit import base + +ACTION_EX_DICT = { + 'id': '123', + 'name': 'some', + 'workflow_name': 'thing', + 'state': 'RUNNING', + 'state_info': 'RUNNING somehow.', + 'accepted': True +} + +ACTION_EX_RESULT = {"test": "is", "passed": "successfully"} +ACTION_EX_INPUT = {"param1": "val1", "param2": 2} + +ACTION_EX_WITH_OUTPUT_DICT = ACTION_EX_DICT.copy() +ACTION_EX_WITH_OUTPUT_DICT.update({'output': json.dumps(ACTION_EX_RESULT)}) +ACTION_EX_WITH_INPUT_DICT = ACTION_EX_DICT.copy() +ACTION_EX_WITH_INPUT_DICT.update({'input': json.dumps(ACTION_EX_INPUT)}) + +ACTION_EX = action_ex.ActionExecution(mock, ACTION_EX_DICT) +ACTION_EX_WITH_OUTPUT = action_ex.ActionExecution( + mock, ACTION_EX_WITH_OUTPUT_DICT +) +ACTION_EX_WITH_INPUT = action_ex.ActionExecution( + mock, ACTION_EX_WITH_INPUT_DICT +) + + +class TestCLIActionExecutions(base.BaseCommandTest): + @mock.patch( + 'mistralclient.api.v2.action_executions.ActionExecutionManager.update' + ) + def test_update(self, mock): + mock.return_value = ACTION_EX + + result = self.call(action_ex_cmd.Update, + app_args=['id', '--state', 'ERROR']) + + self.assertEqual( + ('123', 'some', 'thing', 'RUNNING', + 'RUNNING somehow.', True), result[1] + ) + + @mock.patch( + 'mistralclient.api.v2.action_executions.ActionExecutionManager.list' + ) + def test_list(self, mock): + mock.return_value = (ACTION_EX,) + + result = self.call(action_ex_cmd.List) + + self.assertEqual( + [('123', 'some', 'thing', 'RUNNING', + 'RUNNING somehow.', True)], result[1] + ) + + @mock.patch( + 'mistralclient.api.v2.action_executions.ActionExecutionManager.get' + ) + def test_get(self, mock): + mock.return_value = ACTION_EX + + result = self.call(action_ex_cmd.Get, app_args=['id']) + + self.assertEqual( + ('123', 'some', 'thing', 'RUNNING', + 'RUNNING somehow.', True), result[1] + ) + + @mock.patch( + 'mistralclient.api.v2.action_executions.ActionExecutionManager.get' + ) + def test_get_output(self, mock): + mock.return_value = ACTION_EX_WITH_OUTPUT + + self.call(action_ex_cmd.GetOutput, app_args=['id']) + + self.app.stdout.write.assert_called_with( + json.dumps(ACTION_EX_RESULT, indent=4) + "\n") + + @mock.patch( + 'mistralclient.api.v2.action_executions.ActionExecutionManager.get' + ) + def test_get_input(self, mock): + mock.return_value = ACTION_EX_WITH_INPUT + + self.call(action_ex_cmd.GetInput, app_args=['id']) + + self.app.stdout.write.assert_called_with( + json.dumps(ACTION_EX_INPUT, indent=4) + "\n" + ) diff --git a/mistralclient/tests/unit/v2/test_cli_tasks.py b/mistralclient/tests/unit/v2/test_cli_tasks.py index 387e7cb5..e32b36d7 100644 --- a/mistralclient/tests/unit/v2/test_cli_tasks.py +++ b/mistralclient/tests/unit/v2/test_cli_tasks.py @@ -43,7 +43,7 @@ TASK_WITH_RESULT = tasks.Task(mock, TASK_WITH_RESULT_DICT) TASK_WITH_INPUT = tasks.Task(mock, TASK_WITH_INPUT_DICT) -class TestCLIT1asksV2(base.BaseCommandTest): +class TestCLITasksV2(base.BaseCommandTest): @mock.patch('mistralclient.api.v2.tasks.TaskManager.update') def test_update(self, mock): mock.return_value = TASK