diff --git a/heat/engine/hot/functions.py b/heat/engine/hot/functions.py index f38973d0df..3852860f90 100644 --- a/heat/engine/hot/functions.py +++ b/heat/engine/hot/functions.py @@ -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): - - - ... - ''' + """ 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: + - + - + - + - ... + + where and , ... are optional arguments. If there + is no , 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. diff --git a/heat/engine/hot/template.py b/heat/engine/hot/template.py index 968c91144c..38ff120b9b 100644 --- a/heat/engine/hot/template.py +++ b/heat/engine/hot/template.py @@ -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, diff --git a/heat/engine/resource.py b/heat/engine/resource.py index 1b46e06b62..c530d66284 100644 --- a/heat/engine/resource.py +++ b/heat/engine/resource.py @@ -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. diff --git a/heat/engine/stack.py b/heat/engine/stack.py index 8101b493ed..3196bc1164 100644 --- a/heat/engine/stack.py +++ b/heat/engine/stack.py @@ -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. diff --git a/heat/tests/test_hot.py b/heat/tests/test_hot.py index ed97c7feb0..b5f17d55fb 100644 --- a/heat/tests/test_hot.py +++ b/heat/tests/test_hot.py @@ -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)) diff --git a/heat/tests/test_resource.py b/heat/tests/test_resource.py index e624eb03d9..67d84538ae 100644 --- a/heat/tests/test_resource.py +++ b/heat/tests/test_resource.py @@ -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) diff --git a/heat/tests/test_stack_collect_attributes.py b/heat/tests/test_stack_collect_attributes.py index fe5e52b2d6..b10069d12b 100644 --- a/heat/tests/test_stack_collect_attributes.py +++ b/heat/tests/test_stack_collect_attributes.py @@ -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):