From e28e538c6ed7520ecef305c776eb6036aff42d06 Mon Sep 17 00:00:00 2001 From: Michel Blanc Date: Wed, 23 Jan 2013 21:39:34 +0100 Subject: [PATCH 1/2] Adds user-selectable hash merging support in vars Hash variables are currently overriden if they are redefined. This doesn't let the user refine hash entries or overriding selected keys, which can, for some, be a desirable feature. This patch let the user force hash merging by setting the hash_behaviour value to "merge" (without the quotes) in ansible.cfg However, by default, ansible behaves like it always did and if any value besides "merge" is used ("replace" is suggested in the example ansible.cfg file), it will also behave as always. --- examples/ansible.cfg | 10 ++++++++ lib/ansible/constants.py | 1 + .../inventory/vars_plugins/group_vars.py | 14 ++++++++--- lib/ansible/utils/__init__.py | 23 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/examples/ansible.cfg b/examples/ansible.cfg index 80c263e584d..0ba9957f642 100644 --- a/examples/ansible.cfg +++ b/examples/ansible.cfg @@ -76,6 +76,16 @@ remote_port=22 sudo_exe=sudo +# how to handle hash defined in several places +# hash can be merged, or replaced +# if you use replace, and have multiple hashes named 'x', the last defined +# will override the previously defined one +# if you use merge here, hash will cumulate their keys, but keys will still +# override each other +# replace is the default value, and is how ansible always handled hash variables +# +# hash_behaviour=replace + # if set, always use this private key file for authentication, same as if passing # --private-key to ansible or ansible-playbook diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py index 236c5c2b9f0..117b9a1e91a 100644 --- a/lib/ansible/constants.py +++ b/lib/ansible/constants.py @@ -92,6 +92,7 @@ DEFAULT_MANAGED_STR = get_config(p, DEFAULTS, 'ansible_managed', None, DEFAULT_SYSLOG_FACILITY = get_config(p, DEFAULTS, 'syslog_facility', 'ANSIBLE_SYSLOG_FACILITY', 'LOG_USER') DEFAULT_KEEP_REMOTE_FILES = get_config(p, DEFAULTS, 'keep_remote_files', 'ANSIBLE_KEEP_REMOTE_FILES', '0') DEFAULT_SUDO_EXE = get_config(p, DEFAULTS, 'sudo_exe', 'ANSIBLE_SUDO_EXE', 'sudo') +DEFAULT_HASH_BEHAVIOUR = get_config(p, DEFAULTS, 'hash_behaviour', 'ANSIBLE_HASH_BEHAVIOUR', 'replace') DEFAULT_ACTION_PLUGIN_PATH = shell_expand_path(get_config(p, DEFAULTS, 'action_plugins', None, '/usr/share/ansible_plugins/action_plugins')) DEFAULT_CALLBACK_PLUGIN_PATH = shell_expand_path(get_config(p, DEFAULTS, 'callback_plugins', None, '/usr/share/ansible_plugins/callback_plugins')) diff --git a/lib/ansible/inventory/vars_plugins/group_vars.py b/lib/ansible/inventory/vars_plugins/group_vars.py index e8ce2019bf2..e13271031ba 100644 --- a/lib/ansible/inventory/vars_plugins/group_vars.py +++ b/lib/ansible/inventory/vars_plugins/group_vars.py @@ -19,6 +19,7 @@ import os import glob from ansible import errors from ansible import utils +import ansible.constants as C class VarsModule(object): @@ -48,7 +49,11 @@ class VarsModule(object): data = utils.parse_yaml_from_file(path) if type(data) != dict: raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path) - results.update(data) + if C.DEFAULT_HASH_BEHAVIOUR == "merge": + # let data content override results if needed + results = utils.merge_hash(results, data) + else: + results.update(data) # load vars in playbook_dir/group_vars/name_of_host path = os.path.join(basedir, "host_vars/%s" % host.name) @@ -56,7 +61,10 @@ class VarsModule(object): data = utils.parse_yaml_from_file(path) if type(data) != dict: raise errors.AnsibleError("%s must be stored as a dictionary/hash" % path) - results.update(data) - + if C.DEFAULT_HASH_BEHAVIOUR == "merge": + # let data content override results if needed + results = utils.merge_hash(results, data) + else: + results.update(data) return results diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index c79a1a0fa40..5bf5cf7a2a2 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -19,6 +19,7 @@ import sys import os import shlex import yaml +import copy import optparse import operator from ansible import errors @@ -273,6 +274,28 @@ def parse_kv(args): options[k]=v return options +def merge_hash(a, b): + ''' merges hash b into a + this means that if b has key k, the resulting has will have a key k + which value comes from b + said differently, all key/value combination from b will override a's ''' + + # let's create a deep copy of a + result = copy.deepcopy(a) + # and iterate over b keys + for k, v in b.iteritems(): + if k in result and isinstance(result[k], dict): + # if this key is a hash and exists in a + # we recursively call ourselves with + # the key value of b + result[k] = merge_hash(result[k], v) + else: + # k is not in a, no need to merge b, we just deecopy + # or k is not a dictionnary, no need to merge b either, we just deecopy it + result[k] = copy.deepcopy(v) + # finally, return the resulting hash when we're done iterating keys + return result + def md5s(data): ''' Return MD5 hex digest of data. ''' From 8eb7d740b02571fdbfc3fae1f1aa99192882da27 Mon Sep 17 00:00:00 2001 From: Michel Blanc Date: Thu, 24 Jan 2013 00:53:12 +0100 Subject: [PATCH 2/2] Fixes hash merging No need for deep copy (vars are essentially immutable once loaded, there should not be any modifications) --- lib/ansible/utils/__init__.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/ansible/utils/__init__.py b/lib/ansible/utils/__init__.py index 5bf5cf7a2a2..139e1584e9c 100644 --- a/lib/ansible/utils/__init__.py +++ b/lib/ansible/utils/__init__.py @@ -280,21 +280,19 @@ def merge_hash(a, b): which value comes from b said differently, all key/value combination from b will override a's ''' - # let's create a deep copy of a - result = copy.deepcopy(a) # and iterate over b keys for k, v in b.iteritems(): - if k in result and isinstance(result[k], dict): + if k in a and isinstance(a[k], dict): # if this key is a hash and exists in a # we recursively call ourselves with # the key value of b - result[k] = merge_hash(result[k], v) + a[k] = merge_hash(a[k], v) else: # k is not in a, no need to merge b, we just deecopy # or k is not a dictionnary, no need to merge b either, we just deecopy it - result[k] = copy.deepcopy(v) + a[k] = v # finally, return the resulting hash when we're done iterating keys - return result + return a def md5s(data): ''' Return MD5 hex digest of data. '''