From d246508752829f17d81eccc234252207acf2b7eb Mon Sep 17 00:00:00 2001 From: Pablo Andres Fuente Date: Fri, 17 Jan 2014 14:15:40 -0300 Subject: [PATCH] HOT templates get_attr allows extra attributes The HOT templates now allows the use of extra attributes for the get_attr function. Wich this change is possible to traverse complex resource attributes. For example, getting an IP address for a server on a given network: when the network name is stable, and always known: get_attr: - my_server - networks - public - 0 or if the network name is something other than public: get_attr: - my_server - networks - get_param: my_network_name - 0 If one of the extra arguments does not match with a key or a valid index, an empty string is returned. As a bonus track the Fn::GetAttr support was removed from the HOT templates. Change-Id: Ia7361fbd10611cc796bd90e781f4ae387c970dbd Implements: blueprint hot-select --- doc/source/template_guide/hot_spec.rst | 29 +++- heat/engine/hot.py | 22 ++- heat/tests/generic_resource.py | 45 +++++- heat/tests/test_hot.py | 189 ++++++++++++++++++++++--- heat/tests/test_parser.py | 2 + 5 files changed, 255 insertions(+), 32 deletions(-) diff --git a/doc/source/template_guide/hot_spec.rst b/doc/source/template_guide/hot_spec.rst index bd11d465c..c3ac22b3e 100644 --- a/doc/source/template_guide/hot_spec.rst +++ b/doc/source/template_guide/hot_spec.rst @@ -472,23 +472,31 @@ resource definition is shown below. get_attr -------- -The *get_attr* function allows for referencing an attribute of a resource. At +The *get_attr* function allows referencing an attribute of a resource. At runtime, it will be resolved to the value of an attribute of a resource instance created from the respective resource definition of the template. The syntax of the get_attr function is as follows: :: - get_attr: [ , ] + get_attr: + - + - + - (optional) + - (optional) + - ... resource ID - This parameter specifies the resource the attribute of which shall be + This parameter specifies the resource for which the attributes shall be resolved. This resource must be defined within the *resources* section of the template (see also :ref:`hot_spec_resources`). attribute name - This parameter specifies the attribute to be resolved. + The attribute name is required as it specifies the attribute + to be resolved. If the attribute returns a complex data structure + such as a list or a map, then subsequent keys or indexes can be specified + which navigate the data structure to return the desired value. -An example of using the get_attr function is shown below: +Some examples of how to use the get_attr function are shown below: :: @@ -501,7 +509,18 @@ An example of using the get_attr function is shown below: instance_ip: description: IP address of the deployed compute instance value: { get_attr: [my_instance, first_address] } + instance_private_ip: + description: Private IP address of the deployed compute instance + value: { get_attr: [my_instance, networks, private, 0] } +In this example, if the networks attribute contained the following data: + +:: + + {"public": ["2001:0db8:0000:0000:0000:ff00:0042:8329", "1.2.3.4"], + "private": ["10.0.0.1"]} + +then the value of the get_attr function would resolve to "10.0.0.1". get_resource ------------ diff --git a/heat/engine/hot.py b/heat/engine/hot.py index db1b64d1a..b5a63cf77 100644 --- a/heat/engine/hot.py +++ b/heat/engine/hot.py @@ -165,14 +165,14 @@ class HOTemplate(template.Template): Resolve constructs of the form { get_attr: [my_resource, my_attr] } """ def match_get_attr(key, value): - return (key in ['get_attr', 'Fn::GetAtt'] and + return (key in ['get_attr'] and isinstance(value, list) and - len(value) == 2 and + len(value) >= 2 and None not in value and value[0] in resources) def handle_get_attr(args): - resource, att = args + resource = args[0] try: r = resources[resource] if r.state in ( @@ -182,10 +182,20 @@ class HOTemplate(template.Template): (r.RESUME, r.COMPLETE), (r.UPDATE, r.IN_PROGRESS), (r.UPDATE, r.COMPLETE)): - return r.FnGetAtt(att) - except KeyError: + rsrc_attr = args[1] + attr = r.FnGetAtt(rsrc_attr) + try: + for inner_attr in args[2:]: + if hasattr(attr, str(inner_attr)): + attr = getattr(attr, inner_attr) + else: + attr = attr[inner_attr] + return attr + except (KeyError, IndexError, TypeError): + return '' + except (KeyError, IndexError): raise exception.InvalidTemplateAttribute(resource=resource, - key=att) + key=rsrc_attr) return template._resolve(match_get_attr, handle_get_attr, s, transform) diff --git a/heat/tests/generic_resource.py b/heat/tests/generic_resource.py index 5ee5d777c..97f605f20 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -54,12 +54,51 @@ class GenericResource(resource.Resource): class ResourceWithProps(GenericResource): - properties_schema = {'Foo': {'Type': 'String'}} + properties_schema = {'Foo': {'Type': 'String'}} + + +class ResourceWithComplexAttributes(GenericResource): + attributes_schema = {'list': 'A list', + 'flat_dict': 'A flat dictionary', + 'nested_dict': 'A nested dictionary', + 'simple_object': 'An object', + 'complex_object': 'A really complex object', + 'none': 'A None' + } + + list = ['foo', 'bar'] + flat_dict = {'key1': 'val1', 'key2': 'val2', 'key3': 'val3'} + nested_dict = {'list': [1, 2, 3], + 'string': 'abc', + 'dict': {'a': 1, 'b': 2, 'c': 3}} + + class AnObject(object): + def __init__(self, first, second, third): + self.first = first + self.second = second + self.third = third + + simple_object = AnObject('a', 'b', 'c') + complex_object = AnObject('a', flat_dict, simple_object) + + def _resolve_attribute(self, name): + if name == 'list': + return self.list + if name == 'flat_dict': + return self.flat_dict + if name == 'nested_dict': + return self.nested_dict + if name == 'simple_object': + return self.simple_object + if name == 'complex_object': + return self.complex_object + if name == 'none': + return None class ResourceWithRequiredProps(GenericResource): - properties_schema = {'Foo': {'Type': 'String', - 'Required': True}} + properties_schema = {'Foo': {'Type': 'String', + 'Required': True}} class SignalResource(signal_responder.SignalResponder): diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index f0f6871d9..4849eef89 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -14,6 +14,7 @@ from heat.common import template_format from heat.common import exception from heat.engine import parser +from heat.engine import resource from heat.engine import hot from heat.engine import parameters from heat.engine import template @@ -22,12 +23,27 @@ from heat.engine import constraints from heat.tests.common import HeatTestCase from heat.tests import test_parser from heat.tests import utils +from heat.tests import generic_resource as generic_rsrc hot_tpl_empty = template_format.parse(''' heat_template_version: 2013-05-23 ''') +hot_tpl_generic_resource = template_format.parse(''' +heat_template_version: 2013-05-23 +resources: + resource1: + type: GenericResourceType +''') + +hot_tpl_complex_attrs = template_format.parse(''' +heat_template_version: 2013-05-23 +resources: + resource1: + type: ResourceWithComplexAttributesType +''') + class HOTemplateTest(HeatTestCase): """Test processing of HOT templates.""" @@ -195,16 +211,10 @@ class StackTest(test_parser.StackTest): """Test stack function when stack was created from HOT template.""" @utils.stack_delete_after - def test_get_attr(self): + def test_get_attr_multiple_rsrc_status(self): """Test resolution of get_attr occurrences in HOT template.""" - hot_tpl = template_format.parse(''' - heat_template_version: 2013-05-23 - resources: - resource1: - type: GenericResourceType - ''') - + hot_tpl = hot_tpl_generic_resource self.stack = parser.Stack(self.ctx, 'test_get_attr', template.Template(hot_tpl)) self.stack.store() @@ -227,27 +237,45 @@ class StackTest(test_parser.StackTest): # GenericResourceType has an attribute 'foo' which yields the # resource name. self.assertEqual({'Value': 'resource1'}, resolved) - # test invalid reference + + @utils.stack_delete_after + def test_get_attr_invalid(self): + """Test resolution of get_attr occurrences in HOT template.""" + + hot_tpl = hot_tpl_generic_resource + self.stack = parser.Stack(self.ctx, 'test_get_attr', + template.Template(hot_tpl)) + self.stack.store() + self.stack.create() + self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE), + self.stack.state) self.assertRaises(exception.InvalidTemplateAttribute, hot.HOTemplate.resolve_attributes, {'Value': {'get_attr': ['resource1', 'NotThere']}}, self.stack) - snippet = {'Value': {'Fn::GetAtt': ['resource1', 'foo']}} + @utils.stack_delete_after + def test_get_attr_invalid_resource(self): + """Test resolution of get_attr occurrences in HOT template.""" + + hot_tpl = hot_tpl_complex_attrs + self.stack = parser.Stack(self.ctx, + 'test_get_attr_invalid_none', + template.Template(hot_tpl)) + self.stack.store() + self.stack.create() + self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE), + self.stack.state) + + snippet = {'Value': {'get_attr': ['resource2', 'who_cares']}} resolved = hot.HOTemplate.resolve_attributes(snippet, self.stack) - self.assertEqual({'Value': 'resource1'}, resolved) + self.assertEqual(snippet, resolved) @utils.stack_delete_after def test_get_resource(self): """Test resolution of get_resource occurrences in HOT template.""" - hot_tpl = template_format.parse(''' - heat_template_version: 2013-05-23 - resources: - resource1: - type: GenericResourceType - ''') - + hot_tpl = hot_tpl_generic_resource self.stack = parser.Stack(self.ctx, 'test_get_resource', template.Template(hot_tpl)) self.stack.store() @@ -260,6 +288,131 @@ class StackTest(test_parser.StackTest): self.assertEqual({'value': 'resource1'}, resolved) +class StackAttributesTest(HeatTestCase): + """ + Test stack get_attr function when stack was created from HOT template. + """ + def setUp(self): + super(StackAttributesTest, self).setUp() + + utils.setup_dummy_db() + self.ctx = utils.dummy_context() + + resource._register_class('GenericResourceType', + generic_rsrc.GenericResource) + resource._register_class('ResourceWithComplexAttributesType', + generic_rsrc.ResourceWithComplexAttributes) + + self.m.ReplayAll() + + scenarios = [ + ('get_flat_attr', + dict(hot_tpl=hot_tpl_generic_resource, + snippet={'Value': {'get_attr': ['resource1', 'foo']}}, + resource_name='resource1', + expected={'Value': 'resource1'})), + ('get_list_attr', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', 'list', 0]}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes.list[0]})), + ('get_flat_dict_attr', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'flat_dict', + 'key2']}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes. + flat_dict['key2']})), + ('get_nested_attr_list', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'nested_dict', + 'list', + 0]}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes. + nested_dict['list'][0]})), + ('get_nested_attr_dict', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'nested_dict', + 'dict', + 'a']}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes. + nested_dict['dict']['a']})), + ('get_simple_object', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'simple_object', + 'first']}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes. + simple_object.first})), + ('get_complex_object', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'complex_object', + 'second', + 'key1']}}, + resource_name='resource1', + expected={ + 'Value': + generic_rsrc.ResourceWithComplexAttributes. + complex_object.second['key1']})), + ('get_complex_object_invalid_argument', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'complex_object', + 'not_there']}}, + resource_name='resource1', + expected={'Value': ''})), + ('get_attr_none', + dict(hot_tpl=hot_tpl_complex_attrs, + snippet={'Value': {'get_attr': ['resource1', + 'none', + 'who_cares']}}, + resource_name='resource1', + expected={'Value': ''})) + ] + + @utils.stack_delete_after + def test_get_attr(self): + """Test resolution of get_attr occurrences in HOT template.""" + + self.stack = parser.Stack(self.ctx, 'test_get_attr', + template.Template(self.hot_tpl)) + self.stack.store() + self.stack.create() + self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE), + self.stack.state) + + rsrc = self.stack[self.resource_name] + for action, status in ( + (rsrc.CREATE, rsrc.IN_PROGRESS), + (rsrc.CREATE, rsrc.COMPLETE), + (rsrc.RESUME, rsrc.IN_PROGRESS), + (rsrc.RESUME, rsrc.COMPLETE), + (rsrc.UPDATE, rsrc.IN_PROGRESS), + (rsrc.UPDATE, rsrc.COMPLETE)): + rsrc.state_set(action, status) + + resolved = hot.HOTemplate.resolve_attributes(self.snippet, + self.stack) + self.assertEqual(self.expected, resolved) + + class HOTParamValidatorTest(HeatTestCase): """Test HOTParamValidator""" diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index 39013d06b..b48480504 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -714,6 +714,8 @@ class StackTest(HeatTestCase): generic_rsrc.GenericResource) resource._register_class('ResourceWithPropsType', generic_rsrc.ResourceWithProps) + resource._register_class('ResourceWithComplexAttributesType', + generic_rsrc.ResourceWithComplexAttributes) self.m.ReplayAll()