+class Cfn(object):
+
+ base_template = {
+ 'HeatTemplateFormatVersion': '2012-12-12',
+ 'Description': []
+ }
+ get_resource = 'Ref'
+ get_param = 'Ref'
+ description = 'Description'
+ parameters = 'Parameters'
+ outputs = 'Outputs'
+ resources = 'Resources'
+ type = 'Type'
+ properties = 'Properties'
+ metadata = 'Metadata'
+ depends_on = 'DependsOn'
+ get_attr = 'Fn::GetAtt'
+
+
+class Hot(object):
+
+ base_template = {
+ 'heat_template_version': '2013-05-23',
+ 'description': []
+ }
+ get_resource = 'get_resource'
+ get_param = 'get_param'
+ description = 'description'
+ parameters = 'parameters'
+ outputs = 'outputs'
+ resources = 'resources'
+ type = 'type'
+ properties = 'properties'
+ metadata = 'metadata'
+ depends_on = 'depends_on'
+ get_attr = 'get_attr'
+
+
+lang = Cfn()
+
+
+def apply_maps(template):
+ """Apply Merge::Map within template.
+
+ Any dict {'Merge::Map': {'Foo': 'Bar', 'Baz': 'Quux'}}
+ will resolve to ['Bar', 'Quux'] - that is a dict with key
+ 'Merge::Map' is replaced entirely by that dict['Merge::Map'].values().
+ """
+ if isinstance(template, dict):
+ if 'Merge::Map' in template:
+ return sorted(
+ apply_maps(value) for value in template['Merge::Map'].values()
+ )
+ else:
+ return dict((key, apply_maps(value))
+ for key, value in template.items())
+ elif isinstance(template, list):
+ return [apply_maps(item) for item in template]
+ else:
+ return template
+
+
+def apply_scaling(template, scaling, in_copies=None):
+ """Apply a set of scaling operations to template.
+
+ This is a single pass recursive function: for each call we process one
+ dict or list and recurse to handle children containers.
+
+ Values are handled via scale_value.
+
+ Keys in dicts are copied per the scaling rule.
+ Values are either replaced or copied depending on whether the given
+ scaling rule is in in_copies.
+
+ in_copies is reset to None when a dict {'Merge::Map': someobject} is
+ encountered.
+
+ :param scaling: A dict of prefix -> (count, blacklists).
+ """
+ in_copies = dict(in_copies or {})
+ # Shouldn't be needed but to avoid unexpected side effects/bugs we short
+ # circuit no-ops.
+ if not scaling:
+ return template
+ if isinstance(template, dict):
+ if 'Merge::Map' in template:
+ in_copies = None
+ new_template = {}
+ for key, value in template.items():
+ for prefix, copy_num, new_key in scale_value(
+ key, scaling, in_copies):
+ if prefix:
+ # e.g. Compute0, 1, Compute1Foo
+ in_copies[prefix] = prefix[:-1] + str(copy_num)
+ if isinstance(value, (dict, list)):
+ new_value = apply_scaling(value, scaling, in_copies)
+ new_template[new_key] = new_value
+ else:
+ new_values = list(scale_value(value, scaling, in_copies))
+ # We have nowhere to multiply a non-container value of a
+ # dict, so it may be copied or unchanged but not scaled.
+ assert len(new_values) == 1
+ new_template[new_key] = new_values[0][2]
+ if prefix:
+ del in_copies[prefix]
+ return new_template
+ elif isinstance(template, list):
+ new_template = []
+ for value in template:
+ if isinstance(value, (dict, list)):
+ new_template.append(apply_scaling(value, scaling, in_copies))
+ else:
+ for _, _, new_value in scale_value(value, scaling, in_copies):
+ new_template.append(new_value)
+ return new_template
+ else:
+ raise Exception("apply_scaling called with non-container %r" % template)
+
+
+def scale_value(value, scaling, in_copies):
+ """Scale out a value.
+
+ :param value: The value to scale (not a container).
+ :param scaling: The scaling map (prefix-> (copies, blacklist) to use.
+ :param in_copies: What containers we're currently copying.
+ :return: An iterator of the new values for the value as tuples:
+ (prefix, copy_num, value). E.g. Compute0, 1, Compute1Foo
+ prefix and copy_num are only set when:
+ - a prefix in scaling matches value
+ - and that prefix is not in in_copies
+ """
+ if isinstance(value, (str, unicode)):
+ for prefix, (copies, blacklist) in scaling.items():
+ if not value.startswith(prefix):
+ continue
+ suffix = value[len(prefix):]
+ if prefix in in_copies:
+ # Adjust to the copy number we're on
+ yield None, None, in_copies[prefix] + suffix
+ return
+ else:
+ for n in range(copies):
+ if n not in blacklist:
+ yield prefix, n, prefix[:-1] + str(n) + suffix
+ return
+ yield None, None, value
+ else:
+ yield None, None, value
+
+
+def parse_scaling(scaling_args):
+ """Translate a list of scaling requests to a dict prefix:count."""
+ scaling_args = scaling_args or []
+ result = {}
+ for item in scaling_args:
+ key, values = item.split('=')
+ values = values.split(',')
+ value = int(values[0])
+ blacklist = frozenset(int(v) for v in values[1:] if v)
+ result[key + '0'] = value, blacklist
+ return result
+
+