diff --git a/doc/source/template_guide/functions.rst b/doc/source/template_guide/functions.rst index 48ae20a265..f5a695008c 100644 --- a/doc/source/template_guide/functions.rst +++ b/doc/source/template_guide/functions.rst @@ -294,3 +294,31 @@ To use it What happened is the metadata in ``top.yaml`` (key: value, some: more stuff) gets passed into the resource template via the `Fn::ResourceFacade`_ function. + +------------------- +Fn::MemberListToMap +------------------- +Convert an AWS style member list into a map. + +Parameters +~~~~~~~~~~ +key name: string + The name of the key (normally "Name" or "Key") + +value name: string + The name of the value (normally "Value") + +list: A list of strings + The string to convert. + +Usage +~~~~~ +:: + + {'Fn::MemberListToMap': ['Name', 'Value', ['.member.0.Name=key', + '.member.0.Value=door', + '.member.1.Name=colour', + '.member.1.Value=green']]} + + returns + {'key': 'door', 'colour': 'green'} diff --git a/heat/engine/parser.py b/heat/engine/parser.py index 56a60b6b0e..731845050e 100644 --- a/heat/engine/parser.py +++ b/heat/engine/parser.py @@ -574,6 +574,7 @@ def resolve_runtime_data(template, resources, snippet): functools.partial(template.resolve_attributes, resources=resources), template.resolve_split, + template.resolve_member_list_to_map, template.resolve_select, template.resolve_joins, template.resolve_replace, diff --git a/heat/engine/resources/template_resource.py b/heat/engine/resources/template_resource.py index 240c2c2125..c4433bb600 100644 --- a/heat/engine/resources/template_resource.py +++ b/heat/engine/resources/template_resource.py @@ -78,7 +78,15 @@ class TemplateResource(stack_resource.StackResource): if val is not None: # take a list and create a CommaDelimitedList if v.type() == properties.LIST: - val = ','.join(val) + if isinstance(val[0], dict): + flattened = [] + for (i, item) in enumerate(val): + for (k, v) in iter(item.items()): + mem_str = '.member.%d.%s=%s' % (i, k, v) + flattened.append(mem_str) + params[n] = ','.join(flattened) + else: + val = ','.join(val) # for MAP, the JSON param takes either a collection or string, # so just pass it on and let the param validate as appropriate diff --git a/heat/engine/template.py b/heat/engine/template.py index 5106f18d09..968e087ba1 100644 --- a/heat/engine/template.py +++ b/heat/engine/template.py @@ -16,6 +16,7 @@ import collections import json +from heat.api.aws import utils as aws_utils from heat.db import api as db_api from heat.common import exception from heat.engine.parameters import ParamSchema @@ -361,6 +362,44 @@ class Template(collections.Mapping): return _resolve(lambda k, v: k == 'Fn::Base64', handle_base64, s) + @staticmethod + def resolve_member_list_to_map(s): + ''' + Resolve constructs of the form + {'Fn::MemberListToMap': ['Name', 'Value', ['.member.0.Name=key', + '.member.0.Value=door']]} + the first two arguments are the names of the key and value. + ''' + + def handle_member_list_to_map(args): + correct = ''' + {'Fn::MemberListToMap': ['Name', 'Value', + ['.member.0.Name=key', + '.member.0.Value=door']]} + ''' + if not isinstance(args, (list, tuple)): + raise TypeError('Wrong Arguments try: "%s"' % correct) + if len(args) != 3: + raise TypeError('Wrong Arguments try: "%s"' % correct) + if not isinstance(args[0], basestring): + raise TypeError('Wrong Arguments try: "%s"' % correct) + if not isinstance(args[1], basestring): + raise TypeError('Wrong Arguments try: "%s"' % correct) + if not isinstance(args[2], (list, tuple)): + raise TypeError('Wrong Arguments try: "%s"' % correct) + + partial = {} + for item in args[2]: + sp = item.split('=') + partial[sp[0]] = sp[1] + return aws_utils.extract_param_pairs(partial, + prefix='', + keyname=args[0], + valuename=args[1]) + + return _resolve(lambda k, v: k == 'Fn::MemberListToMap', + handle_member_list_to_map, s) + @staticmethod def resolve_resource_facade(s, stack): ''' diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index d28f29caa0..788888f53c 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -461,6 +461,53 @@ Mappings: parser.Template.resolve_replace(snippet), '"foo" is "${var3}"') + def test_member_list2map_good(self): + snippet = {"Fn::MemberListToMap": [ + 'Name', 'Value', ['.member.0.Name=metric', + '.member.0.Value=cpu', + '.member.1.Name=size', + '.member.1.Value=56']]} + self.assertEqual( + {'metric': 'cpu', 'size': '56'}, + parser.Template.resolve_member_list_to_map(snippet)) + + def test_member_list2map_good2(self): + snippet = {"Fn::MemberListToMap": [ + 'Key', 'Value', ['.member.2.Key=metric', + '.member.2.Value=cpu', + '.member.5.Key=size', + '.member.5.Value=56']]} + self.assertEqual( + {'metric': 'cpu', 'size': '56'}, + parser.Template.resolve_member_list_to_map(snippet)) + + def test_member_list2map_no_key_or_val(self): + snippet = {"Fn::MemberListToMap": [ + 'Key', ['.member.2.Key=metric', + '.member.2.Value=cpu', + '.member.5.Key=size', + '.member.5.Value=56']]} + self.assertRaises(TypeError, + parser.Template.resolve_member_list_to_map, + snippet) + + def test_member_list2map_no_list(self): + snippet = {"Fn::MemberListToMap": [ + 'Key', '.member.2.Key=metric']} + self.assertRaises(TypeError, + parser.Template.resolve_member_list_to_map, + snippet) + + def test_member_list2map_not_string(self): + snippet = {"Fn::MemberListToMap": [ + 'Name', ['Value'], ['.member.0.Name=metric', + '.member.0.Value=cpu', + '.member.1.Name=size', + '.member.1.Value=56']]} + self.assertRaises(TypeError, + parser.Template.resolve_member_list_to_map, + snippet) + def test_resource_facade(self): metadata_snippet = {'Fn::ResourceFacade': 'Metadata'} deletion_policy_snippet = {'Fn::ResourceFacade': 'DeletionPolicy'}