Add action execution client lib and CLI
Partially implements blueprint mistral-refactor-task-output Change-Id: I18c8e7e531ddf8164891bcf69e7497ef41cb21f8
This commit is contained in:
54
mistralclient/api/v2/action_executions.py
Normal file
54
mistralclient/api/v2/action_executions.py
Normal file
@@ -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)
|
@@ -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,
|
||||
|
186
mistralclient/commands/v2/action_executions.py
Normal file
186
mistralclient/commands/v2/action_executions.py
Normal file
@@ -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('<none>' 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")
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
80
mistralclient/tests/unit/v2/test_action_executions.py
Normal file
80
mistralclient/tests/unit/v2/test_action_executions.py
Normal file
@@ -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'])
|
113
mistralclient/tests/unit/v2/test_cli_action_execs.py
Normal file
113
mistralclient/tests/unit/v2/test_cli_action_execs.py
Normal file
@@ -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"
|
||||
)
|
@@ -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
|
||||
|
Reference in New Issue
Block a user