Skip to content

Instantly share code, notes, and snippets.

@udondan
Last active June 15, 2023 17:57
Show Gist options
  • Save udondan/b730206056a018cf9838 to your computer and use it in GitHub Desktop.
Save udondan/b730206056a018cf9838 to your computer and use it in GitHub Desktop.
Ansible action plugin "include_vars_merged". Same as "include_vars" but merges hashes instead of overriding

Why?

Current version of Ansible (1.7.1) does not merge hashes in the include_vars task even if told so via hash_behaviour = merge in your ansible.cfg. ansible/ansible#9116

This action plugin will merge hashes no matter how you have configured your hash_behaviour.

How to setup:

Save include_vars_merged.py to library/plugins/action/include_vars_merged.py

Save include_vars_merged to library/custom/include_vars_merged

The library folder needs to be on the same level as your playbook

Why do you buffer the content in the runner object?

Facts are merged in the runner and not in the plugin. You can access and modify those facts in the plugin when you call include_vars_merged for each file in a separate task, like so:

- include_vars_merged: /path/to/file-A.yml

- include_vars_merged: /path/to/file-B.yml

Though when called in a loop (e.g. with) each task is executed in parallel and at execution time these facts are not yet available. You can't merge what you can't access. Therefore the facts are additionally buffered in the parents runner object.

- include_vars_merged: /path/to/{{ item }}
  with:
    - file-A.yml
    - file-B.yml
# -*- mode: python -*-
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
DOCUMENTATION = '''
---
author: Daniel Schroeder
module: include_vars_merged
short_description: Load and merge variables from multiple files, dynamically within a task.
description:
- Loads variables from multiple YAML files dynamically during task runtime.
options:
free-form:
description:
- The file name from which variables should be loaded, if called from a role it will look for
the file in vars/ subdirectory of the role, otherwise the path would be relative to playbook. An absolute path can also be provided.
required: true
'''
EXAMPLES = """
# Conditionally decide to load in variables when x is 0, otherwise do not.
- include_vars_merged: contingency_plan.yml
when: x == 0
# Load a variable file based on the OS type, or a default if not found.
- include_vars_merged: "{{ item }}"
with_first_found:
- "{{ ansible_distribution }}.yml"
- "{{ ansible_os_family }}.yml"
- "default.yml"
"""
import os
from ansible.utils import template
from ansible import utils
from ansible import errors
from ansible.runner.return_data import ReturnData
class ActionModule(object):
TRANSFERS_FILES = False
def __init__(self, runner):
self.runner = runner
def run(self, conn, tmp, module_name, module_args, inject, complex_args=None, **kwargs):
if not module_args:
result = dict(failed=True, msg="No source file given")
return ReturnData(conn=conn, comm_ok=True, result=result)
source = template.template(self.runner.basedir, module_args, inject)
if '_original_file' in inject:
source = utils.path_dwim_relative(inject['_original_file'], 'vars', source, self.runner.basedir)
else:
source = utils.path_dwim(self.runner.basedir, source)
if os.path.exists(source):
data = utils.parse_yaml_from_file(source, vault_password=self.runner.vault_pass)
if data and type(data) != dict:
raise errors.AnsibleError("%s must be stored as a dictionary/hash" % source)
elif data is None:
data = {}
else:
data = utils.merge_hash(conn.runner.setup_cache[conn.host], data)
if not hasattr(conn.runner, 'mergeBuffer'):
conn.runner.mergeBuffer = {}
if conn.host in conn.runner.mergeBuffer:
data = utils.merge_hash(conn.runner.mergeBuffer[conn.host], data)
conn.runner.mergeBuffer[conn.host] = data
result = dict(ansible_facts=data)
return ReturnData(conn=conn, comm_ok=True, result=result)
else:
result = dict(failed=True, msg="Source file not found.", file=source)
return ReturnData(conn=conn, comm_ok=True, result=result)
@alexandrem
Copy link

Thanks for that plugin, very useful!

I have fixed a small bug in this version where it was impossible to merge variables for host aliases.

You can take a look at my fork here:
https://gist.github.com/alexandrem/50c42da4345c4decef1c

@ozbillwang
Copy link

can we have it in seperate git repository and make it as formal plugin, with that, others can update it with fork and pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment