diff options
Diffstat (limited to 'tripleo_heat_merge/merge.py')
-rw-r--r-- | tripleo_heat_merge/merge.py | 141 |
1 files changed, 129 insertions, 12 deletions
diff --git a/tripleo_heat_merge/merge.py b/tripleo_heat_merge/merge.py index 053a683a..dd254ab4 100644 --- a/tripleo_heat_merge/merge.py +++ b/tripleo_heat_merge/merge.py @@ -4,6 +4,97 @@ import yaml import argparse +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 always copied per the scaling rule. + Values are either replaced or copied depending on whether the given + scaling rule is in in_copies. + """ + 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): + 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 +180,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 +245,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 +271,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 +300,8 @@ def merge(templates, master_role=None, slave_roles=None, end_template['Resources'] = {} end_template['Resources'][r] = rbody + end_template = apply_scaling(end_template, scaling) + def fix_ref(item, old, new): if isinstance(item, dict): copy_item = dict(item) |