Add heat actions

* Added heat actions and action generator
 * Modified base action generator
 * Unit tests

Implements blueprint: mistral-openstack-actions

Change-Id: I043e8f61e00a3fcf81d096790e3486fe0c052da9
This commit is contained in:
Nikolay Mahotkin 2014-08-07 16:00:43 +04:00
parent 86a93cc3ad
commit 9f78c6f251
14 changed files with 165 additions and 25 deletions

View File

@ -48,9 +48,8 @@ def get_registered_namespaces():
def _register_dynamic_action_classes(): def _register_dynamic_action_classes():
all_generators = generator_factory.all_generators() all_generators = generator_factory.all_generators()
for name in all_generators: for generator in all_generators:
ns = _find_or_create_namespace(name) ns = _find_or_create_namespace(generator.action_namespace)
generator = all_generators[name]()
action_classes = generator.create_action_classes() action_classes = generator.create_action_classes()
for action_name, action in action_classes.items(): for action_name, action in action_classes.items():
ns.add(action_name, action) ns.add(action_name, action)

View File

@ -1,6 +1,6 @@
# Copyright 2014 - Mirantis, Inc. # 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 not use this file except in compliance with the License.
# You may obtain a copy of the License at # You may obtain a copy of the License at
# #
@ -16,8 +16,9 @@ from mistral.actions.openstack.action_generator import generators
def all_generators(): def all_generators():
return { return [
"nova": generators.NovaActionGenerator, generators.NovaActionGenerator,
"glance": generators.GlanceActionGenerator, generators.GlanceActionGenerator,
"keystone": generators.KeystoneActionGenerator generators.KeystoneActionGenerator,
} generators.HeatActionGenerator
]

View File

@ -37,27 +37,29 @@ class OpenStackActionGenerator(action_generator.ActionGenerator):
specific python-client and sets needed arguments specific python-client and sets needed arguments
to actions. to actions.
""" """
_action_namespace = None action_namespace = None
base_action_class = None base_action_class = None
def create_action_class(self, method_name): @classmethod
def create_action_class(cls, method_name):
if not method_name: if not method_name:
return None 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) setattr(action_class, 'client_method', method_name)
return action_class return action_class
def create_action_classes(self): @classmethod
def create_action_classes(cls):
mapping = json.loads(open(pkg.resource_filename( mapping = json.loads(open(pkg.resource_filename(
version.version_info.package, version.version_info.package,
MAPPING_PATH)).read()) MAPPING_PATH)).read())
method_dict = mapping[self._action_namespace] method_dict = mapping[cls.action_namespace]
action_classes = {} action_classes = {}
for action_name, method_name in method_dict.items(): 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 return action_classes

View File

@ -17,15 +17,20 @@ from mistral.actions.openstack import actions
class NovaActionGenerator(base.OpenStackActionGenerator): class NovaActionGenerator(base.OpenStackActionGenerator):
_action_namespace = "nova" action_namespace = "nova"
base_action_class = actions.NovaAction base_action_class = actions.NovaAction
class GlanceActionGenerator(base.OpenStackActionGenerator): class GlanceActionGenerator(base.OpenStackActionGenerator):
_action_namespace = "glance" action_namespace = "glance"
base_action_class = actions.GlanceAction base_action_class = actions.GlanceAction
class KeystoneActionGenerator(base.OpenStackActionGenerator): class KeystoneActionGenerator(base.OpenStackActionGenerator):
_action_namespace = "keystone" action_namespace = "keystone"
base_action_class = actions.KeystoneAction base_action_class = actions.KeystoneAction
class HeatActionGenerator(base.OpenStackActionGenerator):
action_namespace = "heat"
base_action_class = actions.HeatAction

View File

@ -12,13 +12,17 @@
# 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 inspect
from glanceclient.v2 import client as glanceclient from glanceclient.v2 import client as glanceclient
from heatclient.v1 import client as heatclient
from keystoneclient.v3 import client as keystoneclient from keystoneclient.v3 import client as keystoneclient
from novaclient.v1_1 import client as novaclient from novaclient.v1_1 import client as novaclient
from oslo.config import cfg from oslo.config import cfg
from mistral.actions.openstack import base from mistral.actions.openstack import base
from mistral import context from mistral import context
from mistral import exceptions as exc
from mistral.utils.openstack import keystone as keystone_utils 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, return self._client_class(token=ctx.auth_token,
auth_url=auth_url) 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))

View File

