diff --git a/mistralclient/api/v2/client.py b/mistralclient/api/v2/client.py index 166cbf88..c7dd0f06 100644 --- a/mistralclient/api/v2/client.py +++ b/mistralclient/api/v2/client.py @@ -23,6 +23,7 @@ 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 +from mistralclient.api.v2 import event_triggers from mistralclient.api.v2 import executions from mistralclient.api.v2 import members from mistralclient.api.v2 import services @@ -38,6 +39,7 @@ _DEFAULT_MISTRAL_URL = "http://localhost:8989/v2" class Client(object): + def __init__(self, auth_type='keystone', **kwargs): # We get the session at this point, as some instances of session # objects might have mutexes that can't be deep-copied. @@ -72,6 +74,7 @@ class Client(object): self.actions = actions.ActionManager(http_client) self.workflows = workflows.WorkflowManager(http_client) self.cron_triggers = cron_triggers.CronTriggerManager(http_client) + self.event_triggers = event_triggers.EventTriggerManager(http_client) self.environments = environments.EnvironmentManager(http_client) self.action_executions = action_executions.ActionExecutionManager( http_client) diff --git a/mistralclient/api/v2/event_triggers.py b/mistralclient/api/v2/event_triggers.py new file mode 100644 index 00000000..0eef9f37 --- /dev/null +++ b/mistralclient/api/v2/event_triggers.py @@ -0,0 +1,61 @@ +# Copyright 2017, OpenStack Foundation +# +# 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 import base + + +class EventTrigger(base.Resource): + resource_name = 'EventTrigger' + + +class EventTriggerManager(base.ResourceManager): + resource_class = EventTrigger + + def create(self, name, workflow_id, exchange, topic, event, + workflow_input=None, workflow_params=None): + self._ensure_not_empty( + name=name, + workflow_id=workflow_id + ) + + data = { + 'workflow_id': workflow_id, + 'name': name, + 'exchange': exchange, + 'topic': topic, + 'event': event + } + + if workflow_input: + data.update({'workflow_input': json.dumps(workflow_input)}) + + if workflow_params: + data.update({'workflow_params': json.dumps(workflow_params)}) + + return self._create('/event_triggers', data) + + def list(self): + return self._list('/event_triggers', response_key='event_triggers') + + def get(self, id): + self._ensure_not_empty(id=id) + + return self._get('/event_triggers/%s' % id) + + def delete(self, id): + self._ensure_not_empty(id=id) + + self._delete('/event_triggers/%s' % id) diff --git a/mistralclient/commands/v2/event_triggers.py b/mistralclient/commands/v2/event_triggers.py new file mode 100644 index 00000000..22101ed0 --- /dev/null +++ b/mistralclient/commands/v2/event_triggers.py @@ -0,0 +1,167 @@ +# Copyright 2017, OpenStack Foundation +# +# 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 osc_lib.command import command + +from mistralclient.commands.v2 import base +from mistralclient import utils + + +def format_list(trigger=None): + return format(trigger, lister=True) + + +def format(trigger=None, lister=False): + columns = ( + 'ID', + 'Name', + 'Workflow ID', + 'Params', + 'Exchange', + 'Topic', + 'Event', + 'Created at', + 'Updated at' + ) + + if trigger: + data = ( + trigger.id, + trigger.name, + trigger.workflow_id, + trigger.workflow_params, + trigger.exchange, + trigger.topic, + trigger.event, + trigger.created_at, + ) + + if hasattr(trigger, 'updated_at'): + data += (trigger.updated_at,) + else: + data += (None,) + else: + data = (tuple('' for _ in range(len(columns))),) + + return columns, data + + +class List(base.MistralLister): + """List all event triggers.""" + + def _get_format_function(self): + return format_list + + def _get_resources(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + return mistral_client.event_triggers.list() + + +class Get(command.ShowOne): + """Show specific event trigger.""" + + def get_parser(self, prog_name): + parser = super(Get, self).get_parser(prog_name) + + parser.add_argument('event_trigger', help='Event trigger ID') + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + return format(mistral_client.event_triggers.get( + parsed_args.event_trigger + )) + + +class Create(command.ShowOne): + """Create new trigger.""" + + def get_parser(self, prog_name): + parser = super(Create, self).get_parser(prog_name) + + parser.add_argument('name', help='Event trigger name') + parser.add_argument('workflow_id', help='Workflow ID') + + parser.add_argument('exchange', + type=str, + help='Event trigger exchange') + + parser.add_argument('topic', + type=str, + help='Event trigger topic') + + parser.add_argument('event', + type=str, + help='Event trigger event name') + + parser.add_argument('workflow_input', + nargs='?', + help='Workflow input') + + parser.add_argument('--params', + help='Workflow params') + + return parser + + @staticmethod + def _get_json_string_or_dict(string): + if string: + return utils.load_json(string) + else: + return {} + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + wf_input = self._get_json_string_or_dict(parsed_args.workflow_input) + wf_params = self._get_json_string_or_dict(parsed_args.params) + + trigger = mistral_client.event_triggers.create( + parsed_args.name, + parsed_args.workflow_id, + parsed_args.exchange, + parsed_args.topic, + parsed_args.event, + wf_input, + wf_params, + ) + + return format(trigger) + + +class Delete(command.Command): + """Delete trigger.""" + + def get_parser(self, prog_name): + parser = super(Delete, self).get_parser(prog_name) + + parser.add_argument( + 'event_trigger_id', + nargs='+', help='ID of event trigger(s).' + ) + + return parser + + def take_action(self, parsed_args): + mistral_client = self.app.client_manager.workflow_engine + + utils.do_action_on_many( + lambda s: mistral_client.event_triggers.delete(s), + parsed_args.event_trigger_id, + "Request to delete event trigger %s has been accepted.", + "Unable to delete the specified event trigger(s)." + ) diff --git a/mistralclient/shell.py b/mistralclient/shell.py index ac567740..02353a27 100644 --- a/mistralclient/shell.py +++ b/mistralclient/shell.py @@ -31,6 +31,7 @@ import mistralclient.commands.v2.action_executions import mistralclient.commands.v2.actions import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.environments +import mistralclient.commands.v2.event_triggers import mistralclient.commands.v2.executions import mistralclient.commands.v2.members import mistralclient.commands.v2.services @@ -53,6 +54,7 @@ def env(*args, **kwargs): class OpenStackHelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, width=None): super(OpenStackHelpFormatter, self).__init__( @@ -78,6 +80,7 @@ class HelpAction(argparse.Action): instance, passed in as the "default" value for the action. """ + def __call__(self, parser, namespace, values, option_string=None): outputs = [] max_len = 0 @@ -682,6 +685,13 @@ class MistralShell(app.App): mistralclient.commands.v2.cron_triggers.Create, 'cron-trigger-delete': mistralclient.commands.v2.cron_triggers.Delete, + 'event-trigger-list': + mistralclient.commands.v2.event_triggers.List, + 'event-trigger-get': mistralclient.commands.v2.event_triggers.Get, + 'event-trigger-create': + mistralclient.commands.v2.event_triggers.Create, + 'event-trigger-delete': + mistralclient.commands.v2.event_triggers.Delete, 'service-list': mistralclient.commands.v2.services.List, 'member-create': mistralclient.commands.v2.members.Create, 'member-delete': mistralclient.commands.v2.members.Delete, diff --git a/mistralclient/tests/functional/cli/v2/base_v2.py b/mistralclient/tests/functional/cli/v2/base_v2.py index 357a149f..808082bc 100644 --- a/mistralclient/tests/functional/cli/v2/base_v2.py +++ b/mistralclient/tests/functional/cli/v2/base_v2.py @@ -202,6 +202,21 @@ class MistralClientTestBase(base.MistralCLIAuth, base.MistralCLIAltAuth): return trigger + def event_trigger_create(self, name, wf_id, exchange, + topic, event, wf_input, admin=True): + + trigger = self.mistral_cli( + admin, + 'event-trigger-create', + params=' '.join((name, wf_id, exchange, topic, event, wf_input))) + ev_tr_id = self.get_field_value(trigger, 'ID') + self.addCleanup(self.mistral_cli, + admin, + 'event-trigger-delete', + params=ev_tr_id) + + return trigger + def execution_create(self, params, admin=True): ex = self.mistral_cli(admin, 'execution-create', params=params) exec_id = self.get_field_value(ex, 'ID') diff --git a/mistralclient/tests/functional/cli/v2/cli_tests_v2.py b/mistralclient/tests/functional/cli/v2/cli_tests_v2.py index 235e1688..18a384a2 100644 --- a/mistralclient/tests/functional/cli/v2/cli_tests_v2.py +++ b/mistralclient/tests/functional/cli/v2/cli_tests_v2.py @@ -72,6 +72,15 @@ class SimpleMistralCLITests(base.MistralCLIAuth): 'Remaining executions', 'Created at', 'Updated at'] ) + def test_event_trigger_list(self): + triggers = self.parser.listing(self.mistral('event-trigger-list')) + + self.assertTableStruct( + triggers, + ['ID', 'Name', 'Workflow ID', 'Exchange', 'Topic', 'Event', + 'Created at', 'Updated at'] + ) + def test_actions_list(self): actions = self.parser.listing(self.mistral('action-list')) @@ -797,6 +806,92 @@ class CronTriggerCLITests(base_v2.MistralClientTestBase): self.assertIsNotNone(created_at) +class EventTriggerCLITests(base_v2.MistralClientTestBase): + """Test suite checks commands to work with event-triggers.""" + + @classmethod + def setUpClass(cls): + super(EventTriggerCLITests, cls).setUpClass() + + def setUp(self): + super(EventTriggerCLITests, self).setUp() + + wf = self.workflow_create(self.wf_def) + + self.wf_id = wf[0]['ID'] + + def test_event_trigger_create_delete(self): + trigger = self.mistral_admin( + 'event-trigger-create', + params=('trigger %s dummy_exchange dummy_topic event.dummy {}' % + self.wf_id)) + + self.assertTableStruct(trigger, ['Field', 'Value']) + + tr_id = self.get_field_value(trigger, 'ID') + tr_name = self.get_field_value(trigger, 'Name') + wf_id = self.get_field_value(trigger, 'Workflow ID') + created_at = self.get_field_value(trigger, 'Created at') + + self.assertEqual('trigger', tr_name) + self.assertEqual(self.wf_id, wf_id) + self.assertIsNotNone(created_at) + + triggers = self.mistral_admin('event-trigger-list') + + self.assertIn(tr_name, [tr['Name'] for tr in triggers]) + self.assertIn(wf_id, [tr['Workflow ID'] for tr in triggers]) + + self.mistral('event-trigger-delete', params=tr_id) + + triggers = self.mistral_admin('event-trigger-list') + + self.assertNotIn(tr_name, [tr['Name'] for tr in triggers]) + + def test_two_event_triggers_for_one_wf(self): + self.event_trigger_create('trigger1', + self.wf_id, + 'dummy_exchange', + 'dummy_topic', + 'event.dummy', + '{}') + + self.event_trigger_create('trigger2', + self.wf_id, + 'dummy_exchange', + 'dummy_topic', + 'dummy.event', + '{}') + + triggers = self.mistral_admin('event-trigger-list') + + self.assertIn('trigger1', [tr['Name'] for tr in triggers]) + self.assertIn('trigger2', [tr['Name'] for tr in triggers]) + + def test_event_trigger_get(self): + trigger = self.event_trigger_create('trigger', + self.wf_id, + 'dummy_exchange', + 'dummy_topic', + 'event.dummy.other', + '{}') + + self.assertTableStruct(trigger, ['Field', 'Value']) + + ev_tr_id = self.get_field_value(trigger, 'ID') + fetched_tr = self.mistral_admin('event-trigger-get', params=ev_tr_id) + + self.assertTableStruct(trigger, ['Field', 'Value']) + + tr_name = self.get_field_value(fetched_tr, 'Name') + wf_id = self.get_field_value(fetched_tr, 'Workflow ID') + created_at = self.get_field_value(fetched_tr, 'Created at') + + self.assertEqual('trigger', tr_name) + self.assertEqual(self.wf_id, wf_id) + self.assertIsNotNone(created_at) + + class TaskCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with tasks.""" @@ -1706,6 +1801,41 @@ class NegativeCLITests(base_v2.MistralClientTestBase): params='tr wb.wf1 {} --count 42 --first-time "4242-12-25 13:37"' ) + def test_event_tr_create_missing_argument(self): + wf = self.workflow_create(self.wf_def) + + self.assertRaises( + exceptions.CommandFailed, + self.mistral_admin, + 'event-trigger-create', + params='tr %s exchange topic' % wf[0]['ID'] + ) + + def test_event_tr_create_nonexistent_wf(self): + self.assertRaises( + exceptions.CommandFailed, + self.mistral_admin, + 'event-trigger-create', + params='456 4307362e-4a4a-4021-aa58-0fab23c9c751 ' + 'exchange topic event {} ' + ) + + def test_event_tr_delete_nonexistent_tr(self): + self.assertRaises( + exceptions.CommandFailed, + self.mistral_admin, + 'event-trigger-delete', + params='789' + ) + + def test_event_tr_get_nonexistent_tr(self): + self.assertRaises( + exceptions.CommandFailed, + self.mistral_admin, + 'event-trigger-get', + params='789' + ) + def test_action_get_nonexistent(self): self.assertRaises( exceptions.CommandFailed, diff --git a/mistralclient/tests/unit/v2/test_cli_event_triggers.py b/mistralclient/tests/unit/v2/test_cli_event_triggers.py new file mode 100644 index 00000000..f670c356 --- /dev/null +++ b/mistralclient/tests/unit/v2/test_cli_event_triggers.py @@ -0,0 +1,100 @@ +# 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 mock + +from mistralclient.api.v2 import event_triggers +from mistralclient.commands.v2 import event_triggers as event_triggers_cmd +from mistralclient.tests.unit import base + + +TRIGGER_DICT = { + 'id': '456', + 'name': 'my_trigger', + 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', + 'workflow_input': {}, + 'workflow_params': {}, + 'exchange': 'dummy_exchange', + 'topic': 'dummy_topic', + 'event': 'event.dummy', + 'created_at': '1', + 'updated_at': '1' +} + +TRIGGER = event_triggers.EventTrigger(mock, TRIGGER_DICT) + + +class TestCLITriggersV2(base.BaseCommandTest): + + @mock.patch('argparse.open', create=True) + def test_create(self, mock_open): + self.client.event_triggers.create.return_value = TRIGGER + mock_open.return_value = mock.MagicMock(spec=open) + + result = self.call( + event_triggers_cmd.Create, + app_args=['my_trigger', '123e4567-e89b-12d3-a456-426655440000', + 'dummy_exchange', 'dummy_topic', 'event.dummy', + '--params', '{}'] + ) + + self.assertEqual( + ( + '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', + {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' + ), + result[1] + ) + + def test_list(self): + self.client.event_triggers.list.return_value = [TRIGGER] + + result = self.call(event_triggers_cmd.List) + + self.assertEqual( + [( + '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', + {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' + )], + result[1] + ) + + def test_get(self): + self.client.event_triggers.get.return_value = TRIGGER + + result = self.call(event_triggers_cmd.Get, app_args=['id']) + + self.assertEqual( + ( + '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', + {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' + ), + result[1] + ) + + def test_delete(self): + self.call(event_triggers_cmd.Delete, app_args=['id']) + + self.client.event_triggers.delete.assert_called_once_with('id') + + def test_delete_with_multi_names(self): + self.call(event_triggers_cmd.Delete, app_args=['id1', 'id2']) + + self.assertEqual(2, self.client.event_triggers.delete.call_count) + self.assertEqual( + [mock.call('id1'), mock.call('id2')], + self.client.event_triggers.delete.call_args_list + ) diff --git a/setup.cfg b/setup.cfg index 98b2f3cf..06946c35 100644 --- a/setup.cfg +++ b/setup.cfg @@ -96,6 +96,11 @@ openstack.workflow_engine.v2 = cron_trigger_create = mistralclient.commands.v2.cron_triggers:Create cron_trigger_delete = mistralclient.commands.v2.cron_triggers:Delete + event_trigger_list = mistralclient.commands.v2.event_triggers:List + event_trigger_show = mistralclient.commands.v2.event_triggers:Get + event_trigger_create = mistralclient.commands.v2.event_triggers:Create + event_trigger_delete = mistralclient.commands.v2.event_triggers:Delete + workflow_engine_service_list = mistralclient.commands.v2.services:List resource_member_list = mistralclient.commands.v2.members:List