diff --git a/action/config_template.py b/action/config_template.py index 63cd217..e5ab7b8 100644 --- a/action/config_template.py +++ b/action/config_template.py @@ -22,20 +22,23 @@ try: except ImportError: import configparser as ConfigParser import datetime + try: from StringIO import StringIO except ImportError: from io import StringIO + import base64 import json import os import pwd import re -import six import time import yaml import tempfile as tmpfilelib +from collections import OrderedDict + from ansible.plugins.action import ActionBase from ansible.module_utils._text import to_bytes, to_text from ansible.module_utils.parsing.convert_bool import boolean @@ -53,13 +56,15 @@ CONFIG_TYPES = { 'yaml': 'return_config_overrides_yaml' } +STRIP_MARKER = '__MARKER__' + class IDumper(AnsibleDumper): def increase_indent(self, flow=False, indentless=False): return super(IDumper, self).increase_indent(flow, False) -class MultiKeyDict(dict): +class MultiKeyDict(OrderedDict): """Dictionary class which supports duplicate keys. This class allows for an item to be added into a standard python dictionary however if a key is created more than once the dictionary will convert the @@ -77,6 +82,32 @@ class MultiKeyDict(dict): ... {'a': tuple(['1', '2']), 'c': {'a': 1}, 'b': ['a', 'b', 'c']} """ + def index(self, key): + index_search = [ + i for i, item in enumerate(self) if item.startswith(key) + ] + if len(index_search) > 1: + raise SystemError('Index search returned more than one value') + return index_search[0] + + def insert(self, index, key, value): + list(self)[index] # Validates the index + shadow = MultiKeyDict() + counter = 0 + for k, v in self.items(): + if counter == index: + shadow[k] = v + shadow[key] = value + else: + shadow[k] = v + counter += 1 + else: + return shadow + + def update(self, E=None, **kwargs): + for key, value in E.items(): + super(MultiKeyDict, self).__setitem__(key, value) + def __setitem__(self, key, value): if key in self: if isinstance(self[key], tuple): @@ -89,7 +120,7 @@ class MultiKeyDict(dict): items = tuple([str(self[key]), str(value)]) super(MultiKeyDict, self).__setitem__(key, items) else: - return dict.__setitem__(self, key, value) + return super(MultiKeyDict, self).__setitem__(key, value) class ConfigTemplateParser(ConfigParser.RawConfigParser): @@ -141,162 +172,144 @@ class ConfigTemplateParser(ConfigParser.RawConfigParser): """ def __init__(self, *args, **kwargs): - self._comments = {} self.ignore_none_type = bool(kwargs.pop('ignore_none_type', True)) self.default_section = str(kwargs.pop('default_section', 'DEFAULT')) self.yml_multilines = bool(kwargs.pop('yml_multilines', False)) + self._comment_prefixes = kwargs.pop('comment_prefixes', '/') + self._empty_lines_in_values = kwargs.get('allow_no_value', True) + self._strict = kwargs.get('strict', False) + self._allow_no_value = self._empty_lines_in_values ConfigParser.RawConfigParser.__init__(self, *args, **kwargs) + def set(self, section, option, value=None): + if not section or section == 'DEFAULT': + sectdict = self._defaults + use_defaults = True + else: + try: + sectdict = self._sections[section] + except KeyError: + raise SystemError('Section %s not found' % section) + else: + use_defaults = False + + option = self.optionxform(option) + try: + index = sectdict.index('#%s' % option) + except (ValueError, IndexError): + sectdict[option] = value + else: + if use_defaults: + self._defaults = sectdict.insert(index, option, value) + else: + self._sections[section] = sectdict.insert( + index, + option, + value + ) + def _write(self, fp, section, key, item, entry): if section: # If we are not ignoring a none type value, then print out # the option name only if the value type is None. if not self.ignore_none_type and item is None: fp.write(key + '\n') - elif (item is not None) or (self._optcre == self.OPTCRE): - fp.write(entry) - else: - fp.write(entry) + return + + fp.write(entry) def _write_check(self, fp, key, value, section=False): - if isinstance(value, (tuple, set)): - for item in value: - item = str(item).replace('\n', '\n\t') - entry = "%s = %s\n" % (key, item) - self._write(fp, section, key, item, entry) - else: - if isinstance(value, list): - _value = [str(i.replace('\n', '\n\t')) for i in value] - entry = '%s = %s\n' % (key, ','.join(_value)) + def _return_entry(option, item): + if item: + return "%s = %s\n" % (option, str(item).replace('\n', '\n\t')) else: - entry = '%s = %s\n' % (key, str(value).replace('\n', '\n\t')) + return "%s\n" % option + + key = key.split(STRIP_MARKER)[0] + if isinstance(value, (tuple, set)): + for i in sorted(value): + entry = _return_entry(option=key, item=i) + self._write(fp, section, key, i, entry) + elif isinstance(value, list): + _value = [str(i.replace('\n', '\n\t')) for i in value] + entry = '%s = %s\n' % (key, ','.join(_value)) + self._write(fp, section, key, value, entry) + else: + entry = _return_entry(option=key, item=value) self._write(fp, section, key, value, entry) - def write(self, fp): + def write(self, fp, **kwargs): def _do_write(section_name, section, section_bool=False): - _write_comments(section_name) fp.write("[%s]\n" % section_name) - for key, value in sorted(section.items()): - _write_comments(section_name, optname=key) - self._write_check(fp, key=key, value=value, - section=section_bool) + for key, value in section.items(): + self._write_check( + fp, + key=key, + value=value, + section=section_bool + ) else: fp.write("\n") - def _write_comments(section, optname=None): - comsect = self._comments.get(section, {}) - if optname in comsect: - fp.write(''.join(comsect[optname])) - - if self.default_section != 'DEFAULT' and self._sections.get( - self.default_section, False): - _do_write(self.default_section, - self._sections[self.default_section], - section_bool=True) - self._sections.pop(self.default_section) - - if self._defaults: + if self.default_section != 'DEFAULT': + if not self._sections.get(self.default_section, False): + _do_write( + section_name=self.default_section, + section=self._sections[self.default_section], + section_bool=True + ) + elif self._defaults: _do_write('DEFAULT', self._defaults) - for section in sorted(self._sections): - _do_write(section, self._sections[section], section_bool=True) + for i in self._sections: + _do_write(i, self._sections[i], section_bool=True) def _read(self, fp, fpname): - comments = [] - cursect = None + def _temp_set(): + _temp_item = [cursect[optname]] + cursect.update({optname: _temp_item}) + optname = None - lineno = 0 - e = None - while True: - line = fp.readline() - if not line: - break - lineno += 1 - if line.strip() == '': - if comments: - comments.append('') - continue - - if line.lstrip()[0] in '#;': - comments.append(line.lstrip()) - continue - - if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR": - continue - if line[0].isspace() and cursect is not None and optname: - value = line.strip() - if value: - try: - if isinstance(cursect[optname], (tuple, set)): - _temp_item = list(cursect[optname]) - del cursect[optname] - cursect[optname] = _temp_item - elif isinstance(cursect[optname], six.text_type): - _temp_item = [cursect[optname]] - del cursect[optname] - cursect[optname] = _temp_item - except NameError: - if isinstance(cursect[optname], (bytes, str)): - _temp_item = [cursect[optname]] - del cursect[optname] - cursect[optname] = _temp_item - cursect[optname].append(value) - else: - mo = self.SECTCRE.match(line) - if mo: - sectname = mo.group('header') - if sectname in self._sections: - cursect = self._sections[sectname] - elif sectname == 'DEFAULT': - cursect = self._defaults - else: - cursect = self._dict() - self._sections[sectname] = cursect - optname = None - - comsect = self._comments.setdefault(sectname, {}) - if comments: - # NOTE(flaper87): Using none as the key for - # section level comments - comsect[None] = comments - comments = [] - elif cursect is None: - raise ConfigParser.MissingSectionHeaderError( - fpname, - lineno, - line - ) + cursect = {} + marker_counter = 0 + for lineno, line in enumerate(fp, start=0): + marker_counter += 1 + mo_match = self.SECTCRE.match(line) + mo_optcre = self._optcre.match(line) + if mo_match: + sectname = mo_match.group('header') + if sectname in self._sections: + cursect = self._sections[sectname] + elif sectname == 'DEFAULT': + cursect = self._defaults else: - mo = self._optcre.match(line) - if mo: - optname, vi, optval = mo.group('option', 'vi', 'value') - optname = self.optionxform(optname.rstrip()) - if optval is not None: - if vi in ('=', ':') and ';' in optval: - pos = optval.find(';') - if pos != -1 and optval[pos - 1].isspace(): - optval = optval[:pos] - optval = optval.strip() - if optval == '""': - optval = '' - cursect[optname] = optval - if comments: - comsect[optname] = comments - comments = [] - else: - if not e: - e = ConfigParser.ParsingError(fpname) - e.append(lineno, repr(line)) - if e: - raise e - all_sections = [self._defaults] - all_sections.extend(self._sections.values()) - for options in all_sections: - for name, val in options.items(): - if isinstance(val, list): - _temp_item = '\n'.join(val) - del options[name] - options[name] = _temp_item + cursect = self._dict() + self._sections[sectname] = cursect + elif mo_optcre: + optname, vi, optval = mo_optcre.group('option', 'vi', 'value') + optname = self.optionxform(optname.rstrip()) + if optname and not optname.startswith('#') and optval: + if vi in ('=', ':') and ';' in optval: + pos = optval.find(';') + if pos != -1 and optval[pos - 1].isspace(): + optval = optval[:pos] + optval = optval.strip() + if optval == '""': + optval = '' + else: + optname = '%s%s-%d' % ( + optname, + STRIP_MARKER, + marker_counter + ) + cursect[optname] = optval + else: + + optname = '%s-%d' % ( + STRIP_MARKER, + marker_counter + ) + cursect[optname] = None class DictCompare(object): @@ -322,6 +335,7 @@ class DictCompare(object): ... {'test1': {'current_val': 'vol1', 'new_val': 'val2'} ... } """ + def __init__(self, base_dict, new_dict): self.new_dict, self.base_dict = new_dict, base_dict self.base_items, self.new_items = set( @@ -408,6 +422,15 @@ class ActionModule(ActionBase): :param resultant: ``str`` || ``unicode`` :returns: ``str``, ``dict`` """ + def _add_section(section_name): + # Attempt to add a section to the config file passing if + # an error is raised that is related to the section + # already existing. + try: + config.add_section(section_name) + except (ConfigParser.DuplicateSectionError, ValueError): + pass + # If there is an exception loading the RawConfigParser The config obj # is loaded again without the extra option. This is being done to # support older python. @@ -417,14 +440,25 @@ class ActionModule(ActionBase): dict_type=MultiKeyDict, ignore_none_type=ignore_none_type, default_section=default_section, - yml_multilines=yml_multilines + yml_multilines=yml_multilines, + comment_prefixes='/' ) config.optionxform = str except Exception: - config = ConfigTemplateParser(dict_type=MultiKeyDict) + config = ConfigTemplateParser( + allow_no_value=True, + dict_type=MultiKeyDict, + comment_prefixes='/' + ) config_object = StringIO(resultant) - config.readfp(config_object) + try: + config.read_file(config_object) + except AttributeError: + config.readfp(config_object) + + if default_section != 'DEFAULT': + _add_section(section_name=default_section) for section, items in config_overrides.items(): # If the items value is not a dictionary it is assumed that the @@ -432,20 +466,15 @@ class ActionModule(ActionBase): if not isinstance(items, dict): if isinstance(items, list): items = ','.join(to_text(i) for i in items) + self._option_write( config, - 'DEFAULT', + default_section, section, items ) else: - # Attempt to add a section to the config file passing if - # an error is raised that is related to the section - # already existing. - try: - config.add_section(section) - except (ConfigParser.DuplicateSectionError, ValueError): - pass + _add_section(section_name=section) for key, value in items.items(): try: self._option_write(config, section, key, value) @@ -459,10 +488,10 @@ class ActionModule(ActionBase): else: config_object.close() - config_dict_new = {} + config_dict_new = OrderedDict() config_defaults = config.defaults() for s in config.sections(): - config_dict_new[s] = {} + config_dict_new[s] = OrderedDict() for k, v in config.items(s): if k not in config_defaults or config_defaults[k] != v: config_dict_new[s][k] = v @@ -571,7 +600,7 @@ class ActionModule(ActionBase): ) elif (not isinstance(value, int) and (',' in value or - ('\n' in value and not yml_multilines))): + ('\n' in value and not yml_multilines))): base_items[key] = re.split(',|\n', value) base_items[key] = [i.strip() for i in base_items[key] if i] elif isinstance(value, list): @@ -702,6 +731,21 @@ class ActionModule(ActionBase): remote_src=remote_src ) + def resultant_ini_as_dict(self, resultant_dict, return_dict=None): + if not return_dict: + return_dict = {} + + for key, value in resultant_dict.items(): + if not value: + continue + key = key.split(STRIP_MARKER)[0] + if isinstance(value, (OrderedDict, MultiKeyDict, dict)): + return_dict[key] = self.resultant_ini_as_dict(value) + else: + return_dict[key] = value + + return return_dict + def run(self, tmp=None, task_vars=None): """Run the method""" @@ -767,7 +811,6 @@ class ActionModule(ActionBase): self._templar._available_variables ) - config_dict_base = {} type_merger = getattr(self, CONFIG_TYPES.get(_vars['config_type'])) resultant, config_dict_base = type_merger( config_overrides=_vars['config_overrides'], @@ -785,8 +828,7 @@ class ActionModule(ActionBase): module_args=dict(src=_vars['dest']), task_vars=task_vars ) - - config_dict_new = {} + config_dict_new = dict() if 'content' in slurpee: dest_data = base64.b64decode( slurpee['content']).decode('utf-8') @@ -809,7 +851,10 @@ class ActionModule(ActionBase): # Compare source+overrides with dest to look for changes and # build diff - cmp_dicts = DictCompare(config_dict_new, config_dict_base) + cmp_dicts = DictCompare( + self.resultant_ini_as_dict(resultant_dict=config_dict_new), + self.resultant_ini_as_dict(resultant_dict=config_dict_base) + ) mods, changed = cmp_dicts.get_changes() # Re-template the resultant object as it may have new data within it diff --git a/releasenotes/notes/enhance-comment-parser-6fcb40646cdad662.yaml b/releasenotes/notes/enhance-comment-parser-6fcb40646cdad662.yaml new file mode 100644 index 0000000..872511b --- /dev/null +++ b/releasenotes/notes/enhance-comment-parser-6fcb40646cdad662.yaml @@ -0,0 +1,8 @@ +--- +features: + - Config template comment parser will now respect all comments and spacing + throughout INI files. This will allow us to use OSLO config to generate + files which contain commentary about the various defaults. This enhancement + will allow operators to benefit from deployer comments and system commentary + all from within the on-disk files while keeping all of the flexibility the + `config_template` action plugin provides. diff --git a/releasenotes/notes/enhance-option-insert-412e9032d8d6cb86.yaml b/releasenotes/notes/enhance-option-insert-412e9032d8d6cb86.yaml new file mode 100644 index 0000000..3a2fea6 --- /dev/null +++ b/releasenotes/notes/enhance-option-insert-412e9032d8d6cb86.yaml @@ -0,0 +1,8 @@ +--- +features: + - The `config_template` action plugin will now search for options within a + given section that may be commented within an INI file, and if a an option is + following the OSLO config pattern, '#OPTION_KEY...' pattern, `config_template` + will insert the override one line after the comment. This provides operators + the ability to see in service options and any comments regarding the option + within the configuration file on-disk. diff --git a/tests/files/test_comment_configs.ini.expected b/tests/files/test_comment_configs.ini.expected new file mode 100644 index 0000000..95ef5d0 --- /dev/null +++ b/tests/files/test_comment_configs.ini.expected @@ -0,0 +1,46 @@ +[DEFAULT] + +# +# From nova.conf +# + +# +# Availability zone for internal services. For more information, refer to the +# documentation. (string value) +#internal_service_availability_zone = internal + +# +# Default availability zone for compute services. For more information, refer to +# the documentation. (string value) +#default_availability_zone = nova +default_availability_zone = zone1 + +# +# Default availability zone for instances. For more information, refer to the +# documentation. (string value) +#default_schedule_zone = + +# Length of generated instance admin passwords (integer value) +# Minimum value = 0 +#password_length = 12 +password_length = 100 + +# +# Time period to generate instance usages for. It is possible to define optional +# offset to given period by appending @ character followed by a number defining +# offset. For more information, refer to the documentation. (string value) +#instance_usage_audit_period = month +instance_usage_audit_period = blah blah blah + +test = test1,test2 + +[SubSection] +#Comments and overrides in a subsection +#testopt1 = 9000 +testopt1 = 9000 + +# This is another test opt +#testop2 = over 9000 + +[TestSection] +things = stuff diff --git a/tests/files/test_default_section.ini.expected b/tests/files/test_default_section.ini.expected index 69bd9f0..94540b9 100644 --- a/tests/files/test_default_section.ini.expected +++ b/tests/files/test_default_section.ini.expected @@ -1,8 +1,9 @@ [global] test1 = 1 +# This is a post option comment + test2 = 2 [section1] setting1 = 1 -setting2 = 2 - +setting2 = 2 \ No newline at end of file diff --git a/tests/files/test_remote_src_multistropts.ini.expected b/tests/files/test_remote_src_multistropts.ini.expected index 67827e6..5887042 100644 --- a/tests/files/test_remote_src_multistropts.ini.expected +++ b/tests/files/test_remote_src_multistropts.ini.expected @@ -3,8 +3,10 @@ test = test1 test = test2 test = test3 -[remote_src_section] -test = output [testsection] test = output + + +[remote_src_section] +test = output diff --git a/tests/templates/test_comment_configs.ini b/tests/templates/test_comment_configs.ini new file mode 100644 index 0000000..a00a81a --- /dev/null +++ b/tests/templates/test_comment_configs.ini @@ -0,0 +1,38 @@ +[DEFAULT] + +# +# From nova.conf +# + +# +# Availability zone for internal services. For more information, refer to the +# documentation. (string value) +#internal_service_availability_zone = internal + +# +# Default availability zone for compute services. For more information, refer to +# the documentation. (string value) +#default_availability_zone = nova + +# +# Default availability zone for instances. For more information, refer to the +# documentation. (string value) +#default_schedule_zone = + +# Length of generated instance admin passwords (integer value) +# Minimum value: 0 +#password_length = 12 + +# +# Time period to generate instance usages for. It is possible to define optional +# offset to given period by appending @ character followed by a number defining +# offset. For more information, refer to the documentation. (string value) +#instance_usage_audit_period = month + +[SubSection] +#Comments and overrides in a subsection +#testopt1 = 9000 +testopt1 = 9000 + +# This is another test opt +#testop2 = over 9000 diff --git a/tests/templates/test_default_section.ini b/tests/templates/test_default_section.ini index 531e938..d759463 100644 --- a/tests/templates/test_default_section.ini +++ b/tests/templates/test_default_section.ini @@ -1,5 +1,6 @@ -[section1] -setting1=1 - [global] -test1=1 +test1 = 1 +# This is a post option comment + +[section1] +setting1 = 1 \ No newline at end of file diff --git a/tests/templates/test_multistropts.ini b/tests/templates/test_multistropts.ini index ba2d61c..e20c822 100644 --- a/tests/templates/test_multistropts.ini +++ b/tests/templates/test_multistropts.ini @@ -2,4 +2,3 @@ test = test1 test = test2 test = test3 - diff --git a/tests/templates/test_with_comments.ini b/tests/templates/test_with_comments.ini deleted file mode 100644 index 85a351e..0000000 --- a/tests/templates/test_with_comments.ini +++ /dev/null @@ -1,16 +0,0 @@ - # This comment tests bug 1755821 -# A default section comment -# broken into multiple lines -[DEFAULT] - -# This tests the py3 unicode bug #1763422 -test_hosts = - +_unicode - 1 - string - -[foo] -#This is a comment -baz = baz - -[bar] diff --git a/tests/test-common-tasks.yml b/tests/test-common-tasks.yml index 4b73159..65a729b 100644 --- a/tests/test-common-tasks.yml +++ b/tests/test-common-tasks.yml @@ -15,344 +15,6 @@ # # Test basic function of config_template -- name: Template test INI template - config_template: - src: "{{ playbook_dir }}/templates/test.ini" - dest: "/tmp/test.ini" - config_overrides: "{{ test_config_ini_overrides }}" - config_type: "ini" - register: test_ini - notify: test_ini check diff - -- name: Read test.ini - slurp: - src: /tmp/test.ini - register: ini_file -- debug: - msg: "ini - {{ ini_file.content | b64decode }}" -- name: Validate output - assert: - that: - - "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test.ini')) == 'new_value'" - - "(lookup('ini', 'baz section=foo file=/tmp/test.ini')) == 'bar'" - -# Test basic function of config_template with content instead of src -- name: Template test INI template - config_template: - content: "{{ lookup('file', playbook_dir + '/templates/test.ini') }}" - dest: "/tmp/test_with_content.ini" - config_overrides: "{{ test_config_ini_overrides }}" - config_type: "ini" - register: test_with_content_ini - notify: test_with_content_ini check diff - -- name: Read test.ini - slurp: - src: /tmp/test_with_content.ini - register: ini_file_with_content -- debug: - msg: "ini - {{ ini_file_with_content.content | b64decode }}" -- name: Validate output - assert: - that: - - "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test_with_content.ini')) == 'new_value'" - - "(lookup('ini', 'baz section=foo file=/tmp/test_with_content.ini')) == 'bar'" - -# Test list additions in config_template -- name: Template test YML template - config_template: - src: "{{ playbook_dir }}/templates/test.yml" - dest: "/tmp/test_extend.yml" - config_overrides: "{{ test_config_yml_overrides }}" - config_type: "yaml" - list_extend: True - register: test_extend_yml - notify: test_extend_yml check diff - -- name: Read test_extend.yml - slurp: - src: /tmp/test_extend.yml - register: extend_file -- debug: - msg: "extend - {{ extend_file.content | b64decode }}" -- debug: - msg: "extend.expected - {{ extend_file_expected.content | b64decode }}" -- name: Compare files - assert: - that: - - "(extend_file.content | b64decode) == (extend_file_expected.content | b64decode)" - -# Test list replacement in config_template -- name: Template test YML template - config_template: - src: "{{ playbook_dir }}/templates/test.yml" - dest: "/tmp/test_no_extend.yml" - config_overrides: "{{ test_config_yml_overrides }}" - config_type: "yaml" - list_extend: False - register: test_no_extend_yml - notify: test_no_extend_yml check diff - -- name: Read test_no_extend.yml - slurp: - src: /tmp/test_no_extend.yml - register: no_extend_file -- debug: - msg: "no_extend - {{ no_extend_file.content | b64decode }}" -- debug: - msg: "no_extend.expected - {{ no_extend_file_expected.content | b64decode }}" -- name: Compare files - assert: - that: - - "(no_extend_file.content | b64decode) == (no_extend_file_expected.content | b64decode)" - -# Test dumping hostvars using config overrides -- name: Template test YML template with hostvars override - config_template: - src: "{{ playbook_dir }}/templates/test.yml" - dest: "/tmp/test_hostvars.yml" - config_overrides: "{{ test_config_yml_hostvars_overrides }}" - config_type: "yaml" - register: test_hostvars_yml - notify: test_hostvars_yml check diff - -- name: Read test_hostvars.yml - slurp: - src: /tmp/test_hostvars.yml - register: hostvars_file -- debug: - msg: "hostvars - {{ (hostvars_file.content | b64decode | from_yaml).test_hostvar }}" -- debug: - msg: "hostvars.expected - {{ test_config_yml_hostvars_overrides.test_hostvar }}" -- name: Compare files - assert: - that: - - "((hostvars_file.content | b64decode | from_yaml).test_hostvar) == (test_config_yml_hostvars_overrides.test_hostvar)" - -# Values containing newlines should not be chopped into a list -# when yml_multilines is set to True -- name: Test multiline strings in yaml - config_template: - src: "{{ playbook_dir }}/templates/test_multiline_strs.yml" - dest: "/tmp/multiline_strs.yml" - config_overrides: "{{ test_multiline_strs_yml_overrides }}" - config_type: yaml - yml_multilines: True -- name: Read multiline_strs.yml - slurp: - src: /tmp/multiline_strs.yml - register: multiline_strs_file -- debug: - msg: "Multiline Yaml Strings - {{ multiline_strs_file.content | b64decode }}" -- debug: - msg: "Multiline Yaml Strings Expected - {{ multiline_strs_file_expected.content | b64decode }}" -- name: Compare files - assert: - that: - - "(multiline_strs_file_expected.content | b64decode) == (multiline_strs_file.content | b64decode)" - -# Test multistropt ordering -- name: Template MultiStrOpts using overrides - config_template: - src: test_multistropts.ini - dest: /tmp/test_multistropts.ini - config_overrides: - testsection: - test: output - config_type: ini -- name: Create expected MultiStrOpts file - copy: - src: files/test_multistropts.ini.expected - dest: /tmp/test_multistropts.ini.expected - -- name: Read test_multistropts.ini - slurp: - src: /tmp/test_multistropts.ini - register: multistropts_file -- name: Read test_multistropts.ini.expected - slurp: - src: /tmp/test_multistropts.ini.expected - register: multistropts_expected_file -- name: Set content facts - set_fact: - _multistropts_file: "{{ (multistropts_file.content | b64decode).strip() }}" - _multistropts_expected_file: "{{ (multistropts_expected_file.content | b64decode).strip() }}" -- name: Show rendered file - debug: - msg: "multistropts rendered - {{ _multistropts_file }}" -- name: Show expected file - debug: - msg: "multistropts expected - {{ _multistropts_expected_file }}" -- name: Compare files - assert: - that: - - _multistropts_file == _multistropts_expected_file - -# Test remote_src -- name: Template remote source using overrides - config_template: - src: /tmp/test_multistropts.ini - dest: /tmp/test_remote_src_multistropts.ini - remote_src: true - config_overrides: - remote_src_section: - test: output - config_type: ini -- name: Create expected MultiStrOpts file - copy: - src: files/test_remote_src_multistropts.ini.expected - dest: /tmp/test_remote_src_multistropts.ini.expected - -- name: Read test_remote_src_multistropts.ini - slurp: - src: /tmp/test_remote_src_multistropts.ini - register: multistropts_file -- name: Read test_remote_src_multistropts.ini.expected - slurp: - src: /tmp/test_remote_src_multistropts.ini.expected - register: multistropts_expected_file -- name: Set content facts - set_fact: - _remote_src_file: "{{ (multistropts_file.content | b64decode).strip() }}" - _remote_src_expected_file: "{{ (multistropts_expected_file.content | b64decode).strip() }}" -- name: Show rendered file - debug: - msg: "multistropts rendered - {{ _remote_src_file }}" -- name: Show expected file - debug: - msg: "multistropts expected - {{ _remote_src_expected_file }}" -- name: Compare files - assert: - that: - - _remote_src_file == _remote_src_expected_file - -# Test content attribute with a dictionary input and config_type equal to 'json' -- name: Template test JSON template with content attribute - config_template: - dest: "/tmp/test_content_no_overrides.json" - config_overrides: {} - config_type: "json" - content: "{{ lookup('file', playbook_dir ~ '/templates/test.json') | from_json }}" - register: test_content_no_overrides_json - notify: test_content_no_overrides_json check diff - -- name: Read test_content_no_overrides.json - slurp: - src: /tmp/test_content_no_overrides.json - register: content_no_overrides_file -- debug: - msg: "content_no_overrides.json - {{ content_no_overrides_file.content | b64decode | from_json }}" -- debug: - msg: "content_no_overrides.json.expected - {{ content_no_overrides_file_expected.content | b64decode | from_json }}" -# NOTE (alextricity25): The config_template module doesn't use ordered dicts when reading and writing json -# data, so we can't guarantee that the string literal of both file's content will be the same. Instead, we compare -# the content after transforming it into a dictionary. -- name: Compare file content - assert: - that: - - "(content_no_overrides_file.content | b64decode | from_json) == (content_no_overrides_file_expected.content | b64decode | from_json)" - -# Test the ignore_none_type attribute when set to False -- name: Template test with ignore_none_type set to false - config_template: - src: "{{ playbook_dir }}/templates/test_ignore_none_type.ini" - dest: "/tmp/test_ignore_none_type.ini" - config_overrides: "{{ test_config_ini_overrides }}" - config_type: "ini" - ignore_none_type: False - register: test_ignore_none_type_ini - notify: test_ignore_none_type_ini check diff - -- name: Read test_ignore_none_type.ini - slurp: - src: /tmp/test_ignore_none_type.ini - register: test_ignore_none_type -- debug: - msg: "test_ignore_none_type.ini - {{ test_ignore_none_type.content | b64decode }}" -- name: Validate output has valueless options printed out - assert: - that: - - "{{ test_ignore_none_type.content | b64decode | search('(?m)^india$') }}" - - "{{ test_ignore_none_type.content | b64decode | search('(?m)^juliett kilo$') }}" - - # Test basic function of config_template -- name: Template test INI comments - config_template: - src: "{{ playbook_dir }}/templates/test_with_comments.ini" - dest: "/tmp/test_with_comments.ini" - config_overrides: "{{ test_config_ini_overrides }}" - config_type: "ini" - tags: test - register: test_with_comments_ini - notify: test_with_comments_ini check diff - -- name: Read test.ini - slurp: - src: /tmp/test_with_comments.ini - register: ini_file - tags: test - -- debug: - msg: "ini - {{ ini_file.content | b64decode }}" -- name: Validate output - tags: test - assert: - that: - - "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test_with_comments.ini')) == 'new_value'" - - "(lookup('ini', 'baz section=foo file=/tmp/test_with_comments.ini')) == 'bar'" - - "{{ ini_file.content | b64decode | search('# This comment tests bug 1755821')}}" - - "{{ not(ini_file.content | b64decode | search(' # This comment tests bug 1755821'))}}" - - "{{ ini_file.content | b64decode | search('#This is a comment')}}" - - "{{ ini_file.content | b64decode | search('# A default section comment\n# broken into multiple lines\n\\[DEFAULT\\]')}}" - -- name: Template multiple times to assert no changes - config_template: - src: "{{ playbook_dir }}/templates/test_with_comments.ini" - dest: "/tmp/test_with_comments.ini" - config_type: "ini" - config_overrides: "{{ item[1] }}" - register: template_changed - failed_when: template_changed is changed - with_nested: - - [ 0, 1, 2 ] - - [ "{{ test_config_ini_overrides }}" ] - -- name: Put down default_section_expected file - copy: - src: "{{ playbook_dir }}/files/test_default_section.ini.expected" - dest: "/tmp/test_default_section.ini" - -- name: Template using default_section - config_template: - src: "{{ playbook_dir }}/templates/test_default_section.ini" - dest: "/tmp/test_default_section.ini" - config_type: "ini" - config_overrides: "{{ test_default_section_overrides }}" - default_section: "global" - register: template_changed - failed_when: template_changed is changed - -- name: Write ini for testing diff output - config_template: - src: "{{ playbook_dir }}/templates/test_diff.ini" - dest: "/tmp/test_diff.ini" - config_type: "ini" - config_overrides: {} - -- name: Test ini with additions and changed - config_template: - src: "{{ playbook_dir }}/templates/test_diff.ini" - dest: "/tmp/test_diff.ini" - config_type: "ini" - config_overrides: "{{ test_diff_overrides }}" - register: test_diff_ini - notify: test_diff_ini check diff - -- name: Test ini with removes - config_template: - src: "{{ playbook_dir }}/templates/test_diff_remove.ini" - dest: "/tmp/test_diff.ini" - config_type: "ini" - config_overrides: "{{ test_diff_overrides }}" - register: test_diff_remove_ini - notify: test_diff_remove_ini check diff +- import_tasks: test-ini.yml +- import_tasks: test-yaml.yml +- import_tasks: test-json.yml diff --git a/tests/test-ini.yml b/tests/test-ini.yml new file mode 100644 index 0000000..a356237 --- /dev/null +++ b/tests/test-ini.yml @@ -0,0 +1,286 @@ +--- +# Copyright 2018, Rackspace US +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Test basic function of config_template + +# Test basic ini template +- name: Template test INI template + config_template: + src: "{{ playbook_dir }}/templates/test.ini" + dest: "/tmp/test.ini" + config_overrides: "{{ test_config_ini_overrides }}" + config_type: "ini" + +- name: Read test.ini + slurp: + src: /tmp/test.ini + register: ini_file + +- debug: + msg: "ini - {{ ini_file.content | b64decode }}" + +- name: Validate output + assert: + that: + - "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test.ini')) == 'new_value'" + - "(lookup('ini', 'baz section=foo file=/tmp/test.ini')) == 'bar'" + + +# Test basic function of config_template with content instead of src +- name: Template test INI template + config_template: + content: "{{ lookup('file', playbook_dir + '/templates/test.ini') }}" + dest: "/tmp/test_with_content.ini" + config_overrides: "{{ test_config_ini_overrides }}" + config_type: "ini" + +- name: Read test.ini + slurp: + src: /tmp/test_with_content.ini + register: ini_file_with_content + +- debug: + msg: "ini - {{ ini_file_with_content.content | b64decode }}" + +- name: Validate output + assert: + that: + - "(lookup('ini', 'new_key section=DEFAULT file=/tmp/test_with_content.ini')) == 'new_value'" + - "(lookup('ini', 'baz section=foo file=/tmp/test_with_content.ini')) == 'bar'" + + +# Test multistropt ordering +- name: Template MultiStrOpts using overrides + config_template: + src: test_multistropts.ini + dest: /tmp/test_multistropts.ini + config_overrides: + testsection: + test: output + config_type: ini + +- name: Create expected MultiStrOpts file + copy: + src: files/test_multistropts.ini.expected + dest: /tmp/test_multistropts.ini.expected + +- name: Read test_multistropts.ini + slurp: + src: /tmp/test_multistropts.ini + register: multistropts_file + +- name: Read test_multistropts.ini.expected + slurp: + src: /tmp/test_multistropts.ini.expected + register: multistropts_expected_file + +- name: Set content facts + set_fact: + _multistropts_file: "{{ (multistropts_file.content | b64decode).strip() }}" + _multistropts_expected_file: "{{ (multistropts_expected_file.content | b64decode).strip() }}" + +- name: Show rendered file + debug: + msg: "multistropts rendered - {{ _multistropts_file }}" + +- name: Show expected file + debug: + msg: "multistropts expected - {{ _multistropts_expected_file }}" + +- name: Compare files + assert: + that: + - _multistropts_file == _multistropts_expected_file + + +# Test remote_src +- name: Template remote source using overrides + config_template: + src: /tmp/test_multistropts.ini + dest: /tmp/test_remote_src_multistropts.ini + remote_src: true + config_overrides: + remote_src_section: + test: output + config_type: ini + +- name: Create expected MultiStrOpts file + copy: + src: files/test_remote_src_multistropts.ini.expected + dest: /tmp/test_remote_src_multistropts.ini.expected + +- name: Read test_remote_src_multistropts.ini + slurp: + src: /tmp/test_remote_src_multistropts.ini + register: multistropts_file + +- name: Read test_remote_src_multistropts.ini.expected + slurp: + src: /tmp/test_remote_src_multistropts.ini.expected + register: multistropts_expected_file + +- name: Set content facts + set_fact: + _remote_src_file: "{{ (multistropts_file.content | b64decode).strip() }}" + _remote_src_expected_file: "{{ (multistropts_expected_file.content | b64decode).strip() }}" + +- name: Show rendered file + debug: + msg: "multistropts rendered - {{ _remote_src_file }}" + +- name: Show expected file + debug: + msg: "multistropts expected - {{ _remote_src_expected_file }}" + +- name: Compare files + assert: + that: + - _remote_src_file == _remote_src_expected_file + + +# Test the ignore_none_type attribute when set to False +- name: Template test with ignore_none_type set to false + config_template: + src: "{{ playbook_dir }}/templates/test_ignore_none_type.ini" + dest: "/tmp/test_ignore_none_type.ini" + config_overrides: "{{ test_config_ini_overrides }}" + config_type: "ini" + ignore_none_type: False + +- name: Read test_ignore_none_type.ini + slurp: + src: /tmp/test_ignore_none_type.ini + register: test_ignore_none_type + +- debug: + msg: "test_ignore_none_type.ini - {{ test_ignore_none_type.content | b64decode }}" + +- name: Validate output has valueless options printed out + assert: + that: + - "{{ test_ignore_none_type.content | b64decode | search('(?m)^india$') }}" + - "{{ test_ignore_none_type.content | b64decode | search('(?m)^juliett kilo$') }}" + + +# Test enhanced comments +- name: Template test INI template + config_template: + content: "{{ lookup('file', playbook_dir + '/templates/test_comment_configs.ini') }}" + dest: "/tmp/test_comment_configs.ini" + config_overrides: "{{ test_enhanced_comments_ini_overrides }}" + config_type: "ini" + +- name: Create expected enhanced comments file + copy: + src: files/test_comment_configs.ini.expected + dest: /tmp/test_comment_configs.ini.expected + +- name: Read test_comment_configs.ini + slurp: + src: /tmp/test_comment_configs.ini + register: test_comment_configs + +- name: Read test_comment_configs.ini.expected + slurp: + src: /tmp/test_comment_configs.ini.expected + register: test_comment_configs_expected + +- name: Set content facts + set_fact: + _enhanced_comments_file: "{{ (test_comment_configs.content | b64decode).strip() }}" + _enhanced_comments_expected_file: "{{ (test_comment_configs_expected.content | b64decode).strip() }}" + +- name: Show rendered file + debug: + msg: "multistropts rendered - {{ _enhanced_comments_file }}" + +- name: Show expected file + debug: + msg: "multistropts expected - {{ _enhanced_comments_expected_file }}" + +- name: Compare files + assert: + that: + - _enhanced_comments_file == _enhanced_comments_expected_file + + +# Test setting a default_section +- name: Template using default_section + config_template: + src: "{{ playbook_dir }}/templates/test_default_section.ini" + dest: "/tmp/test_default_section.ini" + config_type: "ini" + config_overrides: "{{ test_default_section_overrides }}" + default_section: "global" + +- name: Put down default_section_expected file + copy: + src: "{{ playbook_dir }}/files/test_default_section.ini.expected" + dest: "/tmp/test_default_section.ini.expected" + +- name: Read test_default_section.ini + slurp: + src: "/tmp/test_default_section.ini" + register: test_default_section + +- name: Read test_default_section.ini.expected + slurp: + src: "/tmp/test_default_section.ini.expected" + register: test_default_section_expected + +- name: Set content facts + set_fact: + _test_default_section_file: "{{ (test_default_section.content | b64decode).strip() }}" + _test_default_section_expected_file: "{{ (test_default_section_expected.content | b64decode).strip() }}" + +- name: Show rendered file + debug: + msg: "default rendered - {{ _test_default_section_file }}" + +- name: Show expected file + debug: + msg: "default expected - {{ _test_default_section_expected_file }}" + +- name: Compare files + assert: + that: + - _test_default_section_file == _test_default_section_expected_file + + +# Check output diff +- name: Write ini for testing diff output + config_template: + src: "{{ playbook_dir }}/templates/test_diff.ini" + dest: "/tmp/test_diff.ini" + config_type: "ini" + config_overrides: {} + +- name: Test ini with additions and changed + config_template: + src: "{{ playbook_dir }}/templates/test_diff.ini" + dest: "/tmp/test_diff.ini" + config_type: "ini" + config_overrides: "{{ test_diff_overrides }}" + register: test_diff_ini + notify: test_diff_ini check diff + +- name: Test ini with removes + config_template: + src: "{{ playbook_dir }}/templates/test_diff_remove.ini" + dest: "/tmp/test_diff.ini" + config_type: "ini" + config_overrides: "{{ test_diff_overrides }}" + register: test_diff_remove_ini + notify: test_diff_remove_ini check diff diff --git a/tests/test-json.yml b/tests/test-json.yml new file mode 100644 index 0000000..7e5ed75 --- /dev/null +++ b/tests/test-json.yml @@ -0,0 +1,45 @@ +--- +# Copyright 2018, Rackspace US +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Test basic function of config_template + +# Test content attribute with a dictionary input and config_type equal to 'json' +- name: Template test JSON template with content attribute + config_template: + dest: "/tmp/test_content_no_overrides.json" + config_overrides: {} + config_type: "json" + content: "{{ lookup('file', playbook_dir ~ '/templates/test.json') | from_json }}" + register: test_content_no_overrides_json + notify: test_content_no_overrides_json check diff + +- name: Read test_content_no_overrides.json + slurp: + src: /tmp/test_content_no_overrides.json + register: content_no_overrides_file + +- debug: + msg: "content_no_overrides.json - {{ content_no_overrides_file.content | b64decode | from_json }}" + +- debug: + msg: "content_no_overrides.json.expected - {{ content_no_overrides_file_expected.content | b64decode | from_json }}" + +# NOTE (alextricity25): The config_template module doesn't use ordered dicts when reading and writing json +# data, so we can't guarantee that the string literal of both file's content will be the same. Instead, we compare +# the content after transforming it into a dictionary. +- name: Compare file content + assert: + that: + - "(content_no_overrides_file.content | b64decode | from_json) == (content_no_overrides_file_expected.content | b64decode | from_json)" diff --git a/tests/test-yaml.yml b/tests/test-yaml.yml new file mode 100644 index 0000000..4d578a6 --- /dev/null +++ b/tests/test-yaml.yml @@ -0,0 +1,125 @@ +--- +# Copyright 2018, Rackspace US +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Test basic function of config_template + +# Test list additions in config_template +- name: Template test YML template + config_template: + src: "{{ playbook_dir }}/templates/test.yml" + dest: "/tmp/test_extend.yml" + config_overrides: "{{ test_config_yml_overrides }}" + config_type: "yaml" + list_extend: True + register: test_extend_yml + notify: test_extend_yml check diff + +- name: Read test_extend.yml + slurp: + src: /tmp/test_extend.yml + register: extend_file + +- debug: + msg: "extend - {{ extend_file.content | b64decode }}" + +- debug: + msg: "extend.expected - {{ extend_file_expected.content | b64decode }}" + +- name: Compare files + assert: + that: + - "(extend_file.content | b64decode) == (extend_file_expected.content | b64decode)" + + +# Test list replacement in config_template +- name: Template test YML template + config_template: + src: "{{ playbook_dir }}/templates/test.yml" + dest: "/tmp/test_no_extend.yml" + config_overrides: "{{ test_config_yml_overrides }}" + config_type: "yaml" + list_extend: False + register: test_no_extend_yml + notify: test_no_extend_yml check diff + +- name: Read test_no_extend.yml + slurp: + src: /tmp/test_no_extend.yml + register: no_extend_file + +- debug: + msg: "no_extend - {{ no_extend_file.content | b64decode }}" + +- debug: + msg: "no_extend.expected - {{ no_extend_file_expected.content | b64decode }}" + +- name: Compare files + assert: + that: + - "(no_extend_file.content | b64decode) == (no_extend_file_expected.content | b64decode)" + + +# Test dumping hostvars using config overrides +- name: Template test YML template with hostvars override + config_template: + src: "{{ playbook_dir }}/templates/test.yml" + dest: "/tmp/test_hostvars.yml" + config_overrides: "{{ test_config_yml_hostvars_overrides }}" + config_type: "yaml" + register: test_hostvars_yml + notify: test_hostvars_yml check diff + +- name: Read test_hostvars.yml + slurp: + src: /tmp/test_hostvars.yml + register: hostvars_file + +- debug: + msg: "hostvars - {{ (hostvars_file.content | b64decode | from_yaml).test_hostvar }}" + +- debug: + msg: "hostvars.expected - {{ test_config_yml_hostvars_overrides.test_hostvar }}" + +- name: Compare files + assert: + that: + - "((hostvars_file.content | b64decode | from_yaml).test_hostvar) == (test_config_yml_hostvars_overrides.test_hostvar)" + + +# Values containing newlines should not be chopped into a list +# when yml_multilines is set to True +- name: Test multiline strings in yaml + config_template: + src: "{{ playbook_dir }}/templates/test_multiline_strs.yml" + dest: "/tmp/multiline_strs.yml" + config_overrides: "{{ test_multiline_strs_yml_overrides }}" + config_type: yaml + yml_multilines: True + +- name: Read multiline_strs.yml + slurp: + src: /tmp/multiline_strs.yml + register: multiline_strs_file + +- debug: + msg: "Multiline Yaml Strings - {{ multiline_strs_file.content | b64decode }}" + +- debug: + msg: "Multiline Yaml Strings Expected - {{ multiline_strs_file_expected.content | b64decode }}" + +- name: Compare files + assert: + that: + - "(multiline_strs_file_expected.content | b64decode) == (multiline_strs_file.content | b64decode)" diff --git a/tests/test.yml b/tests/test.yml index ac58306..5891162 100644 --- a/tests/test.yml +++ b/tests/test.yml @@ -69,16 +69,6 @@ delegate_to: container1 handlers: - - name: test_ini check diff - assert: - that: - - test_ini.diff[0].prepared|from_json == diff_ini - - - name: test_with_content_ini check diff - assert: - that: - - test_with_content_ini.diff[0].prepared|from_json == diff_ini - - name: test_extend_yml check diff assert: that: @@ -99,17 +89,6 @@ that: - test_content_no_overrides_json.diff[0].prepared|from_json == diff_content_no_overrides_json - - name: test_ignore_none_type_ini check diff - assert: - that: - - test_ignore_none_type_ini.diff[0].prepared|from_json == diff_ignore_none_type_ini - - - name: test_with_comments_ini check diff - tags: test - assert: - that: - - test_with_comments_ini.diff[0].prepared|from_json == diff_with_comments_ini - - name: test_diff_ini check diff tags: test assert: @@ -130,6 +109,8 @@ baz: "bar" section1: key1: "String1" + key10: 10 + key11: 11 key2: "string2" key3: "string3" key4: "string4" @@ -138,8 +119,10 @@ key7: 1 key8: 2 key9: 3 - key10: 10 - key11: 11 + section10: + key1: 1 + section11: + key1: 1 section2: key1: "value1" section3: @@ -156,10 +139,6 @@ key1: 1 section9: key1: 1 - section10: - key1: 1 - section11: - key1: 1 test_config_yml_overrides: list_one: - four @@ -180,53 +159,20 @@ baz: "hotel" section3: alfa: "bravo" - diff_with_comments_ini: - added: - DEFAULT: - new_key: "new_value" - test_hosts: "\n+_unicode\n1\nstring" - bar: {} - foo: - baz: "bar" - section1: - key1: "String1" - key10: "10" - key11: "11" - key2: "string2" - key3: "string3" - key4: "string4" - key5: "string5" - key6: "string6" - key7: "1" - key8: "2" - key9: "3" - section10: - key1: "1" - section11: - key1: "1" - section2: - key1: "value1" - section3: - key1: "value1" - section4: - key1: "value1" - section5: - key1: "value1" - section6: - key1: "value1" - section7: - key1: "value1" - section8: - key1: "1" - section9: - key1: "1" - changed: {} - removed: {} + test_enhanced_comments_ini_overrides: + DEFAULT: + default_availability_zone: zone1 + instance_usage_audit_period: blah blah blah + password_length: 100 + test: + - test1 + - test2 + TestSection: + things: stuff diff_ini: added: DEFAULT: new_key: "new_value" - bar: {} foo: baz: "bar" section1: @@ -298,54 +244,6 @@ test_hostvar: "{{ ansible_default_ipv4.address }}" changed: {} removed: {} - diff_ignore_none_type_ini: - added: - DEFAULT: - new_key: "new_value" - alfa: - bravo: "charlie" - delta: "echo" - foo: - baz: "bar" - foxtrot: - golf: "hotel" - india: null - juliett kilo: null - lima: "mike" - section1: - key1: "String1" - key10: "10" - key11: "11" - key2: "string2" - key3: "string3" - key4: "string4" - key5: "string5" - key6: "string6" - key7: "1" - key8: "2" - key9: "3" - section10: - key1: "1" - section11: - key1: "1" - section2: - key1: "value1" - section3: - key1: "value1" - section4: - key1: "value1" - section5: - key1: "value1" - section6: - key1: "value1" - section7: - key1: "value1" - section8: - key1: "1" - section9: - key1: "1" - changed: {} - removed: {} diff_content_no_overrides_json: added: alfa: "bravo"