from StringIO import StringIO
except ImportError:
from io import StringIO
+import base64
import json
import os
import pwd
comments.append('')
continue
- if line[0] in '#;':
- comments.append(line)
+ if line.lstrip()[0] in '#;':
+ comments.append(line.lstrip())
continue
if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
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'}
+ >>> dc = DictCompare(base_dict, new_dict)
+ >>> dc.added()
+ ... ['test4']
+ >>> dc.removed()
+ ... ['test2']
+ >>> dc.changed()
+ ... ['test1']
+ >>> dc.get_changes()
+ ... {'added':
+ ... {'test4': 'val3'},
+ ... '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
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
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.
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()
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,
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(
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.
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(
module_args=new_module_args,
task_vars=task_vars
)
+ copy_changed = rc.get('changed')
+ if not copy_changed:
+ 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