From b44df7a1dbb9131b075fd18082882bb906941fb5 Mon Sep 17 00:00:00 2001 From: huangtianhua Date: Tue, 14 Apr 2015 11:52:48 +0800 Subject: [PATCH] Support to generate hot template based on resource type Currently heat supports to create CFN template based on the given resource type. And this patch adds an option to allow user to specify template type to generate(support HOT template at the same time). blueprint support-to-generate-hot-templates Change-Id: I55cfb9b0f87e638350f2f6367fb399d772fff7e1 --- heat/api/openstack/v1/stacks.py | 13 ++++- heat/common/param_utils.py | 9 ++++ heat/engine/attributes.py | 28 ++++++---- heat/engine/cfn/functions.py | 2 +- heat/engine/properties.py | 65 +++++++++++++++++++--- heat/engine/resource.py | 53 +++++++++++++----- heat/engine/service.py | 7 +-- heat/rpc/api.py | 4 +- heat/rpc/client.py | 8 ++- heat/tests/test_api_openstack_v1.py | 25 ++++++++- heat/tests/test_engine_service.py | 2 +- heat/tests/test_resource.py | 83 ++++++++++++++++++++++++++++- heat/tests/test_rpc_client.py | 3 +- 13 files changed, 259 insertions(+), 43 deletions(-) diff --git a/heat/api/openstack/v1/stacks.py b/heat/api/openstack/v1/stacks.py index 8eaa8be262..46254e4f7a 100644 --- a/heat/api/openstack/v1/stacks.py +++ b/heat/api/openstack/v1/stacks.py @@ -506,7 +506,18 @@ class StackController(object): """ Generates a template based on the specified type. """ - return self.rpc_client.generate_template(req.context, type_name) + template_type = 'cfn' + if rpc_api.TEMPLATE_TYPE in req.params: + try: + template_type = param_utils.extract_template_type( + req.params.get(rpc_api.TEMPLATE_TYPE)) + except ValueError as ex: + msg = _("Template type is not supported: %s") % ex + raise exc.HTTPBadRequest(six.text_type(msg)) + + return self.rpc_client.generate_template(req.context, + type_name, + template_type) @util.identified_stack def snapshot(self, req, identity, body): diff --git a/heat/common/param_utils.py b/heat/common/param_utils.py index 3f2e44d40d..dc32ca6fe6 100644 --- a/heat/common/param_utils.py +++ b/heat/common/param_utils.py @@ -62,3 +62,12 @@ def extract_tags(subject): raise ValueError(_('Invalid tag, "%s" is longer than 80 ' 'characters') % tag) return tags + + +def extract_template_type(subject): + template_type = subject.lower() + if template_type not in ('cfn', 'hot'): + raise ValueError(_('Invalid template type "%(value)s", valid ' + 'types are: cfn, hot.') % + {'value': subject}) + return template_type diff --git a/heat/engine/attributes.py b/heat/engine/attributes.py index f35d61ffa5..44f01e0510 100644 --- a/heat/engine/attributes.py +++ b/heat/engine/attributes.py @@ -95,19 +95,28 @@ class Attribute(object): def support_status(self): return self.schema.support_status - def as_output(self, resource_name): + def as_output(self, resource_name, template_type='cfn'): """ Return an Output schema entry for a provider template with the given resource name. :param resource_name: the logical name of the provider resource - :returns: This attribute as a template 'Output' entry + :param template_type: the template type to generate + :returns: This attribute as a template 'Output' entry for + cfn template and 'output' entry for hot template """ - return { - "Value": '{"Fn::GetAtt": ["%s", "%s"]}' % (resource_name, - self.name), - "Description": self.schema.description - } + if template_type == 'hot': + return { + "value": '{"get_attr": ["%s", "%s"]}' % (resource_name, + self.name), + "description": self.schema.description + } + else: + return { + "Value": '{"Fn::GetAtt": ["%s", "%s"]}' % (resource_name, + self.name), + "Description": self.schema.description + } class Attributes(collections.Mapping): @@ -127,7 +136,7 @@ class Attributes(collections.Mapping): return dict((n, Attribute(n, d)) for n, d in schema.items()) @staticmethod - def as_outputs(resource_name, resource_class): + def as_outputs(resource_name, resource_class, template_type='cfn'): """ :param resource_name: logical name of the resource :param resource_class: resource implementation class @@ -137,7 +146,8 @@ class Attributes(collections.Mapping): schema = resource_class.attributes_schema attribs = Attributes._make_attributes(schema).items() - return dict((n, att.as_output(resource_name)) for n, att in attribs) + return dict((n, att.as_output(resource_name, + template_type)) for n, att in attribs) @staticmethod def schema_from_outputs(json_snippet): diff --git a/heat/engine/cfn/functions.py b/heat/engine/cfn/functions.py index 42db1e8a84..08a1ab6425 100644 --- a/heat/engine/cfn/functions.py +++ b/heat/engine/cfn/functions.py @@ -21,7 +21,6 @@ from heat.api.aws import utils as aws_utils from heat.common import exception from heat.common.i18n import _ from heat.engine import function -from heat.engine import resource class FindInMap(function.Function): @@ -187,6 +186,7 @@ class GetAtt(function.Function): super(GetAtt, self).validate() res = self._resource() attr = function.resolve(self._attribute) + from heat.engine import resource if (type(res).FnGetAtt == resource.Resource.FnGetAtt and attr not in six.iterkeys(res.attributes_schema)): raise exception.InvalidTemplateAttribute( diff --git a/heat/engine/properties.py b/heat/engine/properties.py index 559e80a9f2..f6a99a431c 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -20,6 +20,7 @@ from heat.common import exception from heat.common.i18n import _ from heat.engine import constraints as constr from heat.engine import function +from heat.engine.hot import parameters as hot_param from heat.engine import parameters from heat.engine import support @@ -516,8 +517,57 @@ class Properties(collections.Mapping): else: return {'Ref': name} + @staticmethod + def _hot_param_def_from_prop(schema): + """ + Return parameter definition corresponding to a property for + hot template. + """ + param_type_map = { + schema.INTEGER: hot_param.HOTParamSchema.NUMBER, + schema.STRING: hot_param.HOTParamSchema.STRING, + schema.NUMBER: hot_param.HOTParamSchema.NUMBER, + schema.BOOLEAN: hot_param.HOTParamSchema.BOOLEAN, + schema.MAP: hot_param.HOTParamSchema.MAP, + schema.LIST: hot_param.HOTParamSchema.LIST, + } + + def param_items(): + yield hot_param.HOTParamSchema.TYPE, param_type_map[schema.type] + + if schema.description is not None: + yield hot_param.HOTParamSchema.DESCRIPTION, schema.description + + if schema.default is not None: + yield hot_param.HOTParamSchema.DEFAULT, schema.default + + for constraint in schema.constraints: + if (isinstance(constraint, constr.Length) or + isinstance(constraint, constr.Range)): + if constraint.min is not None: + yield hot_param.MIN, constraint.min + if constraint.max is not None: + yield hot_param.MAX, constraint.max + elif isinstance(constraint, constr.AllowedValues): + yield hot_param.ALLOWED_VALUES, list(constraint.allowed) + elif isinstance(constraint, constr.AllowedPattern): + yield hot_param.ALLOWED_PATTERN, constraint.pattern + + if schema.type == schema.BOOLEAN: + yield hot_param.ALLOWED_VALUES, ['True', 'true', + 'False', 'false'] + + return dict(param_items()) + + @staticmethod + def _hot_prop_def_from_prop(name, schema): + """ + Return a provider template property definition for a property. + """ + return {'get_param': name} + @classmethod - def schema_to_parameters_and_properties(cls, schema): + def schema_to_parameters_and_properties(cls, schema, template_type='cfn'): """Generates properties with params resolved for a resource's properties_schema. @@ -532,16 +582,19 @@ class Properties(collections.Mapping): output: {'foo': {'Type': 'String'}, 'bar': {'Type': 'Json'}}, {'foo': {'Ref': 'foo'}, 'bar': {'Ref': 'bar'}} """ - def param_prop_def_items(name, schema): - param_def = cls._param_def_from_prop(schema) - prop_def = cls._prop_def_from_prop(name, schema) - + def param_prop_def_items(name, schema, template_type): + if template_type == 'hot': + param_def = cls._hot_param_def_from_prop(schema) + prop_def = cls._hot_prop_def_from_prop(name, schema) + else: + param_def = cls._param_def_from_prop(schema) + prop_def = cls._prop_def_from_prop(name, schema) return (name, param_def), (name, prop_def) if not schema: return {}, {} - param_prop_defs = [param_prop_def_items(n, s) + param_prop_defs = [param_prop_def_items(n, s, template_type) for n, s in six.iteritems(schemata(schema)) if s.implemented] param_items, prop_items = zip(*param_prop_defs) diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 9eabe063fc..e82fad84b5 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -31,9 +31,11 @@ from heat.common import identifier from heat.common import short_id from heat.common import timeutils from heat.engine import attributes +from heat.engine.cfn import template as cfn_tmpl from heat.engine import environment from heat.engine import event from heat.engine import function +from heat.engine.hot import template as hot_tmpl from heat.engine import properties from heat.engine import resources from heat.engine import rsrc_defn @@ -1183,29 +1185,52 @@ class Resource(object): self.name) @classmethod - def resource_to_template(cls, resource_type): + def resource_to_template(cls, resource_type, template_type='cfn'): ''' :param resource_type: The resource type to be displayed in the template + :param template_type: the template type to generate, cfn or hot. :returns: A template where the resource's properties_schema is mapped as parameters, and the resource's attributes_schema is mapped as outputs ''' schema = cls.properties_schema params, props = (properties.Properties. - schema_to_parameters_and_properties(schema)) - + schema_to_parameters_and_properties(schema, + template_type)) resource_name = cls.__name__ - return { - 'HeatTemplateFormatVersion': '2012-12-12', - 'Parameters': params, - 'Resources': { - resource_name: { - 'Type': resource_type, - 'Properties': props - } - }, - 'Outputs': attributes.Attributes.as_outputs(resource_name, cls) - } + outputs = attributes.Attributes.as_outputs(resource_name, cls, + template_type) + description = 'Initial template of %s' % resource_name + return cls.build_template_dict(resource_name, resource_type, + template_type, params, props, + outputs, description) + + @staticmethod + def build_template_dict(res_name, res_type, tmpl_type, + params, props, outputs, description): + if tmpl_type == 'hot': + tmpl_dict = { + hot_tmpl.HOTemplate20150430.VERSION: '2015-04-30', + hot_tmpl.HOTemplate20150430.DESCRIPTION: description, + hot_tmpl.HOTemplate20150430.PARAMETERS: params, + hot_tmpl.HOTemplate20150430.OUTPUTS: outputs, + hot_tmpl.HOTemplate20150430.RESOURCES: { + res_name: { + hot_tmpl.RES_TYPE: res_type, + hot_tmpl.RES_PROPERTIES: props}}} + else: + tmpl_dict = { + cfn_tmpl.CfnTemplate.ALTERNATE_VERSION: '2012-12-12', + cfn_tmpl.CfnTemplate.DESCRIPTION: description, + cfn_tmpl.CfnTemplate.PARAMETERS: params, + cfn_tmpl.CfnTemplate.RESOURCES: { + res_name: { + cfn_tmpl.RES_TYPE: res_type, + cfn_tmpl.RES_PROPERTIES: props} + }, + cfn_tmpl.CfnTemplate.OUTPUTS: outputs} + + return tmpl_dict def data(self): ''' diff --git a/heat/engine/service.py b/heat/engine/service.py index 73ee234523..201d09387c 100644 --- a/heat/engine/service.py +++ b/heat/engine/service.py @@ -266,7 +266,7 @@ class EngineService(service.Service): by the RPC caller. """ - RPC_API_VERSION = '1.8' + RPC_API_VERSION = '1.9' def __init__(self, host, topic, manager=None): super(EngineService, self).__init__() @@ -1028,16 +1028,17 @@ class EngineService(service.Service): rpc_api.RES_SCHEMA_ATTRIBUTES: dict(attributes_schema()), } - def generate_template(self, cnxt, type_name): + def generate_template(self, cnxt, type_name, template_type='cfn'): """ Generate a template based on the specified type. :param cnxt: RPC context. :param type_name: Name of the resource type to generate a template for. + :param template_type: the template type to generate, cfn or hot. """ try: return resources.global_env().get_class( - type_name).resource_to_template(type_name) + type_name).resource_to_template(type_name, template_type) except (exception.InvalidResourceType, exception.ResourceTypeNotFound, exception.TemplateNotFound) as ex: diff --git a/heat/rpc/api.py b/heat/rpc/api.py index c7bffb9994..8221b92d1e 100644 --- a/heat/rpc/api.py +++ b/heat/rpc/api.py @@ -19,13 +19,13 @@ PARAM_KEYS = ( PARAM_SHOW_DELETED, PARAM_SHOW_NESTED, PARAM_EXISTING, PARAM_CLEAR_PARAMETERS, PARAM_GLOBAL_TENANT, PARAM_LIMIT, PARAM_NESTED_DEPTH, PARAM_TAGS, PARAM_SHOW_HIDDEN, PARAM_TAGS_ANY, - PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY + PARAM_NOT_TAGS, PARAM_NOT_TAGS_ANY, TEMPLATE_TYPE, ) = ( 'timeout_mins', 'disable_rollback', 'adopt_stack_data', 'show_deleted', 'show_nested', 'existing', 'clear_parameters', 'global_tenant', 'limit', 'nested_depth', 'tags', 'show_hidden', 'tags_any', - 'not_tags', 'not_tags_any' + 'not_tags', 'not_tags_any', 'template_type', ) STACK_KEYS = ( diff --git a/heat/rpc/client.py b/heat/rpc/client.py index ecfdadc90e..68b0315779 100644 --- a/heat/rpc/client.py +++ b/heat/rpc/client.py @@ -29,6 +29,7 @@ class EngineClient(object): 1.0 - Initial version. 1.1 - Add support_status argument to list_resource_types() 1.4 - Add support for service list + 1.9 - Add template_type option to generate_template() ''' BASE_RPC_API_VERSION = '1.0' @@ -334,15 +335,18 @@ class EngineClient(object): return self.call(ctxt, self.make_msg('resource_schema', type_name=type_name)) - def generate_template(self, ctxt, type_name): + def generate_template(self, ctxt, type_name, template_type='cfn'): """ Generate a template based on the specified type. :param ctxt: RPC context. :param type_name: The resource type name to generate a template for. + :param template_type: the template type to generate, cfn or hot. """ return self.call(ctxt, self.make_msg('generate_template', - type_name=type_name)) + type_name=type_name, + template_type=template_type), + version='1.9') def list_events(self, ctxt, stack_identity, filters=None, limit=None, marker=None, sort_keys=None, sort_dir=None,): diff --git a/heat/tests/test_api_openstack_v1.py b/heat/tests/test_api_openstack_v1.py index 7a26c0cc36..de33d60685 100644 --- a/heat/tests/test_api_openstack_v1.py +++ b/heat/tests/test_api_openstack_v1.py @@ -2071,13 +2071,32 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): self.m.StubOutWithMock(rpc_client.EngineClient, 'call') rpc_client.EngineClient.call( req.context, - ('generate_template', {'type_name': 'TEST_TYPE'}) + ('generate_template', {'type_name': 'TEST_TYPE', + 'template_type': 'cfn'}), + version='1.9' ).AndReturn(engine_response) self.m.ReplayAll() self.controller.generate_template(req, tenant_id=self.tenant, type_name='TEST_TYPE') self.m.VerifyAll() + def test_generate_template_invalid_template_type(self, mock_enforce): + self._mock_enforce_setup(mock_enforce, 'generate_template', True) + params = {'template_type': 'invalid'} + mock_call = self.patchobject(rpc_client.EngineClient, 'call') + + req = self._get('/resource_types/TEST_TYPE/template', + params=params) + + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.generate_template, + req, tenant_id=self.tenant, + type_name='TEST_TYPE') + self.assertIn('Template type is not supported: Invalid template ' + 'type "invalid", valid types are: cfn, hot.', + six.text_type(ex)) + self.assertFalse(mock_call.called) + def test_generate_template_not_found(self, mock_enforce): self._mock_enforce_setup(mock_enforce, 'generate_template', True) req = self._get('/resource_types/NOT_FOUND/template') @@ -2086,7 +2105,9 @@ class StackControllerTest(ControllerTest, common.HeatTestCase): self.m.StubOutWithMock(rpc_client.EngineClient, 'call') rpc_client.EngineClient.call( req.context, - ('generate_template', {'type_name': 'NOT_FOUND'}) + ('generate_template', {'type_name': 'NOT_FOUND', + 'template_type': 'cfn'}), + version='1.9' ).AndRaise(to_remote_error(error)) self.m.ReplayAll() resp = request_with_middleware(fault.FaultWrapper, diff --git a/heat/tests/test_engine_service.py b/heat/tests/test_engine_service.py index 6b8ff952d9..41aa68c902 100644 --- a/heat/tests/test_engine_service.py +++ b/heat/tests/test_engine_service.py @@ -1402,7 +1402,7 @@ class StackServiceTest(common.HeatTestCase): def test_make_sure_rpc_version(self): self.assertEqual( - '1.8', + '1.9', service.EngineService.RPC_API_VERSION, ('RPC version is changed, please update this test to new version ' 'and make sure additional test cases are added for RPC APIs ' diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 7100c0f314..9782fa2beb 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -1011,7 +1011,7 @@ class ResourceTest(common.HeatTestCase): self.assertRaises(exception.ResourceFailure, resume) self.assertEqual((res.RESUME, res.FAILED), res.state) - def test_resource_class_to_template(self): + def test_resource_class_to_cfn_template(self): class TestResource(resource.Resource): list_schema = {'wont_show_up': {'Type': 'Number'}} @@ -1044,6 +1044,7 @@ class ResourceTest(common.HeatTestCase): expected_template = { 'HeatTemplateFormatVersion': '2012-12-12', + 'Description': 'Initial template of TestResource', 'Parameters': { 'name': {'Type': 'String'}, 'bool': {'Type': 'Boolean', @@ -1089,6 +1090,86 @@ class ResourceTest(common.HeatTestCase): TestResource.resource_to_template( 'Test::Resource::resource')) + def test_resource_class_to_hot_template(self): + + class TestResource(resource.Resource): + list_schema = {'wont_show_up': {'Type': 'Number'}} + map_schema = {'will_show_up': {'Type': 'Integer'}} + + properties_schema = { + 'name': {'Type': 'String'}, + 'bool': {'Type': 'Boolean'}, + 'implemented': {'Type': 'String', + 'Implemented': True, + 'AllowedPattern': '.*', + 'MaxLength': 7, + 'MinLength': 2, + 'Required': True}, + 'not_implemented': {'Type': 'String', + 'Implemented': False}, + 'number': {'Type': 'Number', + 'MaxValue': 77, + 'MinValue': 41, + 'Default': 42}, + 'list': {'Type': 'List', 'Schema': {'Type': 'Map', + 'Schema': list_schema}}, + 'map': {'Type': 'Map', 'Schema': map_schema}, + } + + attributes_schema = { + 'output1': attributes.Schema('output1_desc'), + 'output2': attributes.Schema('output2_desc') + } + + expected_template = { + 'heat_template_version': '2015-04-30', + 'description': 'Initial template of TestResource', + 'parameters': { + 'name': {'type': 'string'}, + 'bool': {'type': 'boolean', + 'allowed_values': ['True', 'true', 'False', 'false']}, + 'implemented': { + 'type': 'string', + 'allowed_pattern': '.*', + 'max': 7, + 'min': 2 + }, + 'number': {'type': 'number', + 'max': 77, + 'min': 41, + 'default': 42}, + 'list': {'type': 'comma_delimited_list'}, + 'map': {'type': 'json'} + }, + 'resources': { + 'TestResource': { + 'type': 'Test::Resource::resource', + 'properties': { + 'name': {'get_param': 'name'}, + 'bool': {'get_param': 'bool'}, + 'implemented': {'get_param': 'implemented'}, + 'number': {'get_param': 'number'}, + 'list': {'get_param': 'list'}, + 'map': {'get_param': 'map'} + } + } + }, + 'outputs': { + 'output1': { + 'description': 'output1_desc', + 'value': '{"get_attr": ["TestResource", "output1"]}' + }, + 'output2': { + 'description': 'output2_desc', + 'value': '{"get_attr": ["TestResource", "output2"]}' + } + } + } + self.assertEqual(expected_template, + TestResource.resource_to_template( + 'Test::Resource::resource', + template_type='hot')) + def test_is_using_neutron(self): snippet = rsrc_defn.ResourceDefinition('aresource', 'GenericResourceType') diff --git a/heat/tests/test_rpc_client.py b/heat/tests/test_rpc_client.py index 676944fb93..223c67f34c 100644 --- a/heat/tests/test_rpc_client.py +++ b/heat/tests/test_rpc_client.py @@ -200,7 +200,8 @@ class EngineRpcAPITestCase(common.HeatTestCase): self._test_engine_api('resource_schema', 'call', type_name="TYPE") def test_generate_template(self): - self._test_engine_api('generate_template', 'call', type_name="TYPE") + self._test_engine_api('generate_template', 'call', + type_name="TYPE", template_type='cfn') def test_list_events(self): kwargs = {'stack_identity': self.identity,