From: Andrew Plunk Date: Fri, 12 Jul 2013 14:56:07 +0000 (-0500) Subject: Generate a template from a resource implementation. X-Git-Tag: 2014.1~323^2 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=13a3d7c5ce90af622e6f7dac49991308ca1fa1e2;p=openstack-build%2Fheat-build.git Generate a template from a resource implementation. Using the properties and attributes schema in a specified resource, generate a template where all properties have been mapped as parameters, and all attributes have beeen mapped as outputs. blueprint resource-template Change-Id: I0f494f039e91daf482385f225f8551826cace485 --- diff --git a/heat/engine/properties.py b/heat/engine/properties.py index 810ddaaa..214abcd2 100644 --- a/heat/engine/properties.py +++ b/heat/engine/properties.py @@ -17,7 +17,7 @@ import collections import re from heat.common import exception - +from heat.engine import parameters SCHEMA_KEYS = ( REQUIRED, IMPLEMENTED, DEFAULT, TYPE, SCHEMA, @@ -249,3 +249,80 @@ class Properties(collections.Mapping): def __iter__(self): return iter(self.props) + + @staticmethod + def _generate_input(schema, params=None, path=None): + '''Generate an input based on a path in the schema or property + defaults. + + :param schema: The schema to generate a parameter or value for. + :param params: A dict to map a schema to a parameter path. + :param path: Required if params != None. The params key + to save the schema at. + :returns: A Ref to the parameter if path != None and params != None + :returns: The property default if params == None or path == None + ''' + if schema.get('Implemented') is False: + return + + if schema[TYPE] == LIST: + params[path] = {parameters.TYPE: parameters.COMMA_DELIMITED_LIST} + return {'Fn::Split': {'Ref': path}} + + elif schema[TYPE] == MAP: + params[path] = {parameters.TYPE: parameters.JSON} + return {'Ref': path} + + elif params is not None and path is not None: + for prop in schema.keys(): + if prop not in parameters.PARAMETER_KEYS and prop in schema: + del schema[prop] + params[path] = schema + return {'Ref': path} + else: + prop = Property(schema) + return prop.has_default() and prop.default() or None + + @staticmethod + def _schema_to_params_and_props(schema, params=None): + '''Generates a default template based on the provided schema. + + ex: input: schema = {'foo': {'Type': 'String'}}, params = {} + output: {'foo': {'Ref': 'foo'}}, + params = {'foo': {'Type': 'String'}} + + ex: input: schema = {'foo' :{'Type': 'List'}, 'bar': {'Type': 'Map'}} + ,params={} + output: {'foo': {'Fn::Split': {'Ref': 'foo'}}, + 'bar': {'Ref': 'bar'}}, + params = {'foo' : {parameters.TYPE: + parameters.COMMA_DELIMITED_LIST}, + 'bar': {parameters.TYPE: parameters.JSON}} + + :param schema: The schema to generate a parameter or value for. + :param params: A dict to map a schema to a parameter path. + :returns: A dict of properties resolved for a template's schema + ''' + properties = {} + for prop, nested_schema in schema.iteritems(): + properties[prop] = Properties._generate_input(nested_schema, + params, + prop) + #remove not implemented properties + if properties[prop] is None: + del properties[prop] + return properties + + @staticmethod + def schema_to_parameters_and_properties(schema): + '''Generates properties with params resolved for a resource's + properties_schema. + :param schema: A resource's properties_schema + :param explode_nested: True if a resource's nested properties schema + should be resolved. + :returns: A tuple of params and properties dicts + ''' + params = {} + properties = (Properties. + _schema_to_params_and_props(schema, params=params)) + return (params, properties) diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 59f5ef05..c12f6c27 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -686,3 +686,29 @@ class Resource(object): if new_metadata: logger.warning("Resource %s does not implement metadata update" % self.name) + + @classmethod + def resource_to_template(cls, resource_type): + ''' + :param resource_type: The resource type to be displayed in the template + :param explode_nested: True if a resource's nested properties schema + should be resolved. + :returns: A template where the resource's properties_schema is mapped + as parameters, and the resource's attributes_schema is mapped as + outputs + ''' + (parameters, properties) = (Properties. + schema_to_parameters_and_properties( + cls.properties_schema)) + + resource_name = cls.__name__ + return { + 'Parameters': parameters, + 'Resources': { + resource_name: { + 'Type': resource_type, + 'Properties': properties + } + }, + 'Outputs': Attributes.as_outputs(resource_name, cls) + } diff --git a/heat/tests/test_properties.py b/heat/tests/test_properties.py index 076ee000..f2eea9ba 100644 --- a/heat/tests/test_properties.py +++ b/heat/tests/test_properties.py @@ -573,3 +573,48 @@ class PropertiesValidationTest(testtools.TestCase): schema = {'foo': {'Type': 'List', 'Default': ['one', 'two']}} props = properties.Properties(schema, {'foo': None}) self.assertEqual(props.validate(), None) + + def test_schema_to_template_nested_map_map_schema(self): + nested_schema = {'Key': {'Type': 'String', + 'Required': True}, + 'Value': {'Type': 'String', + 'Required': True, + 'Default': 'fewaf'}} + schema = {'foo': {'Type': 'Map', 'Schema': {'Type': 'Map', + 'Schema': nested_schema}}} + + prop_expected = {'foo': {'Ref': 'foo'}} + param_expected = {'foo': {'Type': 'Json'}} + (parameters, props) = \ + properties.Properties.schema_to_parameters_and_properties(schema) + self.assertEquals(param_expected, parameters) + self.assertEquals(prop_expected, props) + + def test_schema_to_template_nested_map_list_map_schema(self): + key_schema = {'bar': {'Type': 'Number'}} + nested_schema = {'Key': {'Type': 'Map', 'Schema': {'Type': 'Map', + 'Schema': key_schema}}, + 'Value': {'Type': 'String', + 'Required': True}} + schema = {'foo': {'Type': 'List', 'Schema': {'Type': 'Map', + 'Schema': nested_schema}}} + + prop_expected = {'foo': {'Fn::Split': {'Ref': 'foo'}}} + param_expected = {'foo': {'Type': 'CommaDelimitedList'}} + (parameters, props) = \ + properties.Properties.schema_to_parameters_and_properties(schema) + self.assertEquals(param_expected, parameters) + self.assertEquals(prop_expected, props) + + def test_schema_invalid_parameters_stripped(self): + schema = {'foo': {'Type': 'String', + 'Required': True, + 'Implemented': True}} + + prop_expected = {'foo': {'Ref': 'foo'}} + param_expected = {'foo': {'Type': 'String'}} + + (parameters, props) = \ + properties.Properties.schema_to_parameters_and_properties(schema) + self.assertEquals(param_expected, parameters) + self.assertEquals(prop_expected, props) diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index 060e3842..b69c7ac5 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -476,6 +476,84 @@ class ResourceTest(HeatTestCase): self.assertRaises(exception.ResourceFailure, resume) self.assertEqual((res.RESUME, res.FAILED), res.state) + def test_resource_class_to_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': {'Type': 'Map', + 'Schema': map_schema}}, + } + + attributes_schema = { + 'output1': 'output1_desc', + 'output2': 'output2_desc' + } + + expected_template = { + 'Parameters': { + 'name': {'Type': 'String'}, + 'bool': {'Type': 'Boolean'}, + 'implemented': { + 'Type': 'String', + 'AllowedPattern': '.*', + 'MaxLength': 7, + 'MinLength': 2 + }, + 'number': {'Type': 'Number', + 'MaxValue': 77, + 'MinValue': 41, + 'Default': 42}, + 'list': {'Type': 'CommaDelimitedList'}, + 'map': {'Type': 'Json'} + }, + 'Resources': { + 'TestResource': { + 'Type': 'Test::Resource::resource', + 'Properties': { + 'name': {'Ref': 'name'}, + 'bool': {'Ref': 'bool'}, + 'implemented': {'Ref': 'implemented'}, + 'number': {'Ref': 'number'}, + 'list': {'Fn::Split': {'Ref': 'list'}}, + 'map': {'Ref': 'map'} + } + } + }, + 'Outputs': { + 'output1': { + 'Description': 'output1_desc', + 'Value': '{"Fn::GetAtt": ["TestResource", "output1"]}' + }, + 'output2': { + 'Description': 'output2_desc', + 'Value': '{"Fn::GetAtt": ["TestResource", "output2"]}' + } + } + } + self.assertEqual(expected_template, + TestResource.resource_to_template( + 'Test::Resource::resource') + ) + class MetadataTest(HeatTestCase): def setUp(self):