Add get_attr which returns all attributes

Add get_attr functionality which returns dict
of all resource's attributes.

New class expands existing get_attr functionality,
so there's no need to add new syntax for it. Also,
this functionality should be supported since template
version 2015-10-15.

implements bp get-attr-all-attributes

Change-Id: I01e5690dae6dd9af72f316994c5ec90803844889
This commit is contained in:
Peter Razumovsky 2015-07-08 11:32:49 +03:00
parent 0c460f88df
commit ed56c1bd36
7 changed files with 299 additions and 5 deletions

View File

@ -140,8 +140,7 @@ class GetAttThenSelect(cfn_funcs.GetAtt):
class GetAtt(GetAttThenSelect):
'''
A function for resolving resource attributes.
"""A function for resolving resource attributes.
Takes the form::
@ -150,7 +149,7 @@ class GetAtt(GetAttThenSelect):
- <attribute_name>
- <path1>
- ...
'''
"""
def result(self):
path_components = function.resolve(self._path_components)
@ -165,6 +164,62 @@ class GetAtt(GetAttThenSelect):
return None
class GetAttAllAttributes(GetAtt):
"""A function for resolving resource attributes.
Takes the form::
get_attr:
- <resource_name>
- <attributes_name>
- <path1>
- ...
where <attributes_name> and <path1>, ... are optional arguments. If there
is no <attributes_name>, result will be dict of all resource's attributes.
Else function returns resolved resource's attribute.
"""
def _parse_args(self):
if not self.args:
raise ValueError(_('Arguments to "%s" can be of the next '
'forms: [resource_name] or '
'[resource_name, attribute, (path), ...]'
) % self.fn_name)
elif isinstance(self.args, collections.Sequence):
if len(self.args) > 1:
return super(GetAttAllAttributes, self)._parse_args()
else:
return self.args[0], None
else:
raise TypeError(_('Argument to "%s" must be a list') %
self.fn_name)
def dep_attrs(self, resource_name):
"""Check if there is no attribute_name defined, return empty chain."""
if self._attribute is not None:
return super(GetAttAllAttributes, self).dep_attrs(resource_name)
elif self._resource().name == resource_name:
res = self._resource()
attrs = six.iterkeys(res.attributes_schema)
else:
attrs = []
return itertools.chain(function.dep_attrs(self.args,
resource_name), attrs)
def result(self):
if self._attribute is None:
r = self._resource()
if (r.status in (r.IN_PROGRESS, r.COMPLETE) and
r.action in (r.CREATE, r.ADOPT, r.SUSPEND, r.RESUME,
r.UPDATE, r.CHECK, r.SNAPSHOT)):
return r.FnGetAtts()
else:
return None
else:
return super(GetAttAllAttributes, self).result()
class Replace(cfn_funcs.Replace):
'''
A function for performing string substitutions.

View File

@ -330,7 +330,7 @@ class HOTemplate20150430(HOTemplate20141016):
class HOTemplate20151015(HOTemplate20150430):
functions = {
'digest': hot_funcs.Digest,
'get_attr': hot_funcs.GetAtt,
'get_attr': hot_funcs.GetAttAllAttributes,
'get_file': hot_funcs.GetFile,
'get_param': hot_funcs.GetParam,
'get_resource': cfn_funcs.ResourceRef,

View File

@ -1491,6 +1491,19 @@ class Resource(object):
return attributes.select_from_attribute(attribute, path)
def FnGetAtts(self):
"""For the intrinsic function get_attr which returns all attributes.
:returns: dict of all resource's attributes exclude "show" attribute.
"""
if self.stack.has_cache_data(self.name):
attrs = self.stack.cache_data_resource_all_attributes(self.name)
else:
attrs = dict((k, v) for k, v in six.iteritems(self.attributes))
attrs = dict((k, v) for k, v in six.iteritems(attrs)
if k != self.SHOW)
return attrs
def FnBase64(self, data):
'''
For the instrinsic function Fn::Base64.

View File

@ -1635,6 +1635,10 @@ class Stack(collections.Mapping):
return self.cache_data.get(
resource_name, {}).get('attrs', {}).get(attribute_key)
def cache_data_resource_all_attributes(self, resource_name):
attrs = self.cache_data.get(resource_name, {}).get('attributes', {})
return attrs
def mark_complete(self, traversal_id):
'''
Mark the update as complete.

View File

@ -67,6 +67,20 @@ resources:
type: GenericResourceType
''')
hot_tpl_generic_resource_all_attrs = template_format.parse('''
heat_template_version: 2015-10-15
resources:
resource1:
type: GenericResourceType
''')
hot_tpl_complex_attrs_all_attrs = template_format.parse('''
heat_template_version: 2015-10-15
resources:
resource1:
type: ResourceWithComplexAttributesType
''')
hot_tpl_complex_attrs = template_format.parse('''
heat_template_version: 2013-05-23
resources:
@ -87,6 +101,19 @@ resources:
a_map: { get_attr: [ resource1, map] }
''')
hot_tpl_mapped_props_all_attrs = template_format.parse('''
heat_template_version: 2015-10-15
resources:
resource1:
type: ResWithComplexPropsAndAttrs
resource2:
type: ResWithComplexPropsAndAttrs
properties:
a_list: { get_attr: [ resource1, list] }
a_string: { get_attr: [ resource1, string ] }
a_map: { get_attr: [ resource1, map] }
''')
class DummyClass(object):
metadata = None
@ -1323,6 +1350,23 @@ class StackGetAttrValidationTest(common.HeatTestCase):
self.assertEqual('',
stack.resources['resource2'].properties['a_string'])
def test_validate_props_from_attrs_all_attrs(self):
stack = parser.Stack(self.ctx, 'test_props_from_attrs',
template.Template(hot_tpl_mapped_props_all_attrs))
stack.resources['resource1'].list = None
stack.resources['resource1'].map = None
stack.resources['resource1'].string = None
try:
stack.validate()
except exception.StackValidationFailed as exc:
self.fail("Validation should have passed: %s" % six.text_type(exc))
self.assertEqual([],
stack.resources['resource2'].properties['a_list'])
self.assertEqual({},
stack.resources['resource2'].properties['a_map'])
self.assertEqual('',
stack.resources['resource2'].properties['a_string'])
class StackParametersTest(common.HeatTestCase):
"""
@ -1824,3 +1868,107 @@ class HOTParamValidatorTest(common.HeatTestCase):
"stack_testit", template.Template(hot_tpl))
self.assertEqual(
"AllowedPattern must be a string", six.text_type(error))
class TestGetAttAllAttributes(common.HeatTestCase):
scenarios = [
('test_get_attr_all_attributes', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': ['resource1']}},
expected={'Value': {'Foo': 'resource1', 'foo': 'resource1'}},
raises=None
)),
('test_get_attr_all_attributes_str', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': 'resource1'}},
expected='Argument to "get_attr" must be a list',
raises=TypeError
)),
('test_get_attr_all_attributes_invalid_resource_list', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': ['resource2']}},
raises=exception.InvalidTemplateReference,
expected='The specified reference "resource2" '
'(in unknown) is incorrect.'
)),
('test_get_attr_all_attributes_invalid_type', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': {'resource1': 'attr1'}}},
raises=TypeError,
expected='Argument to "get_attr" must be a list'
)),
('test_get_attr_all_attributes_invalid_arg_str', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': ''}},
raises=ValueError,
expected='Arguments to "get_attr" can be of the next '
'forms: [resource_name] or '
'[resource_name, attribute, (path), ...]'
)),
('test_get_attr_all_attributes_invalid_arg_list', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': []}},
raises=ValueError,
expected='Arguments to "get_attr" can be of the next '
'forms: [resource_name] or '
'[resource_name, attribute, (path), ...]'
)),
('test_get_attr_all_attributes_standard', dict(
hot_tpl=hot_tpl_generic_resource_all_attrs,
snippet={'Value': {'get_attr': ['resource1', 'foo']}},
expected={'Value': 'resource1'},
raises=None
)),
('test_get_attr_all_attrs_complex_attrs', dict(
hot_tpl=hot_tpl_complex_attrs_all_attrs,
snippet={'Value': {'get_attr': ['resource1']}},
expected={'Value': {'flat_dict': {'key1': 'val1',
'key2': 'val2',
'key3': 'val3'},
'list': ['foo', 'bar'],
'nested_dict': {'dict': {'a': 1,
'b': 2,
'c': 3},
'list': [1, 2, 3],
'string': 'abc'},
'none': None}},
raises=None
)),
('test_get_attr_all_attrs_complex_attrs_standard', dict(
hot_tpl=hot_tpl_complex_attrs_all_attrs,
snippet={'Value': {'get_attr': ['resource1', 'list', 1]}},
expected={'Value': 'bar'},
raises=None
)),
]
@staticmethod
def resolve(snippet, template, stack=None):
return function.resolve(template.parse(stack, snippet))
def test_get_attr_all_attributes(self):
tmpl = template.Template(self.hot_tpl)
stack = parser.Stack(utils.dummy_context(), 'test_get_attr', tmpl)
stack.store()
stack.create()
self.assertEqual((parser.Stack.CREATE, parser.Stack.COMPLETE),
stack.state)
rsrc = stack['resource1']
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)
if self.raises is not None:
ex = self.assertRaises(self.raises,
self.resolve, self.snippet, tmpl, stack)
self.assertEqual(self.expected, six.text_type(ex))
else:
self.assertEqual(self.expected,
self.resolve(self.snippet, tmpl, stack))

