Merge "Stop using notCompute in favor of controller"
[apex-tripleo-heat-templates.git] / tripleo_heat_merge / merge.py
index 053a683..4571d28 100644 (file)
@@ -4,6 +4,123 @@ import yaml
 import argparse
 
 
+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.
+    """
+    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 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 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):
+                    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, value = item.split('=')
+        value = int(value)
+        result[key + '0'] = value
+    return result
+
+
 def _translate_role(role, master_role, slave_roles):
     if not master_role:
         return role
@@ -89,14 +206,36 @@ def main(argv=None):
     parser.add_argument('--included-template-dir', nargs='?',
                         default=INCLUDED_TEMPLATE_DIR,
                         help='Path for resolving included templates')
+    parser.add_argument('--output',
+                        help='File to write output to. - for stdout',
+                        default='-')
+    parser.add_argument('--scale', action="append",
+        help="Names to scale out. Pass Prefix=1 to cause a key Prefix0Foo to "
+        "be copied to Prefix1Foo in the output, and value Prefix0Bar to be"
+        "renamed to Prefix1Bar inside that copy, or copied to Prefix1Bar "
+        "outside of any copy.")
+    parser.add_argument(
+        '--change-image-params', action='store_true', default=False,
+        help="Change parameters in templates to match resource names. This was "
+             " the default at one time but it causes issues when parameter "
+             " names need to remain stable.")
     args = parser.parse_args(argv)
     templates = args.templates
+    scaling = parse_scaling(args.scale)
     merged_template = merge(templates, args.master_role, args.slave_roles,
-                            args.included_template_dir)
-    sys.stdout.write(merged_template)
+                            args.included_template_dir, scaling=scaling,
+                            change_image_params=args.change_image_params)
+    if args.output == '-':
+        out_file = sys.stdout
+    else:
+        out_file = file(args.output, 'wt')
+    out_file.write(merged_template)
+
 
 def merge(templates, master_role=None, slave_roles=None,
-          included_template_dir=INCLUDED_TEMPLATE_DIR):
+          included_template_dir=INCLUDED_TEMPLATE_DIR,
+          scaling=None, change_image_params=None):
+    scaling = scaling or {}
     errors = []
     end_template={'HeatTemplateFormatVersion': '2012-12-12',
                   'Description': []}
@@ -132,11 +271,12 @@ def merge(templates, master_role=None, slave_roles=None,
         new_resources = template.get('Resources', {})
         for r, rbody in sorted(new_resources.items()):
             if rbody['Type'] in MERGABLE_TYPES:
-                if 'image' in MERGABLE_TYPES[rbody['Type']]:
-                    image_key = MERGABLE_TYPES[rbody['Type']]['image']
-                    # XXX Assuming ImageId is always a Ref
-                    ikey_val = end_template['Parameters'][rbody['Properties'][image_key]['Ref']]
-                    del end_template['Parameters'][rbody['Properties'][image_key]['Ref']]
+                if change_image_params:
+                    if 'image' in MERGABLE_TYPES[rbody['Type']]:
+                        image_key = MERGABLE_TYPES[rbody['Type']]['image']
+                        # XXX Assuming ImageId is always a Ref
+                        ikey_val = end_template['Parameters'][rbody['Properties'][image_key]['Ref']]
+                        del end_template['Parameters'][rbody['Properties'][image_key]['Ref']]
                 role = rbody.get('Metadata', {}).get('OpenStack::Role', r)
                 role = translate_role(role, master_role, slave_roles)
                 if role != r:
@@ -157,10 +297,11 @@ def merge(templates, master_role=None, slave_roles=None,
                 if 'Resources' not in end_template:
                     end_template['Resources'] = {}
                 end_template['Resources'][role] = rbody
-                if 'image' in MERGABLE_TYPES[rbody['Type']]:
-                    ikey = '%sImage' % (role)
-                    end_template['Resources'][role]['Properties'][image_key] = {'Ref': ikey}
-                    end_template['Parameters'][ikey] = ikey_val
+                if change_image_params:
+                    if 'image' in MERGABLE_TYPES[rbody['Type']]:
+                        ikey = '%sImage' % (role)
+                        end_template['Resources'][role]['Properties'][image_key] = {'Ref': ikey}
+                        end_template['Parameters'][ikey] = ikey_val
             elif rbody['Type'] == 'FileInclude':
                 # we trust os.path.join to DTRT: if FileInclude path isn't
                 # absolute, join to included_template_dir (./)
@@ -185,6 +326,9 @@ def merge(templates, master_role=None, slave_roles=None,
                     end_template['Resources'] = {}
                 end_template['Resources'][r] = rbody
 
+    end_template = apply_scaling(end_template, scaling)
+    end_template = apply_maps(end_template)
+
     def fix_ref(item, old, new):
         if isinstance(item, dict):
             copy_item = dict(item)