Add env option to CLI for executions and tasks update

Add optional env argument on execution-update and task-update. The
env argument accepts a string or file in JSON or YAML.

Partially implements: blueprint mistral-rerun-update-env

Change-Id: Icc036dc3b995d2539e0916d161f1b0f5f0099556
This commit is contained in:
Winson Chan 2015-12-22 01:25:38 +00:00
parent 2c816bf238
commit 91535df1bf
11 changed files with 311 additions and 59 deletions

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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): def create_direct_workflow(self, workflow_name, workflow_input, **params):
return self.create(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: if state:
data = {'state': state} data['state'] = state
if description: if description:
data = ({'description': description}) data['description'] = description
if env:
data['params'] = {'env': env}
return self._update('/executions/%s' % id, data) return self._update('/executions/%s' % id, data)

View File

@ -13,6 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from mistralclient.api import base from mistralclient.api import base
@ -36,7 +38,7 @@ class TaskManager(base.ResourceManager):
return self._get('/tasks/%s' % id) 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 url = '/tasks/%s' % task_ex_id
body = { body = {
@ -45,4 +47,7 @@ class TaskManager(base.ResourceManager):
'reset': reset 'reset': reset
} }
if env:
body['env'] = json.dumps(env)
return self._update(url, body) return self._update(url, body)

View File

@ -18,7 +18,6 @@ import logging
from cliff import command from cliff import command
from cliff import show from cliff import show
import yaml
from mistralclient.commands.v2 import base from mistralclient.commands.v2 import base
from mistralclient import utils from mistralclient import utils
@ -89,17 +88,6 @@ def format(environment=None):
return columns, data 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): class List(base.MistralLister):
"""List all environments.""" """List all environments."""
@ -146,7 +134,7 @@ class Create(show.ShowOne):
return parser return parser
def take_action(self, parsed_args): 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 mistral_client = self.app.client_manager.workflow_engine
environment = mistral_client.environments.create(**data) environment = mistral_client.environments.create(**data)
@ -190,7 +178,7 @@ class Update(show.ShowOne):
return parser return parser
def take_action(self, parsed_args): 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 mistral_client = self.app.client_manager.workflow_engine
environment = mistral_client.environments.update(**data) environment = mistral_client.environments.update(**data)

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -16,6 +17,7 @@
import json import json
import logging import logging
import os.path
from cliff import command from cliff import command
from cliff import show from cliff import show
@ -225,15 +227,22 @@ class Update(show.ShowOne):
help='Execution identifier' help='Execution identifier'
) )
group = parser.add_mutually_exclusive_group(required=True) parser.add_argument(
group.add_argument(
'-s', '-s',
'--state', '--state',
dest='state', dest='state',
choices=['RUNNING', 'PAUSED', 'SUCCESS', 'ERROR'], choices=['RUNNING', 'PAUSED', 'SUCCESS', 'ERROR'],
help='Execution state' help='Execution state'
) )
group.add_argument(
parser.add_argument(
'-e',
'--env',
dest='env',
help='Environment variables'
)
parser.add_argument(
'-d', '-d',
'--description', '--description',
dest='description', dest='description',
@ -245,10 +254,18 @@ class Update(show.ShowOne):
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine 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( execution = mistral_client.executions.update(
parsed_args.id, parsed_args.id,
parsed_args.state, parsed_args.state,
parsed_args.description) description=parsed_args.description,
env=env
)
return format(execution) return format(execution)

View File

@ -17,11 +17,13 @@
import json import json
import logging import logging
import os.path
from cliff import command from cliff import command
from cliff import show from cliff import show
from mistralclient.commands.v2 import base from mistralclient.commands.v2 import base
from mistralclient import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -163,13 +165,28 @@ class Rerun(show.ShowOne):
'executions for with-items task') 'executions for with-items task')
) )
parser.add_argument(
'-e',
'--env',
dest='env',
help='Environment variables'
)
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
mistral_client = self.app.client_manager.workflow_engine 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( execution = mistral_client.tasks.rerun(
parsed_args.id, parsed_args.id,
reset=(not parsed_args.resume) reset=(not parsed_args.resume),
env=env
) )
return format(execution) return format(execution)

View File

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

View File

