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,