diff --git a/mistralclient/api/v2/executions.py b/mistralclient/api/v2/executions.py index d158964b..5ac87b35 100644 --- a/mistralclient/api/v2/executions.py +++ b/mistralclient/api/v2/executions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -58,12 +59,17 @@ class ExecutionManager(base.ResourceManager): def create_direct_workflow(self, workflow_name, workflow_input, **params): return self.create(workflow_name, workflow_input, **params) - def update(self, id, state, description=None): + def update(self, id, state, description=None, env=None): + data = {} + if state: - data = {'state': state} + data['state'] = state if description: - data = ({'description': description}) + data['description'] = description + + if env: + data['params'] = {'env': env} return self._update('/executions/%s' % id, data) diff --git a/mistralclient/api/v2/tasks.py b/mistralclient/api/v2/tasks.py index 25b1935c..3db91804 100644 --- a/mistralclient/api/v2/tasks.py +++ b/mistralclient/api/v2/tasks.py @@ -13,6 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + from mistralclient.api import base @@ -36,7 +38,7 @@ class TaskManager(base.ResourceManager): return self._get('/tasks/%s' % id) - def rerun(self, task_ex_id, reset=True): + def rerun(self, task_ex_id, reset=True, env=None): url = '/tasks/%s' % task_ex_id body = { @@ -45,4 +47,7 @@ class TaskManager(base.ResourceManager): 'reset': reset } + if env: + body['env'] = json.dumps(env) + return self._update(url, body) diff --git a/mistralclient/commands/v2/environments.py b/mistralclient/commands/v2/environments.py index 527adc3c..3a145c0d 100644 --- a/mistralclient/commands/v2/environments.py +++ b/mistralclient/commands/v2/environments.py @@ -18,7 +18,6 @@ import logging from cliff import command from cliff import show -import yaml from mistralclient.commands.v2 import base from mistralclient import utils @@ -89,17 +88,6 @@ def format(environment=None): return columns, data -def load_file_content(f): - content = f.read() - - try: - data = yaml.safe_load(content) - except Exception: - data = json.loads(content) - - return data - - class List(base.MistralLister): """List all environments.""" @@ -146,7 +134,7 @@ class Create(show.ShowOne): return parser def take_action(self, parsed_args): - data = load_file_content(parsed_args.file) + data = utils.load_content(parsed_args.file.read()) mistral_client = self.app.client_manager.workflow_engine environment = mistral_client.environments.create(**data) @@ -190,7 +178,7 @@ class Update(show.ShowOne): return parser def take_action(self, parsed_args): - data = load_file_content(parsed_args.file) + data = utils.load_content(parsed_args.file.read()) mistral_client = self.app.client_manager.workflow_engine environment = mistral_client.environments.update(**data) diff --git a/mistralclient/commands/v2/executions.py b/mistralclient/commands/v2/executions.py index cf23a0c0..c44dbe0f 100644 --- a/mistralclient/commands/v2/executions.py +++ b/mistralclient/commands/v2/executions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -16,6 +17,7 @@ import json import logging +import os.path from cliff import command from cliff import show @@ -225,15 +227,22 @@ class Update(show.ShowOne): help='Execution identifier' ) - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument( + parser.add_argument( '-s', '--state', dest='state', choices=['RUNNING', 'PAUSED', 'SUCCESS', 'ERROR'], help='Execution state' ) - group.add_argument( + + parser.add_argument( + '-e', + '--env', + dest='env', + help='Environment variables' + ) + + parser.add_argument( '-d', '--description', dest='description', @@ -245,10 +254,18 @@ class Update(show.ShowOne): def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + env = ( + utils.load_file(parsed_args.env) + if parsed_args.env and os.path.isfile(parsed_args.env) + else utils.load_content(parsed_args.env) + ) + execution = mistral_client.executions.update( parsed_args.id, parsed_args.state, - parsed_args.description) + description=parsed_args.description, + env=env + ) return format(execution) diff --git a/mistralclient/commands/v2/tasks.py b/mistralclient/commands/v2/tasks.py index 5de21d09..207c4eba 100644 --- a/mistralclient/commands/v2/tasks.py +++ b/mistralclient/commands/v2/tasks.py @@ -17,11 +17,13 @@ import json import logging +import os.path from cliff import command from cliff import show from mistralclient.commands.v2 import base +from mistralclient import utils LOG = logging.getLogger(__name__) @@ -163,13 +165,28 @@ class Rerun(show.ShowOne): 'executions for with-items task') ) + parser.add_argument( + '-e', + '--env', + dest='env', + help='Environment variables' + ) + return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine + + env = ( + utils.load_file(parsed_args.env) + if parsed_args.env and os.path.isfile(parsed_args.env) + else utils.load_content(parsed_args.env) + ) + execution = mistral_client.tasks.rerun( parsed_args.id, - reset=(not parsed_args.resume) + reset=(not parsed_args.resume), + env=env ) return format(execution) diff --git a/mistralclient/tests/unit/test_utils.py b/mistralclient/tests/unit/test_utils.py new file mode 100644 index 00000000..8eb08e9e --- /dev/null +++ b/mistralclient/tests/unit/test_utils.py @@ -0,0 +1,57 @@ +# Copyright 2015 - StackStorm, 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 +import os.path +import tempfile +import testtools +import yaml + +from mistralclient import utils + + +ENV_DICT = {'k1': 'abc', 'k2': 123, 'k3': True} +ENV_STR = json.dumps(ENV_DICT) +ENV_YAML = yaml.safe_dump(ENV_DICT, default_flow_style=False) + + +class UtilityTest(testtools.TestCase): + + def test_load_empty(self): + self.assertDictEqual(dict(), utils.load_content(None)) + self.assertDictEqual(dict(), utils.load_content('')) + self.assertDictEqual(dict(), utils.load_content('{}')) + self.assertListEqual(list(), utils.load_content('[]')) + + def test_load_json_content(self): + self.assertDictEqual(ENV_DICT, utils.load_content(ENV_STR)) + + def test_load_json_file(self): + with tempfile.NamedTemporaryFile() as f: + f.write(ENV_STR.encode('utf-8')) + f.flush() + file_path = os.path.abspath(f.name) + + self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) + + def test_load_yaml_content(self): + self.assertDictEqual(ENV_DICT, utils.load_content(ENV_YAML)) + + def test_load_yaml_file(self): + with tempfile.NamedTemporaryFile() as f: + f.write(ENV_YAML.encode('utf-8')) + f.flush() + file_path = os.path.abspath(f.name) + + self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) diff --git a/mistralclient/tests/unit/v2/test_cli_executions.py b/mistralclient/tests/unit/v2/test_cli_executions.py index 2e81855e..391070a2 100644 --- a/mistralclient/tests/unit/v2/test_cli_executions.py +++ b/mistralclient/tests/unit/v2/test_cli_executions.py @@ -1,4 +1,5 @@ -# Copyright 2014 Mirantis, Inc. +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -36,47 +37,95 @@ class TestCLIExecutionsV2(base.BaseCommandTest): def test_create_wf_input_string(self): self.client.executions.create.return_value = EXECUTION - result = self.call(execution_cmd.Create, - app_args=['id', '{ "context": true }']) + result = self.call( + execution_cmd.Create, + app_args=['id', '{ "context": true }'] + ) - self.assertEqual(('123', 'some', '', 'RUNNING', None, - '1', '1'), result[1]) + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) def test_create_wf_input_file(self): self.client.executions.create.return_value = EXECUTION - path = pkg.resource_filename('mistralclient', - 'tests/unit/resources/ctx.json') - result = self.call(execution_cmd.Create, - app_args=['id', path]) - self.assertEqual(('123', 'some', '', 'RUNNING', None, - '1', '1'), result[1]) + path = pkg.resource_filename( + 'mistralclient', + 'tests/unit/resources/ctx.json' + ) + + result = self.call( + execution_cmd.Create, + app_args=['id', path] + ) + + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) def test_create_with_description(self): self.client.executions.create.return_value = EXECUTION - result = self.call(execution_cmd.Create, - app_args=['id', '{ "context": true }', '-d', '']) + result = self.call( + execution_cmd.Create, + app_args=['id', '{ "context": true }', '-d', ''] + ) - self.assertEqual(('123', 'some', '', 'RUNNING', None, - '1', '1'), result[1]) + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) - def test_update(self): + def test_update_state(self): self.client.executions.update.return_value = EXECUTION - result = self.call(execution_cmd.Update, - app_args=['id', '-s', 'SUCCESS']) + result = self.call( + execution_cmd.Update, + app_args=['id', '-s', 'SUCCESS'] + ) - self.assertEqual(('123', 'some', '', 'RUNNING', None, - '1', '1'), result[1]) + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) + + def test_resume_update_env(self): + self.client.executions.update.return_value = EXECUTION + + result = self.call( + execution_cmd.Update, + app_args=['id', '-s', 'RUNNING', '--env', '{"k1": "foobar"}'] + ) + + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) + + def test_update_description(self): + self.client.executions.update.return_value = EXECUTION + + result = self.call( + execution_cmd.Update, + app_args=['id', '-d', 'foobar'] + ) + + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) def test_list(self): self.client.executions.list.return_value = (EXECUTION,) result = self.call(execution_cmd.List) - self.assertEqual([('123', 'some', '', 'RUNNING', None, - '1', '1')], result[1]) + self.assertEqual( + [('123', 'some', '', 'RUNNING', None, '1', '1')], + result[1] + ) def test_list_with_pagination(self): self.client.executions.list.return_value = (EXECUTION,) @@ -111,8 +160,10 @@ class TestCLIExecutionsV2(base.BaseCommandTest): result = self.call(execution_cmd.Get, app_args=['id']) - self.assertEqual(('123', 'some', '', 'RUNNING', None, - '1', '1'), result[1]) + self.assertEqual( + ('123', 'some', '', 'RUNNING', None, '1', '1'), + result[1] + ) def test_delete(self): self.call(execution_cmd.Delete, app_args=['id']) diff --git a/mistralclient/tests/unit/v2/test_cli_tasks.py b/mistralclient/tests/unit/v2/test_cli_tasks.py index 6b76f5f7..8543c06c 100644 --- a/mistralclient/tests/unit/v2/test_cli_tasks.py +++ b/mistralclient/tests/unit/v2/test_cli_tasks.py @@ -1,4 +1,5 @@ -# Copyright 2014 Mirantis, Inc. +# Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -101,3 +102,23 @@ class TestCLITasksV2(base.BaseCommandTest): result = self.call(task_cmd.Rerun, app_args=['id', '--resume']) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) + + def test_rerun_update_env(self): + self.client.tasks.rerun.return_value = TASK + + result = self.call( + task_cmd.Rerun, + app_args=['id', '--env', '{"k1": "foobar"}'] + ) + + self.assertEqual(EXPECTED_TASK_RESULT, result[1]) + + def test_rerun_no_reset_update_env(self): + self.client.tasks.rerun.return_value = TASK + + result = self.call( + task_cmd.Rerun, + app_args=['id', '--resume', '--env', '{"k1": "foobar"}'] + ) + + self.assertEqual(EXPECTED_TASK_RESULT, result[1]) diff --git a/mistralclient/tests/unit/v2/test_executions.py b/mistralclient/tests/unit/v2/test_executions.py index 02089293..2641e53e 100644 --- a/mistralclient/tests/unit/v2/test_executions.py +++ b/mistralclient/tests/unit/v2/test_executions.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -48,12 +49,18 @@ class TestExecutionsV2(base.BaseClientV2Test): 'input': json.dumps(EXEC['input']), } - ex = self.executions.create(EXEC['workflow_name'], - EXEC['input']) + ex = self.executions.create( + EXEC['workflow_name'], + EXEC['input'] + ) self.assertIsNotNone(ex) - self.assertEqual(executions.Execution(self.executions, EXEC).to_dict(), - ex.to_dict()) + + self.assertEqual( + executions.Execution(self.executions, EXEC).to_dict(), + ex.to_dict() + ) + mock.assert_called_once_with(URL_TEMPLATE, json.dumps(body)) @unittest2.expectedFailure @@ -64,8 +71,10 @@ class TestExecutionsV2(base.BaseClientV2Test): @unittest2.expectedFailure def test_create_failure2(self): self.mock_http_post(content=EXEC) - self.executions.create(EXEC['workflow_name'], - list('343', 'sdfsd')) + self.executions.create( + EXEC['workflow_name'], + list('343', 'sdfsd') + ) def test_update(self): mock = self.mock_http_put(content=EXEC) @@ -76,10 +85,43 @@ class TestExecutionsV2(base.BaseClientV2Test): ex = self.executions.update(EXEC['id'], EXEC['state']) self.assertIsNotNone(ex) - self.assertEqual(executions.Execution(self.executions, EXEC).to_dict(), - ex.to_dict()) + + self.assertEqual( + executions.Execution(self.executions, EXEC).to_dict(), + ex.to_dict() + ) + mock.assert_called_once_with( - URL_TEMPLATE_ID % EXEC['id'], json.dumps(body)) + URL_TEMPLATE_ID % EXEC['id'], + json.dumps(body) + ) + + def test_update_env(self): + mock = self.mock_http_put(content=EXEC) + body = { + 'state': EXEC['state'], + 'params': { + 'env': {'k1': 'foobar'} + } + } + + ex = self.executions.update( + EXEC['id'], + EXEC['state'], + env={'k1': 'foobar'} + ) + + self.assertIsNotNone(ex) + + self.assertEqual( + executions.Execution(self.executions, EXEC).to_dict(), + ex.to_dict() + ) + + mock.assert_called_once_with( + URL_TEMPLATE_ID % EXEC['id'], + json.dumps(body) + ) def test_list(self): mock = self.mock_http_get(content={'executions': [EXEC]}) @@ -116,8 +158,11 @@ class TestExecutionsV2(base.BaseClientV2Test): ex = self.executions.get(EXEC['id']) - self.assertEqual(executions.Execution(self.executions, EXEC).to_dict(), - ex.to_dict()) + self.assertEqual( + executions.Execution(self.executions, EXEC).to_dict(), + ex.to_dict() + ) + mock.assert_called_once_with(URL_TEMPLATE_ID % EXEC['id']) def test_delete(self): diff --git a/mistralclient/tests/unit/v2/test_tasks.py b/mistralclient/tests/unit/v2/test_tasks.py index 546b0e8c..d606b4c9 100644 --- a/mistralclient/tests/unit/v2/test_tasks.py +++ b/mistralclient/tests/unit/v2/test_tasks.py @@ -1,4 +1,5 @@ # Copyright 2014 - Mirantis, Inc. +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -101,3 +102,25 @@ class TestTasksV2(base.BaseClientV2Test): }, json.loads(mock.call_args[0][1]) ) + + def test_rerun_update_env(self): + mock = self.mock_http_put(content=TASK) + + task = self.tasks.rerun(TASK['id'], env={'k1': 'foobar'}) + + self.assertDictEqual( + tasks.Task(self.tasks, TASK).to_dict(), + task.to_dict() + ) + + self.assertEqual(1, mock.call_count) + self.assertEqual(URL_TEMPLATE_ID % TASK['id'], mock.call_args[0][0]) + self.assertDictEqual( + { + 'reset': True, + 'state': 'RUNNING', + 'id': TASK['id'], + 'env': json.dumps({'k1': 'foobar'}) + }, + json.loads(mock.call_args[0][1]) + ) diff --git a/mistralclient/utils.py b/mistralclient/utils.py index 17b1485d..d143f9ba 100644 --- a/mistralclient/utils.py +++ b/mistralclient/utils.py @@ -1,4 +1,5 @@ # Copyright 2015 - Huawei Technologies Co. Ltd +# Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,6 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json + +import yaml + from mistralclient import exceptions @@ -29,3 +34,20 @@ def do_action_on_many(action, resources, success_msg, error_msg): if failure_flag: raise exceptions.MistralClientException(error_msg) + + +def load_content(content): + if content is None or content == '': + return dict() + + try: + data = yaml.safe_load(content) + except Exception: + data = json.loads(content) + + return data + + +def load_file(path): + with open(path, 'r') as f: + return load_content(f.read())