@ -306,5 +306,37 @@
"users_resource_class": "users.resource_class", "users_resource_class": "users.resource_class",
"users_update": "users.update", "users_update": "users.update",
"users_update_password": "users.update_password" "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"
} }
} }

View File

@ -0,0 +1,4 @@
Workflow:
tasks:
heat_stack_list:
action: heat.stacks_list

View File

@ -14,14 +14,14 @@
from oslotest import base from oslotest import base
from mistral.actions import generator_factory from mistral.actions.openstack.action_generator import generators
from mistral.actions.openstack import actions from mistral.actions.openstack import actions
class GlanceGeneratorTest(base.BaseTestCase): class GlanceGeneratorTest(base.BaseTestCase):
def test_generator(self): def test_generator(self):
action_name = "glance.images_list" action_name = "glance.images_list"
generator = generator_factory.all_generators()["glance"]() generator = generators.GlanceActionGenerator
action_classes = generator.create_action_classes() action_classes = generator.create_action_classes()
short_action_name = action_name.split(".")[1] short_action_name = action_name.split(".")[1]
action_class = action_classes[short_action_name] action_class = action_classes[short_action_name]

View File

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

View File

@ -14,14 +14,14 @@
from oslotest import base from oslotest import base
from mistral.actions import generator_factory from mistral.actions.openstack.action_generator import generators
from mistral.actions.openstack import actions from mistral.actions.openstack import actions
class KeystoneGeneratorTest(base.BaseTestCase): class KeystoneGeneratorTest(base.BaseTestCase):
def test_generator(self): def test_generator(self):
action_name = "keystone.users_create" action_name = "keystone.users_create"
generator = generator_factory.all_generators()["keystone"]() generator = generators.KeystoneActionGenerator
action_classes = generator.create_action_classes() action_classes = generator.create_action_classes()
short_action_name = action_name.split(".")[1] short_action_name = action_name.split(".")[1]
action_class = action_classes[short_action_name] action_class = action_classes[short_action_name]
@ -29,4 +29,4 @@ class KeystoneGeneratorTest(base.BaseTestCase):
self.assertIsNotNone(generator) self.assertIsNotNone(generator)
self.assertIn(short_action_name, action_classes) self.assertIn(short_action_name, action_classes)
self.assertTrue(issubclass(action_class, actions.KeystoneAction)) self.assertTrue(issubclass(action_class, actions.KeystoneAction))
self.assertEqual("users.create", action_class.client_method) self.assertEqual("users.create", action_class.client_method)

View File

@ -14,14 +14,14 @@
from oslotest import base from oslotest import base
from mistral.actions import generator_factory from mistral.actions.openstack.action_generator import generators
from mistral.actions.openstack import actions from mistral.actions.openstack import actions
class NovaGeneratorTest(base.BaseTestCase): class NovaGeneratorTest(base.BaseTestCase):
def test_generator(self): def test_generator(self):
action_name = "nova.servers_get" action_name = "nova.servers_get"
generator = generator_factory.all_generators()["nova"]() generator = generators.NovaActionGenerator
action_classes = generator.create_action_classes() action_classes = generator.create_action_classes()
short_action_name = action_name.split(".")[1] short_action_name = action_name.split(".")[1]
action_class = action_classes[short_action_name] action_class = action_classes[short_action_name]

View File

@ -54,3 +54,15 @@ class OpenStackActionTest(base.BaseTestCase):
self.assertTrue(mocked().users.get.called) self.assertTrue(mocked().users.get.called)
mocked().users.get.assert_called_once_with(user="1234-abcd") 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")

View File

@ -122,3 +122,28 @@ class OpenStackActionsEngineTest(base.EngineTestCase):
self.assertEqual(states.SUCCESS, task['state']) self.assertEqual(states.SUCCESS, task['state'])
self.assertEqual("servers", task['output']['task'][task_name]) 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])

View File

@ -15,6 +15,7 @@ oslo.config>=1.2.1
oslo.db>=0.2.0 # Apache-2.0 oslo.db>=0.2.0 # Apache-2.0
oslo.messaging>=1.3.0 oslo.messaging>=1.3.0
paramiko>=1.13.0 paramiko>=1.13.0
python-heatclient>=0.2.9
python-keystoneclient>=0.9.0 python-keystoneclient>=0.9.0
python-novaclient>=2.17 python-novaclient>=2.17
python-glanceclient>=0.13 python-glanceclient>=0.13