From b74d7acbf42b6f09acb39e9ecdb7ffae41c914bb Mon Sep 17 00:00:00 2001 From: Michael Vollman Date: Fri, 13 Jul 2018 07:33:40 -0400 Subject: [PATCH] Compare dict vars to determine changed Compare dict vars of before and after configuration to determine whether the config keys or values have changed so a configuration file will not be incorrectly marked as changed when only the ordering has changed. Set diff return variable to a dict of changes applied. Change-Id: Ie67119b1420936c8ed89f8338ea9dce4c47e185c --- action/config_template.py | 172 ++++++- .../notes/diffmode-e8f9a041f662a2ef.yaml | 6 + tests/ansible-role-requirements.yml | 20 + tests/group_vars/all_containers.yml | 5 +- tests/inventory | 19 +- tests/templates/test_diff.ini | 7 + tests/templates/test_diff_remove.ini | 6 + tests/test-common-tasks.yml | 259 ++++++++++ tests/test.yml | 450 +++++++++++------- tox.ini | 7 +- 10 files changed, 724 insertions(+), 227 deletions(-) create mode 100644 releasenotes/notes/diffmode-e8f9a041f662a2ef.yaml create mode 100644 tests/templates/test_diff.ini create mode 100644 tests/templates/test_diff_remove.ini create mode 100644 tests/test-common-tasks.yml diff --git a/action/config_template.py b/action/config_template.py index e88488f..9739610 100644 --- a/action/config_template.py +++ b/action/config_template.py @@ -26,6 +26,7 @@ try: from StringIO import StringIO except ImportError: from io import StringIO +import base64 import json import os import pwd @@ -297,6 +298,90 @@ class ConfigTemplateParser(ConfigParser.RawConfigParser): options[name] = _temp_item +class DictCompare(object): + """ + Calculate the difference between two dictionaries. + + Example Usage: + >>> base_dict = {'test1': 'val1', 'test2': 'val2', 'test3': 'val3'} + >>> new_dict = {'test1': 'val2', 'test3': 'val3', 'test4': 'val3', 'test5': 'val5'} + >>> dc = DictCompare(base_dict, new_dict) + >>> dc.added() + ... ['test5', 'test4'] + >>> dc.removed() + ... ['test2'] + >>> dc.changed() + ... ['test1'] + >>> dc.get_changes() + ... {'added': + ... {'test4': 'val3','test5': 'val5'}, + ... 'removed': + ... {'test2': 'val2'}, + ... 'changed': + ... {'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(self.base_dict.keys()), set(self.new_dict.keys()) + self.intersect = self.new_items.intersection(self.base_items) + + def added(self): + return self.new_items - self.intersect + + def removed(self): + return self.base_items - self.intersect + + def changed(self): + return set(x for x in self.intersect if self.base_dict[x] != self.new_dict[x]) + + def get_changes(self): + """Returns dict of differences between 2 dicts and bool indicating if there are differences + + :param base_dict: ``dict`` + :param new_dict: ``dict`` + :returns: ``dict``, ``bool`` + """ + changed=False + mods={'added': {}, 'removed': {}, 'changed': {}} + + for s in self.changed(): + changed=True + if type(self.base_dict[s]) is not dict: + mods['changed'] = {s: {'current_val': self.base_dict[s], 'new_val': self.new_dict[s]}} + continue + + diff = DictCompare(self.base_dict[s], self.new_dict[s]) + for a in diff.added(): + if s not in mods['added']: + mods['added'][s] = {a: self.new_dict[s][a]} + else: + mods['added'][s][a] = self.new_dict[s][a] + + for r in diff.removed(): + if s not in mods['removed']: + mods['removed'][s] = {r: self.base_dict[s][r]} + else: + mods['removed'][s][r] = self.base_dict[s][r] + + for c in diff.changed(): + if s not in mods['changed']: + mods['changed'][s] = {c: {'current_val': self.base_dict[s][c], 'new_val': self.new_dict[s][c]}} + else: + mods['changed'][s][c] = {'current_val': self.base_dict[s][c], 'new_val': self.new_dict[s][c]} + + for s in self.added(): + changed=True + mods['added'][s] = self.new_dict[s] + + for s in self.removed(): + changed=True + mods['removed'][s] = self.base_dict[s] + + + return mods, changed + + class ActionModule(ActionBase): TRANSFERS_FILES = True @@ -306,11 +391,11 @@ class ActionModule(ActionBase): list_extend=True, ignore_none_type=True, default_section='DEFAULT'): - """Returns string value from a modified config file. + """Returns string value from a modified config file and dict of merged config :param config_overrides: ``dict`` :param resultant: ``str`` || ``unicode`` - :returns: ``str`` + :returns: ``str``, ``dict`` """ # If there is an exception loading the RawConfigParser The config obj # is loaded again without the extra option. This is being done to @@ -328,6 +413,7 @@ class ActionModule(ActionBase): config_object = StringIO(resultant) config.readfp(config_object) + for section, items in config_overrides.items(): # If the items value is not a dictionary it is assumed that the # value is a default item for this config type. @@ -361,10 +447,23 @@ class ActionModule(ActionBase): else: config_object.close() + config_dict_new = {} + config_defaults = config.defaults() + for s in config.sections(): + config_dict_new[s] = {} + for k, v in config.items(s): + if k not in config_defaults or config_defaults[k] != v: + config_dict_new[s][k] = v + else: + if default_section in config_dict_new: + config_dict_new[default_section][k] = v + else: + config_dict_new[default_section] = {k: v} + resultant_stringio = StringIO() try: config.write(resultant_stringio) - return resultant_stringio.getvalue() + return resultant_stringio.getvalue(), config_dict_new finally: resultant_stringio.close() @@ -391,27 +490,26 @@ class ActionModule(ActionBase): list_extend=True, ignore_none_type=True, default_section='DEFAULT'): - """Returns config json + """Returns config json and dict of merged config Its important to note that file ordering will not be preserved as the information within the json file will be sorted by keys. :param config_overrides: ``dict`` :param resultant: ``str`` || ``unicode`` - :returns: ``str`` + :returns: ``str``, ``dict`` """ original_resultant = json.loads(resultant) merged_resultant = self._merge_dict( base_items=original_resultant, new_items=config_overrides, - list_extend=list_extend, - default_section=default_section + list_extend=list_extend ) return json.dumps( merged_resultant, indent=4, sort_keys=True - ) + ), merged_resultant def return_config_overrides_yaml(self, config_overrides, @@ -419,11 +517,11 @@ class ActionModule(ActionBase): list_extend=True, ignore_none_type=True, default_section='DEFAULT'): - """Return config yaml. + """Return config yaml and dict of merged config :param config_overrides: ``dict`` :param resultant: ``str`` || ``unicode`` - :returns: ``str`` + :returns: ``str``, ``dict`` """ original_resultant = yaml.safe_load(resultant) merged_resultant = self._merge_dict( @@ -436,7 +534,7 @@ class ActionModule(ActionBase): Dumper=IDumper, default_flow_style=False, width=1000, - ) + ), merged_resultant def _merge_dict(self, base_items, new_items, list_extend=True): """Recursively merge new_items into base_items. @@ -631,16 +729,47 @@ class ActionModule(ActionBase): self._templar._available_variables ) - if _vars['config_overrides']: - type_merger = getattr(self, CONFIG_TYPES.get(_vars['config_type'])) - resultant = type_merger( - config_overrides=_vars['config_overrides'], - resultant=resultant, - list_extend=_vars.get('list_extend', True), - ignore_none_type=_vars.get('ignore_none_type', True), - default_section=_vars.get('default_section', 'DEFAULT') + config_dict_base = {} + type_merger = getattr(self, CONFIG_TYPES.get(_vars['config_type'])) + resultant, config_dict_base = type_merger( + config_overrides=_vars['config_overrides'], + resultant=resultant, + list_extend=_vars.get('list_extend', True), + ignore_none_type=_vars.get('ignore_none_type', True), + default_section=_vars.get('default_section', 'DEFAULT') + ) + + changed=False + if self._play_context.diff: + slurpee = self._execute_module( + module_name='slurp', + module_args=dict(src=_vars['dest']), + task_vars=task_vars ) + config_dict_new = {} + if 'content' in slurpee: + dest_data = base64.b64decode(slurpee['content']).decode('utf-8') + resultant_dest = self._templar.template( + dest_data, + preserve_trailing_newlines=True, + escape_backslashes=False, + convert_data=False + ) + type_merger = getattr(self, CONFIG_TYPES.get(_vars['config_type'])) + resultant_new, config_dict_new = type_merger( + config_overrides={}, + resultant=resultant_dest, + list_extend=_vars.get('list_extend', True), + ignore_none_type=_vars.get('ignore_none_type', True), + default_section=_vars.get('default_section', 'DEFAULT') + ) + + # Compare source+overrides with dest to look for changes and build diff + cmp_dicts = DictCompare(config_dict_new, config_dict_base) + mods, changed = cmp_dicts.get_changes() + + # Re-template the resultant object as it may have new data within it # as provided by an override variable. resultant = self._templar.template( @@ -691,6 +820,11 @@ class ActionModule(ActionBase): module_args=new_module_args, task_vars=task_vars ) + + rc['changed'] = changed + if self._play_context.diff: + rc['diff'] = [] + rc['diff'].append({'prepared': json.dumps(mods, indent=4, sort_keys=True)}) if self._task.args.get('content'): os.remove(_vars['source']) return rc diff --git a/releasenotes/notes/diffmode-e8f9a041f662a2ef.yaml b/releasenotes/notes/diffmode-e8f9a041f662a2ef.yaml new file mode 100644 index 0000000..7fd196e --- /dev/null +++ b/releasenotes/notes/diffmode-e8f9a041f662a2ef.yaml @@ -0,0 +1,6 @@ +--- +features: + - Compare dict vars of before and after configuration to determine whether + the config keys or values have changed so a configuration file will not + be incorrectly marked as changed when only the ordering has changed. + - Set diff return variable to a dict of changes applied. diff --git a/tests/ansible-role-requirements.yml b/tests/ansible-role-requirements.yml index 30367e9..b2107c8 100644 --- a/tests/ansible-role-requirements.yml +++ b/tests/ansible-role-requirements.yml @@ -1,4 +1,24 @@ +- name: apt_package_pinning + src: https://git.openstack.org/openstack/openstack-ansible-apt_package_pinning + scm: git + version: master - name: pip_install src: https://git.openstack.org/openstack/openstack-ansible-pip_install scm: git version: master +- name: openstack_hosts + src: https://git.openstack.org/openstack/openstack-ansible-openstack_hosts + scm: git + version: master +- name: lxc_hosts + src: https://git.openstack.org/openstack/openstack-ansible-lxc_hosts + scm: git + version: master +- name: lxc_container_create + src: https://git.openstack.org/openstack/openstack-ansible-lxc_container_create + scm: git + version: master +- name: openstack_openrc + src: https://git.openstack.org/openstack/openstack-ansible-openstack_openrc + scm: git + version: master diff --git a/tests/group_vars/all_containers.yml b/tests/group_vars/all_containers.yml index 4f263c6..52847ce 100644 --- a/tests/group_vars/all_containers.yml +++ b/tests/group_vars/all_containers.yml @@ -20,11 +20,8 @@ container_networks: address: "{{ ansible_host }}" bridge: "br-mgmt" interface: "eth1" - netmask: "255.255.252.0" + netmask: "255.255.255.0" type: "veth" - static_routes: - - cidr: 10.100.100.0/24 - gateway: 10.100.100.1 properties: {} diff --git a/tests/inventory b/tests/inventory index d28bbe2..25b2290 100644 --- a/tests/inventory +++ b/tests/inventory @@ -1,20 +1,9 @@ +[all] +localhost +container1 + [hosts] localhost [all_containers] container1 -container2 - -# This is used to test I75f9d0f55ecd875caa1bf608a77c92f950b679a1 -[hosts] -localhost_alt ansible_host=localhost -[all_containers] -container3 physical_host=localhost_alt - -# This is meant to test If594914df53efacc6d5bba148f4f46280f5a117d -[fake_hosts] -fakehost ansible_host=1.1.1.1 -[hosts:children] -fake_hosts -[fake_containers] -fakecontainer container_name="{{ inventory_hostname }}" physical_host=fakehost diff --git a/tests/templates/test_diff.ini b/tests/templates/test_diff.ini new file mode 100644 index 0000000..277fdca --- /dev/null +++ b/tests/templates/test_diff.ini @@ -0,0 +1,7 @@ +[DEFAULT] + +[section1] +baz = baz + +[section2] +foo = bar diff --git a/tests/templates/test_diff_remove.ini b/tests/templates/test_diff_remove.ini new file mode 100644 index 0000000..cdbf8f4 --- /dev/null +++ b/tests/templates/test_diff_remove.ini @@ -0,0 +1,6 @@ +[DEFAULT] + +[section1] +baz = baz + +[section2] diff --git a/tests/test-common-tasks.yml b/tests/test-common-tasks.yml new file mode 100644 index 0000000..0d94a00 --- /dev/null +++ b/tests/test-common-tasks.yml @@ -0,0 +1,259 @@ +--- +# 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 +- 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)" + + +# 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 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 diff --git a/tests/test.yml b/tests/test.yml index ed7d3af..43eb2ac 100644 --- a/tests/test.yml +++ b/tests/test.yml @@ -13,222 +13,109 @@ # See the License for the specific language governing permissions and # limitations under the License. +# Setup the host +- include: common/test-prepare-keys.yml + +- include: common/test-prepare-host.yml + +# Temporary work around for issue with lxc_host where on second run after +# all iptables rules have been removed, the lxc_host role is not checking +# that the rules are defined and adding them back in if they are not. +- name: Add iptables rules back in + hosts: localhost + connection: local + gather_facts: yes + become: yes + + vars: + do_not_fail_lint: true + + tasks: + - name: Ensure lxc iptables rules + command: /usr/local/bin/lxc-system-manage iptables-create + when: do_not_fail_lint + +- include: common/test-prepare-containers.yml + - name: Test config_template hosts: localhost connection: local gather_facts: yes + tasks: - # 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" - - - 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 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 - - - name: Read test_extend.yml - slurp: - src: /tmp/test_extend.yml - register: extend_file - name: Read expected test_extend.yml slurp: src: "{{ playbook_dir }}/files/test_extend.yml.expected" register: extend_file_expected - - 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 - - name: Read test_no_extend.yml - slurp: - src: /tmp/test_no_extend.yml - register: no_extend_file - name: Read expected test_no_extend.yml slurp: src: "{{ playbook_dir }}/files/test_no_extend.yml.expected" register: no_extend_file_expected - - 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" - - 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)" - - - # 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 }}" - - name: Read test_content_no_overrides.json - slurp: - src: /tmp/test_content_no_overrides.json - register: content_no_overrides_file - name: Read expected test_content_no_overrides.json slurp: src: "{{ playbook_dir }}/files/test_content_no_overrides.json.expected" register: content_no_overrides_file_expected - - 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 + + - import_tasks: test-common-tasks.yml + + - import_tasks: test-common-tasks.yml + delegate_to: container1 + + handlers: + - name: test_ini check diff assert: that: - - "(content_no_overrides_file.content | b64decode | from_json) == (content_no_overrides_file_expected.content | b64decode | from_json)" + - test_ini.diff[0].prepared|from_json == diff_ini - # 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 + - name: test_with_content_ini check diff assert: that: - - "{{ test_ignore_none_type.content | b64decode | search('(?m)^india$') }}" - - "{{ test_ignore_none_type.content | b64decode | search('(?m)^juliett kilo$') }}" + - test_with_content_ini.diff[0].prepared|from_json == diff_ini - # 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 + - name: test_extend_yml check diff + assert: + that: + - test_extend_yml.diff[0].prepared|from_json == diff_extend_yml - - name: Read test.ini - slurp: - src: /tmp/test_with_comments.ini - register: ini_file - tags: test + - name: test_no_extend_yml check diff + assert: + that: + - test_no_extend_yml.diff[0].prepared|from_json == diff_no_extend_yml - - debug: - msg: "ini - {{ ini_file.content | b64decode }}" - - name: Validate output + - name: test_hostvars_yml check diff + assert: + that: + - test_hostvars_yml.diff[0].prepared|from_yaml == diff_hostvars_yml + + - name: test_content_no_overrides_json check diff + assert: + 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: - - "(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 is a comment')}}" - - "{{ ini_file.content | b64decode | search('# A default section comment\n# broken into multiple lines\n\\[DEFAULT\\]')}}" + - test_with_comments_ini.diff[0].prepared|from_json == diff_with_comments_ini - - 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: test_diff_ini check diff + tags: test + assert: + that: + - test_diff_ini.diff[0].prepared|from_json == diff_diff_ini - - 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: test_diff_remove_ini check diff + tags: test + assert: + that: + - test_diff_remove_ini.diff[0].prepared|from_json == diff_diff_remove_ini vars: test_config_ini_overrides: @@ -279,3 +166,198 @@ test2: 2 section1: setting2: 2 + test_diff_overrides: + section1: + 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: {} + diff_ini: + added: + DEFAULT: + new_key: "new_value" + 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: {} + diff_extend_yml: + added: + list_one: + - "one" + - "two" + - "three" + - "four" + - 4 + list_two: + - "one" + - "two" + changed: {} + removed: {} + diff_no_extend_yml: + added: + list_one: + - "four" + - 4 + list_two: + - "one" + - "two" + changed: {} + removed: {} + diff_hostvars_yml: + added: + list_one: + - "one" + - "two" + - "three" + list_two: + - "one" + - "two" + 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" + charlie: "echo" + foxtrot: + golf: "hotel" + changed: {} + removed: {} + diff_diff_ini: + added: + section3: + alfa: "bravo" + changed: + section1: + baz: + current_val: "baz" + new_val: "hotel" + removed: {} + diff_diff_remove_ini: + added: {} + changed: {} + removed: + section2: + foo: "bar" diff --git a/tox.ini b/tox.ini index dc1dd07..94a0216 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ commands = passenv = COMMON_TESTS_PATH HOME + USER http_proxy HTTP_PROXY https_proxy @@ -29,12 +30,8 @@ setenv = TEST_IDEMPOTENCE=false VIRTUAL_ENV={envdir} WORKING_DIR={toxinidir} + ANSIBLE_PARAMETERS=--diff ANSIBLE_ACTION_PLUGINS={toxinidir}/action - ANSIBLE_CALLBACK_PLUGINS={toxinidir}/callback - ANSIBLE_CONNECTION_PLUGINS={toxinidir}/connection - ANSIBLE_FILTER_PLUGINS={toxinidir}/filter - ANSIBLE_LOOKUP_PLUGINS={toxinidir}/lookup - ANSIBLE_STRATEGY_PLUGINS={toxinidir}/strategy ANSIBLE_LIBRARY={toxinidir}/library