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
This commit is contained in:
Michael Vollman 2018-07-13 07:33:40 -04:00
parent fb906fb061
commit b74d7acbf4
10 changed files with 724 additions and 227 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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: {}

View File

@ -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

View File

@ -0,0 +1,7 @@
[DEFAULT]
[section1]
baz = baz
[section2]
foo = bar

View File

@ -0,0 +1,6 @@
[DEFAULT]
[section1]
baz = baz
[section2]

259
tests/test-common-tasks.yml Normal file
View File

@ -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

View File

@ -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"

View File

@ -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