View File

@ -1406,6 +1406,42 @@ class ResourceTest(common.HeatTestCase):
res.FnGetAtt('attr2')
self.assertIn("Attribute attr2 is not of type Map", self.LOG.output)
def test_getatts(self):
tmpl = template.Template({
'heat_template_version': '2013-05-23',
'resources': {
'res': {
'type': 'ResourceWithComplexAttributesType'
}
}
})
stack = parser.Stack(utils.dummy_context(), 'test', tmpl)
res = stack['res']
self.assertEqual({'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}},
'none': None}, res.FnGetAtts())
def test_getatts_with_cache_data(self):
tmpl = template.Template({
'heat_template_version': '2013-05-23',
'resources': {
'res': {
'type': 'ResourceWithPropsType'
}
}
})
stack = parser.Stack(utils.dummy_context(), 'test', tmpl,
cache_data={
'res': {'attributes': {'Foo': 'res',
'foo': 'res'}}})
res = stack['res']
self.assertEqual({'foo': 'res', 'Foo': 'res'}, res.FnGetAtts())
def test_properties_data_stored_encrypted_decrypted_on_load(self):
cfg.CONF.set_override('encrypt_parameters_and_properties', True)

View File

@ -147,6 +147,37 @@ outputs:
{get_attr: [BResource, attr_B3]}]
"""
tmpl7 = """
heat_template_version: 2015-10-15
resources:
AResource:
type: ResourceWithPropsType
properties:
Foo: 'abc'
BResource:
type: ResourceWithPropsType
properties:
Foo: {get_attr: [AResource, attr_A1]}
Doo: {get_attr: [AResource, attr_A2]}
metadata:
first: {get_attr: [AResource, meta_A1]}
CResource:
type: ResourceWithPropsType
properties:
Foo: {get_attr: [AResource, attr_A1]}
Doo: {get_attr: [BResource, attr_B2]}
metadata:
Doo: {get_attr: [BResource, attr_B1]}
first: {get_attr: [AResource, meta_A1]}
second: {get_attr: [BResource, meta_B2]}
outputs:
out1:
value: [{get_attr: [AResource, attr_A3]},
{get_attr: [AResource, attr_A4]},
{get_attr: [BResource, attr_B3]},
{get_attr: [CResource]}]
"""
class DepAttrsTest(common.HeatTestCase):
@ -183,7 +214,14 @@ class DepAttrsTest(common.HeatTestCase):
(u'list', 1),
(u'nested_dict', u'dict', u'b'),
(u'nested_dict', u'string')]),
'BResource': set(['attr_B3'])}))
'BResource': set(['attr_B3'])})),
('several_res_several_attrs_and_all_attrs',
dict(tmpl=tmpl7,
expected={'AResource': {'attr_A1', 'attr_A2', 'meta_A1',
'attr_A3', 'attr_A4'},
'BResource': {'attr_B1', 'attr_B2', 'meta_B2',
'attr_B3'},
'CResource': {'foo', 'Foo', 'show'}}))
]
def setUp(self):