From 9f78c6f2513df9ae91cdde7469d48a2f7810869e Mon Sep 17 00:00:00 2001 From: Nikolay Mahotkin Date: Thu, 7 Aug 2014 16:00:43 +0400 Subject: [PATCH] Add heat actions * Added heat actions and action generator * Modified base action generator * Unit tests Implements blueprint: mistral-openstack-actions Change-Id: I043e8f61e00a3fcf81d096790e3486fe0c052da9 --- mistral/actions/action_factory.py | 5 ++- mistral/actions/generator_factory.py | 13 ++++---- .../openstack/action_generator/base.py | 14 ++++---- .../openstack/action_generator/generators.py | 11 +++++-- mistral/actions/openstack/actions.py | 28 ++++++++++++++++ mistral/actions/openstack/mapping.json | 32 +++++++++++++++++++ .../tests/resources/openstack_tasks/heat.yaml | 4 +++ .../openstack/test_glance_generator.py | 4 +-- .../actions/openstack/test_heat_generator.py | 31 ++++++++++++++++++ .../openstack/test_keystone_generator.py | 6 ++-- .../actions/openstack/test_nova_generator.py | 4 +-- .../openstack/test_openstack_actions.py | 12 +++++++ .../unit/engine/test_openstack_actions.py | 25 +++++++++++++++ requirements.txt | 1 + 14 files changed, 165 insertions(+), 25 deletions(-) create mode 100644 mistral/tests/resources/openstack_tasks/heat.yaml create mode 100644 mistral/tests/unit/actions/openstack/test_heat_generator.py diff --git a/mistral/actions/action_factory.py b/mistral/actions/action_factory.py index 1bb0b79f0..fb6023139 100644 --- a/mistral/actions/action_factory.py +++ b/mistral/actions/action_factory.py @@ -48,9 +48,8 @@ def get_registered_namespaces(): def _register_dynamic_action_classes(): all_generators = generator_factory.all_generators() - for name in all_generators: - ns = _find_or_create_namespace(name) - generator = all_generators[name]() + for generator in all_generators: + ns = _find_or_create_namespace(generator.action_namespace) action_classes = generator.create_action_classes() for action_name, action in action_classes.items(): ns.add(action_name, action) diff --git a/mistral/actions/generator_factory.py b/mistral/actions/generator_factory.py index 4eceaef23..65d2b2652 100644 --- a/mistral/actions/generator_factory.py +++ b/mistral/actions/generator_factory.py @@ -1,6 +1,6 @@ # Copyright 2014 - Mirantis, 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 obtain a copy of the License at # @@ -16,8 +16,9 @@ from mistral.actions.openstack.action_generator import generators def all_generators(): - return { - "nova": generators.NovaActionGenerator, - "glance": generators.GlanceActionGenerator, - "keystone": generators.KeystoneActionGenerator - } \ No newline at end of file + return [ + generators.NovaActionGenerator, + generators.GlanceActionGenerator, + generators.KeystoneActionGenerator, + generators.HeatActionGenerator + ] \ No newline at end of file diff --git a/mistral/actions/openstack/action_generator/base.py b/mistral/actions/openstack/action_generator/base.py index 40e44874c..ae362d7b1 100644 --- a/mistral/actions/openstack/action_generator/base.py +++ b/mistral/actions/openstack/action_generator/base.py @@ -37,27 +37,29 @@ class OpenStackActionGenerator(action_generator.ActionGenerator): specific python-client and sets needed arguments to actions. """ - _action_namespace = None + action_namespace = None base_action_class = None - def create_action_class(self, method_name): + @classmethod + def create_action_class(cls, method_name): if not method_name: return None - action_class = type(str(method_name), (self.base_action_class,), {}) + action_class = type(str(method_name), (cls.base_action_class,), {}) setattr(action_class, 'client_method', method_name) return action_class - def create_action_classes(self): + @classmethod + def create_action_classes(cls): mapping = json.loads(open(pkg.resource_filename( version.version_info.package, MAPPING_PATH)).read()) - method_dict = mapping[self._action_namespace] + method_dict = mapping[cls.action_namespace] action_classes = {} for action_name, method_name in method_dict.items(): - action_classes[action_name] = self.create_action_class(method_name) + action_classes[action_name] = cls.create_action_class(method_name) return action_classes diff --git a/mistral/actions/openstack/action_generator/generators.py b/mistral/actions/openstack/action_generator/generators.py index 803f99a33..f43f53a80 100644 --- a/mistral/actions/openstack/action_generator/generators.py +++ b/mistral/actions/openstack/action_generator/generators.py @@ -17,15 +17,20 @@ from mistral.actions.openstack import actions class NovaActionGenerator(base.OpenStackActionGenerator): - _action_namespace = "nova" + action_namespace = "nova" base_action_class = actions.NovaAction class GlanceActionGenerator(base.OpenStackActionGenerator): - _action_namespace = "glance" + action_namespace = "glance" base_action_class = actions.GlanceAction class KeystoneActionGenerator(base.OpenStackActionGenerator): - _action_namespace = "keystone" + action_namespace = "keystone" base_action_class = actions.KeystoneAction + + +class HeatActionGenerator(base.OpenStackActionGenerator): + action_namespace = "heat" + base_action_class = actions.HeatAction diff --git a/mistral/actions/openstack/actions.py b/mistral/actions/openstack/actions.py index d833d8a39..6977eb4c6 100644 --- a/mistral/actions/openstack/actions.py +++ b/mistral/actions/openstack/actions.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect + from glanceclient.v2 import client as glanceclient +from heatclient.v1 import client as heatclient from keystoneclient.v3 import client as keystoneclient from novaclient.v1_1 import client as novaclient from oslo.config import cfg from mistral.actions.openstack import base from mistral import context +from mistral import exceptions as exc from mistral.utils.openstack import keystone as keystone_utils @@ -57,3 +61,27 @@ class KeystoneAction(base.OpenStackAction): return self._client_class(token=ctx.auth_token, auth_url=auth_url) + + +class HeatAction(base.OpenStackAction): + _client_class = heatclient.Client + + def _get_client(self): + ctx = context.ctx() + endpoint_url_tmpl = keystone_utils.get_endpoint_for_project('heat') + endpoint_url = endpoint_url_tmpl % {'tenant_id': ctx.project_id} + + return self._client_class(endpoint_url, + token=ctx.auth_token, + username=ctx.user_name) + + def run(self): + try: + method = self._get_client_method() + result = method(**self._kwargs_for_run) + if inspect.isgenerator(result): + return [v for v in result] + return result + except Exception as e: + raise exc.ActionException("%s failed: %s" + % (self.__class__.__name__, e)) \ No newline at end of file diff --git a/mistral/actions/openstack/mapping.json b/mistral/actions/openstack/mapping.json index 4155d6c82..e15a8a419 100644 --- a/mistral/actions/openstack/mapping.json +++ b/mistral/actions/openstack/mapping.json @@ -306,5 +306,37 @@ "users_resource_class": "users.resource_class", "users_update": "users.update", "users_update_password": "users.update_password" + }, + "heat": { + "_comment": "It uses heatclient.v2.", + "actions_resume": "actions.resume", + "actions_suspend": "actions.suspend", + "build_info_build_info": "build_info.build_info", + "events_get": "events.get", + "events_list": "events.list", + "resources_generate_template": "resources.generate_template", + "resources_get": "resources.get", + "resources_list": "resources.list", + "resources_metadata": "resources.metadata", + "resources_signal": "resources.signal", + "resource_types_get": "resource_types.get", + "resource_types_list": "resource_types.list", + "software_configs_create": "software_configs.create", + "software_configs_delete": "software_configs.delete", + "software_configs_get": "software_configs.get", + "software_deployments_create": "software_deployments.create", + "software_deployments_delete": "software_deployments.delete", + "software_deployments_get": "software_deployments.get", + "software_deployments_list": "software_deployments.list", + "software_deployments_metadata": "software_deployments.metadata", + "software_deployments_update": "software_deployments.update", + "stacks_abandon": "stacks.abandon", + "stacks_create": "stacks.create", + "stacks_delete": "stacks.delete", + "stacks_get": "stacks.get", + "stacks_list": "stacks.list", + "stacks_template": "stacks.template", + "stacks_update": "stacks.update", + "stacks_validate": "stacks.validate" } } \ No newline at end of file diff --git a/mistral/tests/resources/openstack_tasks/heat.yaml b/mistral/tests/resources/openstack_tasks/heat.yaml new file mode 100644 index 000000000..3ec39cb5f --- /dev/null +++ b/mistral/tests/resources/openstack_tasks/heat.yaml @@ -0,0 +1,4 @@ +Workflow: + tasks: + heat_stack_list: + action: heat.stacks_list \ No newline at end of file diff --git a/mistral/tests/unit/actions/openstack/test_glance_generator.py b/mistral/tests/unit/actions/openstack/test_glance_generator.py index 60ad0a2d0..deeaccdce 100644 --- a/mistral/tests/unit/actions/openstack/test_glance_generator.py +++ b/mistral/tests/unit/actions/openstack/test_glance_generator.py @@ -14,14 +14,14 @@ from oslotest import base -from mistral.actions import generator_factory +from mistral.actions.openstack.action_generator import generators from mistral.actions.openstack import actions class GlanceGeneratorTest(base.BaseTestCase): def test_generator(self): action_name = "glance.images_list" - generator = generator_factory.all_generators()["glance"]() + generator = generators.GlanceActionGenerator action_classes = generator.create_action_classes() short_action_name = action_name.split(".")[1] action_class = action_classes[short_action_name] diff --git a/mistral/tests/unit/actions/openstack/test_heat_generator.py b/mistral/tests/unit/actions/openstack/test_heat_generator.py new file mode 100644 index 000000000..3ef62b766 --- /dev/null +++ b/mistral/tests/unit/actions/openstack/test_heat_generator.py @@ -0,0 +1,31 @@ +# 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 oslotest import base + +from mistral.actions.openstack.action_generator import generators +from mistral.actions.openstack import actions + + +class HeatGeneratorTest(base.BaseTestCase): + def test_generator(self): + action_name = "heat.stacks_list" + generator = generators.HeatActionGenerator + action_classes = generator.create_action_classes() + short_action_name = action_name.split(".")[1] + action_class = action_classes[short_action_name] + + self.assertIsNotNone(generator) + self.assertIn(short_action_name, action_classes) + self.assertTrue(issubclass(action_class, actions.HeatAction)) diff --git a/mistral/tests/unit/actions/openstack/test_keystone_generator.py b/mistral/tests/unit/actions/openstack/test_keystone_generator.py index bb8adfa88..1390d2383 100644 --- a/mistral/tests/unit/actions/openstack/test_keystone_generator.py +++ b/mistral/tests/unit/actions/openstack/test_keystone_generator.py @@ -14,14 +14,14 @@ from oslotest import base -from mistral.actions import generator_factory +from mistral.actions.openstack.action_generator import generators from mistral.actions.openstack import actions class KeystoneGeneratorTest(base.BaseTestCase): def test_generator(self): action_name = "keystone.users_create" - generator = generator_factory.all_generators()["keystone"]() + generator = generators.KeystoneActionGenerator action_classes = generator.create_action_classes() short_action_name = action_name.split(".")[1] action_class = action_classes[short_action_name] @@ -29,4 +29,4 @@ class KeystoneGeneratorTest(base.BaseTestCase): self.assertIsNotNone(generator) self.assertIn(short_action_name, action_classes) self.assertTrue(issubclass(action_class, actions.KeystoneAction)) - self.assertEqual("users.create", action_class.client_method) \ No newline at end of file + self.assertEqual("users.create", action_class.client_method) diff --git a/mistral/tests/unit/actions/openstack/test_nova_generator.py b/mistral/tests/unit/actions/openstack/test_nova_generator.py index 3fbafbbff..aa1d97b57 100644 --- a/mistral/tests/unit/actions/openstack/test_nova_generator.py +++ b/mistral/tests/unit/actions/openstack/test_nova_generator.py @@ -14,14 +14,14 @@ from oslotest import base -from mistral.actions import generator_factory +from mistral.actions.openstack.action_generator import generators from mistral.actions.openstack import actions class NovaGeneratorTest(base.BaseTestCase): def test_generator(self): action_name = "nova.servers_get" - generator = generator_factory.all_generators()["nova"]() + generator = generators.NovaActionGenerator action_classes = generator.create_action_classes() short_action_name = action_name.split(".")[1] action_class = action_classes[short_action_name] diff --git a/mistral/tests/unit/actions/openstack/test_openstack_actions.py b/mistral/tests/unit/actions/openstack/test_openstack_actions.py index e04dc0bb2..55e9337e2 100644 --- a/mistral/tests/unit/actions/openstack/test_openstack_actions.py +++ b/mistral/tests/unit/actions/openstack/test_openstack_actions.py @@ -54,3 +54,15 @@ class OpenStackActionTest(base.BaseTestCase): self.assertTrue(mocked().users.get.called) mocked().users.get.assert_called_once_with(user="1234-abcd") + + @mock.patch.object(actions.HeatAction, '_get_client') + def test_heat_action(self, mocked): + method_name = "stacks.get" + action_class = actions.HeatAction + action_class.client_method = method_name + params = {'id': '1234-abcd'} + action = action_class(**params) + action.run() + + self.assertTrue(mocked().stacks.get.called) + mocked().stacks.get.assert_called_once_with(id="1234-abcd") diff --git a/mistral/tests/unit/engine/test_openstack_actions.py b/mistral/tests/unit/engine/test_openstack_actions.py index 7fb03e051..3255a2e65 100644 --- a/mistral/tests/unit/engine/test_openstack_actions.py +++ b/mistral/tests/unit/engine/test_openstack_actions.py @@ -122,3 +122,28 @@ class OpenStackActionsEngineTest(base.EngineTestCase): self.assertEqual(states.SUCCESS, task['state']) self.assertEqual("servers", task['output']['task'][task_name]) + + @mock.patch.object(actions.HeatAction, 'run', + mock.Mock(return_value="stacks")) + def test_heat_action(self): + context = {} + wb = create_workbook('openstack_tasks/heat.yaml') + task_name = 'heat_stack_list' + execution = self.engine.start_workflow_execution(wb['name'], + task_name, + context) + + # We have to reread execution to get its latest version. + execution = db_api.execution_get(execution['id']) + + self.assertEqual(states.SUCCESS, execution['state']) + + tasks = db_api.tasks_get(workbook_name=wb['name'], + execution_id=execution['id']) + + self.assertEqual(1, len(tasks)) + + task = self._assert_single_item(tasks, name=task_name) + + self.assertEqual(states.SUCCESS, task['state']) + self.assertEqual("stacks", task['output']['task'][task_name]) diff --git a/requirements.txt b/requirements.txt index e660a5aca..ec1043ebb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ oslo.config>=1.2.1 oslo.db>=0.2.0 # Apache-2.0 oslo.messaging>=1.3.0 paramiko>=1.13.0 +python-heatclient>=0.2.9 python-keystoneclient>=0.9.0 python-novaclient>=2.17 python-glanceclient>=0.13