@ -1,4 +1,5 @@
# Copyright 2014 Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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): def test_create_wf_input_string(self):
self.client.executions.create.return_value = EXECUTION self.client.executions.create.return_value = EXECUTION
result = self.call(execution_cmd.Create, result = self.call(
app_args=['id', '{ "context": true }']) execution_cmd.Create,
app_args=['id', '{ "context": true }']
)
self.assertEqual(('123', 'some', '', 'RUNNING', None, self.assertEqual(
'1', '1'), result[1]) ('123', 'some', '', 'RUNNING', None, '1', '1'),
result[1]
)
def test_create_wf_input_file(self): def test_create_wf_input_file(self):
self.client.executions.create.return_value = EXECUTION 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, path = pkg.resource_filename(
'1', '1'), result[1]) '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): def test_create_with_description(self):
self.client.executions.create.return_value = EXECUTION self.client.executions.create.return_value = EXECUTION
result = self.call(execution_cmd.Create, result = self.call(
app_args=['id', '{ "context": true }', '-d', '']) execution_cmd.Create,
app_args=['id', '{ "context": true }', '-d', '']
)
self.assertEqual(('123', 'some', '', 'RUNNING', None, self.assertEqual(
'1', '1'), result[1]) ('123', 'some', '', 'RUNNING', None, '1', '1'),
result[1]
)
def test_update(self): def test_update_state(self):
self.client.executions.update.return_value = EXECUTION self.client.executions.update.return_value = EXECUTION
result = self.call(execution_cmd.Update, result = self.call(
app_args=['id', '-s', 'SUCCESS']) execution_cmd.Update,
app_args=['id', '-s', 'SUCCESS']
)
self.assertEqual(('123', 'some', '', 'RUNNING', None, self.assertEqual(
'1', '1'), result[1]) ('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): def test_list(self):
self.client.executions.list.return_value = (EXECUTION,) self.client.executions.list.return_value = (EXECUTION,)
result = self.call(execution_cmd.List) result = self.call(execution_cmd.List)
self.assertEqual([('123', 'some', '', 'RUNNING', None, self.assertEqual(
'1', '1')], result[1]) [('123', 'some', '', 'RUNNING', None, '1', '1')],
result[1]
)
def test_list_with_pagination(self): def test_list_with_pagination(self):
self.client.executions.list.return_value = (EXECUTION,) self.client.executions.list.return_value = (EXECUTION,)
@ -111,8 +160,10 @@ class TestCLIExecutionsV2(base.BaseCommandTest):
result = self.call(execution_cmd.Get, app_args=['id']) result = self.call(execution_cmd.Get, app_args=['id'])
self.assertEqual(('123', 'some', '', 'RUNNING', None, self.assertEqual(
'1', '1'), result[1]) ('123', 'some', '', 'RUNNING', None, '1', '1'),
result[1]
)
def test_delete(self): def test_delete(self):
self.call(execution_cmd.Delete, app_args=['id']) self.call(execution_cmd.Delete, app_args=['id'])

View File

@ -1,4 +1,5 @@
# Copyright 2014 Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# All Rights Reserved # All Rights Reserved
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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']) result = self.call(task_cmd.Rerun, app_args=['id', '--resume'])
self.assertEqual(EXPECTED_TASK_RESULT, result[1]) 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])

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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']), 'input': json.dumps(EXEC['input']),
} }
ex = self.executions.create(EXEC['workflow_name'], ex = self.executions.create(
EXEC['input']) EXEC['workflow_name'],
EXEC['input']
)
self.assertIsNotNone(ex) 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)) mock.assert_called_once_with(URL_TEMPLATE, json.dumps(body))
@unittest2.expectedFailure @unittest2.expectedFailure
@ -64,8 +71,10 @@ class TestExecutionsV2(base.BaseClientV2Test):
@unittest2.expectedFailure @unittest2.expectedFailure
def test_create_failure2(self): def test_create_failure2(self):
self.mock_http_post(content=EXEC) self.mock_http_post(content=EXEC)
self.executions.create(EXEC['workflow_name'], self.executions.create(
list('343', 'sdfsd')) EXEC['workflow_name'],
list('343', 'sdfsd')
)
def test_update(self): def test_update(self):
mock = self.mock_http_put(content=EXEC) mock = self.mock_http_put(content=EXEC)
@ -76,10 +85,43 @@ class TestExecutionsV2(base.BaseClientV2Test):
ex = self.executions.update(EXEC['id'], EXEC['state']) ex = self.executions.update(EXEC['id'], EXEC['state'])
self.assertIsNotNone(ex) 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( 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): def test_list(self):
mock = self.mock_http_get(content={'executions': [EXEC]}) mock = self.mock_http_get(content={'executions': [EXEC]})
@ -116,8 +158,11 @@ class TestExecutionsV2(base.BaseClientV2Test):
ex = self.executions.get(EXEC['id']) ex = self.executions.get(EXEC['id'])
self.assertEqual(executions.Execution(self.executions, EXEC).to_dict(), self.assertEqual(
ex.to_dict()) executions.Execution(self.executions, EXEC).to_dict(),
ex.to_dict()
)
mock.assert_called_once_with(URL_TEMPLATE_ID % EXEC['id']) mock.assert_called_once_with(URL_TEMPLATE_ID % EXEC['id'])
def test_delete(self): def test_delete(self):

View File

@ -1,4 +1,5 @@
# Copyright 2014 - Mirantis, Inc. # Copyright 2014 - Mirantis, Inc.
# Copyright 2015 - StackStorm, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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]) 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])
)

View File

@ -1,4 +1,5 @@
# Copyright 2015 - Huawei Technologies Co. Ltd # Copyright 2015 - Huawei Technologies Co. Ltd
# Copyright 2015 - StackStorm, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with 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 # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
import yaml
from mistralclient import exceptions from mistralclient import exceptions
@ -29,3 +34,20 @@ def do_action_on_many(action, resources, success_msg, error_msg):
if failure_flag: if failure_flag:
raise exceptions.MistralClientException(error_msg) 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())