From 76281f4fa5d7741520b7dfb1baf199cd162c9bfc Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Fri, 23 Aug 2013 08:31:54 +1000 Subject: [PATCH] Implement Fn::MemberListToMap This is to allow the CloudWatch::Alarm to be implemented as a resource template. The Dimensions need to be converted from [{Name: bla, Value: foo}] into a normal dict. So we define the Dimensions as a CommaDelimitedList in the template, then in TemplateResource we see that the property is a list of dicts and convert it into the aws style memberlist '.member.0.Name=bla,.member.0.Value=green' then in the CW template we can do the following: matching_metadata: "Fn::MemberListToMap": [Name, Value, {"Fn:: Split": [",", {Ref: Dimensions}]}] Note: this in not a single case usage as we can use this for the Tags property that is in a lot of other resources. Change-Id: I68910c51eaeb0857531028e87b89b9192db4c8ba --- doc/source/template_guide/functions.rst | 28 +++++++++++++ heat/engine/parser.py | 1 + heat/engine/resources/template_resource.py | 10 ++++- heat/engine/template.py | 39 ++++++++++++++++++ heat/tests/test_parser.py | 47 ++++++++++++++++++++++ 5 files changed, 124 insertions(+), 1 deletion(-) diff --git a/doc/source/template_guide/functions.rst b/doc/source/template_guide/functions.rst index 48ae20a2..f5a69500 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 56a60b6b..73184505 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 240c2c21..c4433bb6 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 5106f18d..968e087b 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 d28f29ca..788888f5 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'} -- 2.45.2