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
This commit is contained in:
Pablo Andres Fuente 2014-01-17 14:15:40 -03:00
parent 5261b5beff
commit d246508752
5 changed files with 255 additions and 32 deletions

View File

@ -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: [ <resource ID>, <attribute name> ]
get_attr:
- <resource ID>
- <attribute name>
- <key/index 1> (optional)
- <key/index 2> (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
------------

View File

@ -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)

View File

@ -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):

View File

@ -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"""

View File

@ -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()