diff --git a/examples/ansible.cfg b/examples/ansible.cfg index 80c263e584..0ba9957f64 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 236c5c2b9f..117b9a1e91 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 e8ce2019bf..e13271031b 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 c79a1a0fa4..5bf5cf7a2a 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. '''