From e05f39c37b115259cf64e2523d7da443507f389c Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Fri, 27 Jul 2012 21:57:59 +1200 Subject: [PATCH] Implements a client side engine RPC API. Patch 1/2, this implements github heat issue 181. The topic string for the rpc call is built from the cfg.CONF parameters 'host' and 'engine_topic' defined in heat.common.config.engine_opts. delete_stack seems to be the only method which returns nothing, so it can be invoked as cast or call, with cast being the default. The tests exercise each api method with the same approach found in nova/nova/tests/compute/test_rpcapi.py Change-Id: Ia20850821083d1236ad628172db00d01f073313c --- heat/common/config.py | 3 + heat/engine/rpcapi.py | 221 ++++++++++++++++++++++++++++++++++++++ heat/tests/test_rpcapi.py | 167 ++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) create mode 100644 heat/engine/rpcapi.py create mode 100644 heat/tests/test_rpcapi.py diff --git a/heat/common/config.py b/heat/common/config.py index 081cf180..ed9c9a52 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -98,6 +98,9 @@ cfg.StrOpt('host', cfg.StrOpt('instance_driver', default='heat.engine.nova', help='Driver to use for controlling instances'), +cfg.StrOpt('engine_topic', + default='engine', + help='the topic engine nodes listen on') ] diff --git a/heat/engine/rpcapi.py b/heat/engine/rpcapi.py new file mode 100644 index 00000000..50829bdb --- /dev/null +++ b/heat/engine/rpcapi.py @@ -0,0 +1,221 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Red Hat, 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. + +""" +Client side of the heat engine RPC API. +""" + +from heat.openstack.common import cfg +from heat.openstack.common import exception +from heat.openstack.common import rpc +from heat.openstack.common.rpc import common as rpc_common +import heat.openstack.common.rpc.proxy + + +FLAGS = cfg.CONF + + +def _engine_topic(topic, ctxt, host): + '''Get the topic to use for a message. + + :param topic: the base topic + :param ctxt: request context + :param host: explicit host to send the message to. + + :returns: A topic string + ''' + if not host: + host = cfg.CONF.host + return rpc.queue_get_for(ctxt, topic, host) + + +class EngineAPI(heat.openstack.common.rpc.proxy.RpcProxy): + '''Client side of the heat engine rpc API. + + API version history: + + 1.0 - Initial version. + ''' + + BASE_RPC_API_VERSION = '1.0' + + def __init__(self): + super(EngineAPI, self).__init__( + topic=FLAGS.engine_topic, + default_version=self.BASE_RPC_API_VERSION) + + def show_stack(self, ctxt, stack_name, params): + """ + The show_stack method returns the attributes of one stack. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to see, + or None to see all + :param params: Dict of http request parameters passed in from API side. + """ + return self.call(ctxt, self.make_msg('show_stack', + stack_name=stack_name, params=params), + topic=_engine_topic(self.topic, ctxt, None)) + + def create_stack(self, ctxt, stack_name, template, params, args): + """ + The create_stack method creates a new stack using the template + provided. + Note that at this stage the template has already been fetched from the + heat-api process if using a template-url. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to create. + :param template: Template of stack you want to create. + :param params: Stack Input Params + :param args: Request parameters/args passed from API + """ + return self.call(ctxt, + self.make_msg('create_stack', stack_name=stack_name, + template=template, params=params, args=args), + topic=_engine_topic(self.topic, ctxt, None)) + + def update_stack(self, ctxt, stack_name, template, params, args): + """ + The update_stack method updates an existing stack based on the + provided template and parameters. + Note that at this stage the template has already been fetched from the + heat-api process if using a template-url. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to create. + :param template: Template of stack you want to create. + :param params: Stack Input Params + :param args: Request parameters/args passed from API + """ + return self.call(ctxt, self.make_msg('update_stack', + stack_name=stack_name, + template=template, params=params, args=args), + topic=_engine_topic(self.topic, ctxt, None)) + + def validate_template(self, ctxt, template, params): + """ + The validate_template method uses the stack parser to check + the validity of a template. + + :param ctxt: RPC context. + :param template: Template of stack you want to create. + :param params: Params passed from API. + """ + return self.call(ctxt, self.make_msg('validate_template', + template=template, params=params), + topic=_engine_topic(self.topic, ctxt, None)) + + def get_template(self, ctxt, stack_name, params): + """ + Get the template. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to see. + :param params: Dict of http request parameters passed in from API side. + """ + return self.call(ctxt, self.make_msg('get_template', + stack_name=stack_name, params=params), + topic=_engine_topic(self.topic, ctxt, None)) + + def delete_stack(self, ctxt, stack_name, params, cast=True): + """ + The delete_stack method deletes a given stack. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to delete. + :param params: Params passed from API. + """ + rpc_method = self.cast if cast else self.call + return rpc_method(ctxt, self.make_msg('delete_stack', + stack_name=stack_name, params=params), + topic=_engine_topic(self.topic, ctxt, None)) + + def list_events(self, ctxt, stack_name, params): + """ + The list_events method lists all events associated with a given stack. + + :param ctxt: RPC context. + :param stack_name: Name of the stack you want to get events for. + :param params: Params passed from API. + """ + return self.call(ctxt, self.make_msg('list_events', + stack_name=stack_name, params=params), + topic=_engine_topic(self.topic, ctxt, None)) + + def describe_stack_resource(self, ctxt, stack_name, resource_name): + return self.call(ctxt, self.make_msg('describe_stack_resource', + stack_name=stack_name, resource_name=resource_name), + topic=_engine_topic(self.topic, ctxt, None)) + + def describe_stack_resources(self, ctxt, stack_name, + physical_resource_id, logical_resource_id): + return self.call(ctxt, self.make_msg('describe_stack_resources', + stack_name=stack_name, + physical_resource_id=physical_resource_id, + logical_resource_id=logical_resource_id), + topic=_engine_topic(self.topic, ctxt, None)) + + def list_stack_resources(self, ctxt, stack_name): + return self.call(ctxt, self.make_msg('list_stack_resources', + stack_name=stack_name), + topic=_engine_topic(self.topic, ctxt, None)) + + def metadata_list_stacks(self, ctxt): + """ + Return the names of the stacks registered with Heat. + """ + return self.call(ctxt, self.make_msg('metadata_list_stacks'), + topic=_engine_topic(self.topic, ctxt, None)) + + def metadata_list_resources(self, ctxt, stack_name): + """ + Return the resource IDs of the given stack. + """ + return self.call(ctxt, self.make_msg('metadata_list_resources', + stack_name=stack_name), + topic=_engine_topic(self.topic, ctxt, None)) + + def metadata_get_resource(self, ctxt, stack_name, resource_name): + """ + Get the metadata for the given resource. + """ + return self.call(ctxt, self.make_msg('metadata_get_resource', + stack_name=stack_name, resource_name=resource_name), + topic=_engine_topic(self.topic, ctxt, None)) + + def metadata_update(self, ctxt, stack_id, resource_name, metadata): + """ + Update the metadata for the given resource. + """ + return self.call(ctxt, self.make_msg('metadata_update', + stack_id=stack_id, + resource_name=resource_name, metadata=metadata), + topic=_engine_topic(self.topic, ctxt, None)) + + def event_create(self, ctxt, event): + return self.call(ctxt, self.make_msg('event_create', + event=event), + topic=_engine_topic(self.topic, ctxt, None)) + + def create_watch_data(self, ctxt, watch_name, stats_data): + ''' + This could be used by CloudWatch and WaitConditions + and treat HA service events like any other CloudWatch. + ''' + return self.call(ctxt, self.make_msg('create_watch_data', + watch_name=watch_name, stats_data=stats_data), + topic=_engine_topic(self.topic, ctxt, None)) diff --git a/heat/tests/test_rpcapi.py b/heat/tests/test_rpcapi.py new file mode 100644 index 00000000..7671fc11 --- /dev/null +++ b/heat/tests/test_rpcapi.py @@ -0,0 +1,167 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012, Red Hat, 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. + +""" +Unit Tests for heat.engine.rpcapi +""" + + +import stubout +import unittest + +from heat.common import config +from heat.common import context +from heat.engine import rpcapi as engine_rpcapi +from heat.openstack.common import cfg +from heat.openstack.common import rpc + + +class EngineRpcAPITestCase(unittest.TestCase): + + def setUp(self): + self.context = context.get_admin_context() + config.register_engine_opts() + cfg.CONF.set_default('rpc_backend', + 'heat.openstack.common.rpc.impl_fake') + cfg.CONF.set_default('verbose', True) + cfg.CONF.set_default('engine_topic', 'engine') + cfg.CONF.set_default('host', 'host') + + self.stubs = stubout.StubOutForTesting() + super(EngineRpcAPITestCase, self).setUp() + + def tearDown(self): + super(EngineRpcAPITestCase, self).tearDown() + + def _test_engine_api(self, method, rpc_method, **kwargs): + ctxt = context.RequestContext('fake_user', 'fake_project') + if 'rpcapi_class' in kwargs: + rpcapi_class = kwargs['rpcapi_class'] + del kwargs['rpcapi_class'] + else: + rpcapi_class = engine_rpcapi.EngineAPI + rpcapi = rpcapi_class() + expected_retval = 'foo' if method == 'call' else None + + expected_version = kwargs.pop('version', rpcapi.BASE_RPC_API_VERSION) + expected_msg = rpcapi.make_msg(method, **kwargs) + + expected_msg['version'] = expected_version + expected_topic = '%s.%s' % (cfg.CONF.engine_topic, cfg.CONF.host) + + cast_and_call = ['delete_stack'] + if rpc_method == 'call' and method in cast_and_call: + kwargs['cast'] = False + + self.fake_args = None + self.fake_kwargs = None + + def _fake_rpc_method(*args, **kwargs): + self.fake_args = args + self.fake_kwargs = kwargs + if expected_retval: + return expected_retval + + self.stubs.Set(rpc, rpc_method, _fake_rpc_method) + + retval = getattr(rpcapi, method)(ctxt, **kwargs) + + self.assertEqual(retval, expected_retval) + expected_args = [ctxt, expected_topic, expected_msg] + for arg, expected_arg in zip(self.fake_args, expected_args): + self.assertEqual(arg, expected_arg) + + def test_show_stack(self): + self._test_engine_api('show_stack', 'call', stack_name='wordpress', + params={'Action': 'ListStacks'}) + + def test_create_stack(self): + self._test_engine_api('create_stack', 'call', stack_name='wordpress', + template={u'Foo': u'bar'}, + params={u'InstanceType': u'm1.xlarge'}, + args={'timeout_mins': u'30'}) + + def test_update_stack(self): + self._test_engine_api('update_stack', 'call', stack_name='wordpress', + template={u'Foo': u'bar'}, + params={u'InstanceType': u'm1.xlarge'}, + args={}) + + def test_validate_template(self): + self._test_engine_api('validate_template', 'call', + template={u'Foo': u'bar'}, params={}) + + def test_get_template(self): + self._test_engine_api('get_template', 'call', + stack_name='wordpress', params={}) + + def test_delete_stack_cast(self): + self._test_engine_api('delete_stack', 'cast', + stack_name='wordpress', params={}) + + def test_delete_stack_call(self): + self._test_engine_api('delete_stack', 'call', + stack_name='wordpress', params={}) + + def test_list_events(self): + self._test_engine_api('list_events', 'call', + stack_name='wordpress', params={}) + + def test_describe_stack_resource(self): + self._test_engine_api('describe_stack_resource', 'call', + stack_name='wordpress', + resource_name='LogicalResourceId') + + def test_describe_stack_resources(self): + self._test_engine_api('describe_stack_resources', 'call', + stack_name='wordpress', + physical_resource_id=u'404d-a85b-5315293e67de', + logical_resource_id=u'WikiDatabase') + + def test_list_stack_resources(self): + self._test_engine_api('list_stack_resources', 'call', + stack_name='wordpress') + + def test_metadata_list_stacks(self): + self._test_engine_api('metadata_list_stacks', 'call') + + def test_metadata_list_resources(self): + self._test_engine_api('metadata_list_resources', 'call', + stack_name='wordpress') + + def test_metadata_get_resource(self): + self._test_engine_api('metadata_get_resource', 'call', + stack_name='wordpress', + resource_name='LogicalResourceId') + + def test_metadata_update(self): + self._test_engine_api('metadata_update', 'call', + stack_id=6, + resource_name='LogicalResourceId', + metadata={u'wordpress': []}) + + def test_event_create(self): + self._test_engine_api('event_create', 'call', + event={ + 'stack': 'wordpress', + 'resource': 'LogicalResourceId', + 'message': 'Foo', + 'reason': 'Bar'}) + + def test_create_watch_data(self): + self._test_engine_api('create_watch_data', 'call', + watch_name='watch1', + stats_data={}) -- 2.45.2