ansible nested variable lookup refactoring - ansible

I'm using this kind of ansible lookup, in order to load the content of a file into a variable :
- name: Prepare ignition for worker nodes
set_fact:
custom_attr: "{{ lookup('file', './files/ignition/{{ oc_cluster_name }}/worker.ign') | b64encode }}"
when: item.name.startswith('worker')
I know that we should avoid using nested variables (moustaches don't stack, right ?). This code is working indeed, but I'm not sure it's the correct way to write this.
Is there another way to do it ? I used to write in two separate "set_fact" blocks, which works as well, but it's not better (using temporary vars) :
- name: Prepare ignition for worker nodes
block:
- name: locate file for worker node
set_fact:
ignition_file: "./files/ignition/{{ oc_cluster_name }}/worker.ign"
- name: load file into fact for worker node
set_fact:
custom_attr: "{{ lookup('file', ignition_file) | b64encode }}"
when: item.name.startswith('worker')
What do you think ?
I'm trying to write nice code with best practices : using no temporary variable and respecting the way to nest interpolation of variables

Moustaches shouldn't be stacked because it's not necessary to do so. You're already in a Jinja expression so you just access variables by name without wrapping them in more delimiters.
- name: Prepare ignition for worker nodes
set_fact:
# Relative paths are looked for in `files/` first, so there's no need to specify it
custom_attr: "{{ lookup('file', 'ignition/' ~ oc_cluster_name ~ '/worker.ign') | b64encode }}"
when: item.name.startswith('worker')
You can also use a temporary variable without a separate set_fact, which can be helpful for breaking up complex expressions:
- name: Prepare ignition for worker nodes
set_fact:
custom_attr: "{{ lookup('file', ignition_file) | b64encode }}"
vars:
ignition_file: ignition/{{ oc_cluster_name }}/worker.ign
when: item.name.startswith('worker')

Q: "Write nice code."
A: Put the declarations into the vars. For example, into the group_vars/all
shell> tree .
.
├── ansible.cfg
├── files
│   └── ignition
│   └── cluster1
│   └── worker.ign
├── group_vars
│   └── all
├── hosts
└── pb.yml
4 directories, 5 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
inventory = $PWD/hosts
remote_tmp = ~/.ansible/tmp
retry_files_enabled = false
stdout_callback = yaml
shell> cat files/ignition/cluster1/worker.ign
test
shell> cat group_vars/all
oc_cluster_name: cluster1
ignition_file: "./files/ignition/{{ oc_cluster_name }}/worker.ign"
custom_attr: "{{ lookup('file', ignition_file)|b64encode }}"
shell> cat hosts
localhost
shell> cat pb.yml
- hosts: localhost
tasks:
- debug:
var: custom_attr|b64decode
shell> ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [debug] *********************************************************************************
ok: [localhost] =>
custom_attr|b64decode: test
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Related

Force Ansible to reevaluate contents of an imported playbook as a task after main playbook that does the importing has started

Please consider the following scenario. An Ansible playbook, that uses a serries of ansible.builtin.import_playbook module calls, to chain other ansible playbooks together. We'll call this playbook main.yaml
...
- name: Import Ansible Playbook A
ansible.builtin.import_playbook: playbook_a.yaml
- name: Import Ansible Playbook B
ansible.builtin.import_playbook: playbook_b.yaml
...
My playbook_a.yaml calls a bash script, that eventually makes some changes in playbook_b.yaml using sed. In this current iteration, once main.yaml is started, my understanding is it reads the contents of all the files, including those imported via the ansible.builtin.import_playbook module, at the start of the playbook run, so any changes introduced into of those files after the start are not considered and are ignored. Essentially it plays them as they were, and not as they have become during the play.
My question is, is there a way for me to force main.yaml to reevaluate the conets of playbook_b.yaml, perhaps by adding another task between the import of playbook_a.yaml and playbook_b.yaml that would accomplish this?
Obviously I can run playbook_a.yaml seperatly, before starting main.yaml to avoid this issue, but my hope is to contain it all in one play.
TIA
It's not possible to reread imported playbooks. Instead, you can modify included tasks in the imported playbooks. For example, given the tree
shell:> tree .
.
├── ansible.cfg
├── hosts
├── pb_a.yml
├── pb_b.yml
├── pb.yml
├── tasks
│   ├── a_tasks.yml
│   └── b_tasks.yml
└── templates
├── tasks_1.yml.j2
└── tasks_2.yml.j2
Create the playbooks
shell> cat pb.yml
- hosts: localhost
vars:
playbook_updates:
- {dest: tasks/a_tasks.yml, template: tasks_1.yml.j2}
- {dest: tasks/b_tasks.yml, template: tasks_2.yml.j2}
tasks:
- name: Update playbooks
template:
src: "{{ item.template }}"
dest: "{{ playbook_dir }}/{{ item.dest }}"
loop: "{{ playbook_updates }}"
- import_playbook: pb_a.yml
- import_playbook: pb_b.yml
shell> cat pb_a.yml
- hosts: localhost
tasks:
- include_tasks: tasks/a_tasks.yml
shell> cat pb_b.yml
- hosts: localhost
tasks:
- include_tasks: tasks/b_tasks.yml
The first play updates the tasks in the imported playbooks according to the list playbook_updates. Create the templates, e.g.
shell> cat templates/tasks_1.yml.j2
- debug:
msg: tasks 1
shell> cat templates/tasks_2.yml.j2
- debug:
msg: tasks 2
Then, the playbook gives
shell> ansible-playbook pb.yml
PLAY [localhost] ****************************************************************************
TASK [Update playbooks] *********************************************************************
changed: [localhost] => (item={'dest': 'tasks/a_tasks.yml', 'template': 'tasks_1.yml.j2'})
changed: [localhost] => (item={'dest': 'tasks/b_tasks.yml', 'template': 'tasks_2.yml.j2'})
PLAY [localhost] ****************************************************************************
TASK [include_tasks] ************************************************************************
included: /export/scratch/tmp7/test-100/tasks/a_tasks.yml for localhost
TASK [debug] ********************************************************************************
ok: [localhost] =>
msg: tasks 1
PLAY [localhost] ****************************************************************************
TASK [include_tasks] ************************************************************************
included: /export/scratch/tmp7/test-100/tasks/b_tasks.yml for localhost
TASK [debug] ********************************************************************************
ok: [localhost] =>
msg: tasks 2
PLAY RECAP **********************************************************************************
localhost: ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can modify the included tasks by modifying the templates and variables.
After some more testing I have come up with two solutions that accomplish what I was hoping to accomplish. One is more of a a hacky solution and the other more of a proper Ansible solution.
Hacky Solution
Back in the main.yaml, instead of importing playbook_b.yaml directly, we import a new playbook, let's call it playbook_c.yaml,
...
- name: Import Ansible Playbook A
ansible.builtin.import_playbook: playbook_a.yaml
- name: Import Ansible Playbook C
ansible.builtin.import_playbook: playbook_c.yaml
...
which in turn only contains the following task:
...
tasks:
1. name: Call Playbook B
ansible.builtin.shell: ansible-playbook playbook_b.yaml
args:
executable: /bin/bash
register: call_playbook_b
changed_when: call_playbook_b != 0
Since ansible will not know any information regading playbook_b.yaml until playbook_c.yaml calls it using a bash command of ansible-playbook playbook_b.yaml, any changes introduced into playbook_b.yaml by playbook_a.yaml will be respected at the time the playbook is called. This feels cheeky but it works, with one major drawback, we don't get to see the output of playbook_b.yaml. We only see the output of playbook_a.yaml and playbook_c.yaml when we run main.yaml
Proper Solution
We leave our main.yaml as is
...
- name: Import Ansible Playbook A
ansible.builtin.import_playbook: playbook_a.yaml
- name: Import Ansible Playbook B
ansible.builtin.import_playbook: playbook_B.yaml
...
but modify the bash script called by playbook_a.yaml to make the sed changes not into playbook_b.yaml directly, but into the inventory file described in ansible.cfg, essentially adding a variable to the host(s) that playbook_b.yaml is playing against, like a dynamic inventory file. Then we add a pre_tasks to playbook_b.yaml before the tasks section, like so
...
pre_tasks:
- name: Refresh Inventory File
ansible.builtin.meta: refresh_inventory
tasks:
...
forcing ansible to reread the inventory file taking into the account the new values of the variables that were seded by the bash script called by playbook_a.yaml. Afterwards the variables can easily be referenced in playbook_b.yaml and they will take the latest value introduced after the start of main.yaml and playbook_a.yaml.

Ansible pattern matching doesn't work as expected

I am unable to find why this simple pattern doesn't seem to match anything. I have two Ansible hosts as targets. This is my inventory file:
[web_Xubuntu]
192.168.160.128
[database_Fedora]
192.168.160.132
And this is what my YAML playbook looks like:
# Hosts: where our play will run and options it will run with
hosts: *Fedora
become: True
#gather_facts: False
# Vars: variables that will apply to the play, on all target systems
vars:
motd: "Welcome to Fedora Linux - Ansible Rocks\n"
# Tasks: the list of tasks that will be executed within the playbook
tasks:
- name: Configure a MOTD (message of the day)
copy:
content: "{{ motd }}"
dest: /etc/motd
notify: MOTD changed
# Handlers: the list of handlers that are executed as a notify key from a task
handlers:
- name: MOTD changed
debug:
msg: The MOTD was changed
On processing this playbook, Ansible reports the following error:
ERROR! We were unable to read either as JSON nor YAML, these are the errors we got from each:
JSON: Expecting value: line 1 column 1 (char 0)
Syntax Error while loading YAML.
found undefined alias
The offending line appears to be:
# Hosts: where our play will run and options it will run with
hosts: *Fedora
^ here
What is the right way to use a wildcard?
You can use the asterisk *( wildcard) with FQDN or IP only. For example,
192.0.*
*.example.com
*.com
See Patterns: targeting hosts and groups.
Use the inventory plugin constructed if you want to run all *Fedora groups. See
shell> ansible-doc -t inventory constructed
For example, given the tree
shell> tree .
.
├── ansible.cfg
├── inventory
│   ├── 01-hosts
│   └── 02-constructed.yml
└── pb.yml
1 directory, 4 files
the inventory
shell> cat inventory/01-hosts
[web_Xubuntu]
192.168.160.128
[database_Fedora]
192.168.160.132
[web_Fedora]
192.168.160.133
the contructed plugin
shell> cat inventory/02-constructed.yml
plugin: constructed
groups:
Fedora: group_names|select('regex', '^.*Fedora$')
Test the inventory
shell> ansible-inventory -i inventory --list --yaml
all:
children:
Fedora:
hosts:
192.168.160.132: {}
192.168.160.133: {}
database_Fedora:
hosts:
192.168.160.132: {}
ungrouped: {}
web_Fedora:
hosts:
192.168.160.133: {}
web_Xubuntu:
hosts:
192.168.160.128: {}
Then, test the playbook
shell> cat pb.yml
- hosts: Fedora
gather_facts: false
tasks:
- debug:
var: inventory_hostname
gives
shell> ansible-playbook -i inventory pb.yml
PLAY [Fedora] *********************************************************************************
TASK [debug] **********************************************************************************
ok: [192.168.160.132] =>
inventory_hostname: 192.168.160.132
ok: [192.168.160.133] =>
inventory_hostname: 192.168.160.133
PLAY RECAP ************************************************************************************
192.168.160.132: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
192.168.160.133: ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible: Find string in array and return string from array

I am writing up a playbook that takes a user input to find out if the file exists in the directory.
This is what I have so far
- name: Encrypt file
hosts: localhost
connection: local
vars:
working_directory: "{{ playbook_dir }}"
enc_files: []
my_file: shared_config
tasks:
- name: Get all Decrypted .yaml files
find:
paths: "{{ working_directory }}"
patterns: '*.yaml'
recurse: yes
excludes: "*.enc.yaml,decrypt.yaml,encrypt_all.yaml,encrypt_file.yaml"
register: files
- name: Add Decrypted files to Array
set_fact:
enc_files: "{{ enc_files + [item.path | basename] }}"
loop: "{{ files.files }}"
no_log: true
- debug:
msg: "{{ enc_files }}"
when: '"{{ my_file | lower }}" in "{{ enc_files | lower }}"'
What I can't seem to get to work is that it it finds if the file by name, not extension exists. If it does, I want to return the file with extension to do things with it.
Here is my current tree:
├── README.md
├── database
│   └── postgres_config.enc.yaml
├── decrypt.yaml
├── encrypt_all.yaml
├── encrypt_file.yaml
├── infra
│   ├── infra_config.enc.yaml
│   └── infra_config.yaml
├── middleware
│   ├── middleware_config.enc.yaml
│   └── middleware_config.yaml
├── services
│   ├── log_service_config.enc.yaml
│   ├── log_service_config.yaml
│
├── shared
│   ├── shared_config.enc.yaml
│   └── shared_config.yaml
What I want to do is have the user input either shared_config or shared_config.yaml and return shared_config.yaml so I can encrypt that file. I am also trying to figure out a way they can pass shared config in their input (as well as any of the other possible inputs, but I can try to figure that out on my own later).
I think I wouldn't approach it this way (e.g. list all files and then find if the user input matches a files).
I would probably rather approach it the other way around: get the input of the user and then assert that the corresponding file does exists.
Then you are just left with playing with Jinja filter and Python string manipulation to transform the user input in the expected path.
Below is a proposed playbook, with upside and pitfalls.
Upside:
It will work with multiple chunk of path separated by spaces some path will give some/some_path.yaml
It should be robust enough to correct erroneous multiple spaces: some path will give some/some_path.yaml
It also accepts underscores some_path will give some/some_path.yaml
It should be robust enough to correct erroneous multiple underscore: some___path will give some/some_path.yaml
As well as a mix of both underscore and spaces some path_here multispace___multiunderscore will give some/some_path_here_multispace_multiunderscore.yaml
It works with or without extension specified in the user input
It should give a bit of resilience against a transversal path attack
Pitfall:
But it won't cope with a space between the last word and the extension, as it will bring an underscore too much: some path .yaml will give some/some_path_.yaml
And, so, here is the said playbook:
- hosts: all
gather_facts: no
vars_prompt:
- name: file_name
prompt: Which decrypted file do you need?
private: no
pre_tasks:
- set_fact:
fqn: "{{ chunk.0 ~ '/' ~ chunk | join('_') if chunk | length > 1 else chunk | join('_') }}"
vars:
chunk: "{{ ((file if file.endswith('.yaml') else file ~ '.yaml') | replace('_',' ')).split() }}"
file: "{{ file_name | trim }}"
- assert:
that:
- fqn is file
- not fqn.startswith('.') # I am just trying to limit transversal path attack here
- not fqn.startswith('/') # I am just trying to limit transversal path attack here
- "'enc' != fqn.split('.')[-2]" # this one is a protection against accessing the encrypted files
msg: "{{ fqn }} is not a file"
tasks:
- debug:
msg: "Now, do whatever you like best with the file `{{ fqn | basename }}` at `{{ fqn }}` because I am sure it exists"
Here is two examples of running it:
With a correct user input:
Which decrypted file do you need?: middleware config
PLAY [all] *********************************************************************
TASK [set_fact] ****************************************************************
ok: [localhost]
TASK [assert] ******************************************************************
ok: [localhost] => changed=false
msg: All assertions passed
TASK [debug] *******************************************************************
ok: [localhost] =>
msg: Now, do whatever you like best with the file `middleware_config.yaml` at `middleware/middleware_config.yaml` because I am sure it exists
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With an erroneous user input:
Which decrypted file do you need?: middleware config
PLAY [all] *********************************************************************
TASK [set_fact] ****************************************************************
ok: [localhost]
TASK [assert] ******************************************************************
fatal: [localhost]: FAILED! => changed=false
assertion: fqn is file
evaluated_to: false
msg: fake/fake_config.yaml is not a file
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0

Playbook host_vars

Is it possible to set variables for specific hosts in Ansible in the playbook itself, using the the global vars?
So the playbook would be configured something like this:
---
- hosts:
- host-1
- host-2
vars:
host-1: # < - set vars for host-1 specifically
a_var: yes
host-2: # < - set vars for host-2 specifically
a_var: no
I know I using either group_vars, host_vars, an inventory file, or set_fact during runtime is possible, but this is not what I want.
The docs describe "playbook host_vars", but I haven't figured out how that is configured.
What you are referring to is not really in the playbook, per se. But it can be in the directory structure next to the playbook itself.
This is further explained in Organizing host and group variables.
Although you can store variables in the main inventory file, storing separate host and group variables files may help you organize your variable values more easily. Host and group variable files must use YAML syntax. Valid file extensions include ‘.yml’, ‘.yaml’, ‘.json’, or no file extension. See YAML Syntax if you are new to YAML.
Ansible loads host and group variable files by searching paths relative to the inventory file or the playbook file.
Source: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#organizing-host-and-group-variables, emphasis, mine
So, what you can have is this:
.
├── host_vars
│ └── localhost.yml
└── play.yml
Where localhost.yml matches the name of the host we want to target and would contain something like:
foo: bar
And the file play.yml would be the playbook:
- hosts: all
gather_facts: no
tasks:
- debug:
var: foo
Then running it would give the expected:
PLAY [all] **********************************************************************************************************
TASK [debug] ********************************************************************************************************
ok: [localhost] => {
"foo": "bar"
}
PLAY RECAP **********************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Q: "Set variables for specific hosts in Ansible in the playbook itself, using the global vars."
A: Put the host-specific variables into a global dictionary, e.g. my_hostvars. (De facto, create your own hostvars). For example
- hosts: host-1,host-2
vars:
my_hostvars:
host-1: # < - set vars for host-1 specifically
a_var: yes
host-2: # < - set vars for host-2 specifically
a_var: no
tasks:
- debug:
msg: "{{ my_hostvars[inventory_hostname]['a_var'] }}"
gives
ok: [host-1] =>
msg: true
ok: [host-2] =>
msg: false
It's up to you where you declare the dictionary and how you reference it in the playbook. See Variable precedence: Where should I put a variable?.
Put the variables into your own facts to simplify the access. For example
- set_fact:
my_facts: "{{ my_hostvars[inventory_hostname] }}"
- debug:
var: my_facts.a_var
give
ok: [host-1] =>
my_facts.a_var: true
ok: [host-2] =>
my_facts.a_var: false
You can simplify the access further by setting the variables if needed. (For example, to avoid rewriting a code already using the variables).
- set_fact:
a_var: "{{ my_facts.a_var }}"
b_var: "{{ my_facts.b_var }}"
c_var: "{{ my_facts.c_var }}"
You can use it to set or customize (precedence 19.) the default values if needed. For example,
- set_fact:
a_var: "{{ my_facts.a_var|default('a') }}"
b_var: "{{ my_facts.b_var }}"
c_var: "{{ my_facts.c_var }}"

Ansible : host in multiple groups

I have a host in 2 groups : pc and Servers
I have 2 group_vars (pc and servers) with, in each the file packages.yml
These files define the list of packages to be installed on pc hosts and on servers hosts
I have a role to install default package
The problem is : only the group_vars/pc/packages.yml is take into account by the role task, packages from group_vars/servers/packages.yml are not installed
Of course what I want is installation of packages defined for pc and servers
I do not know if it is a bug or a feature ...
Thanks for your help
here is the configuration :
# file: production
[pc]
armen
kerbel
kerzo
[servers]
kerbel
---
# packages on servers
packages:
- lftp
- mercurial
---
# packages on pc
packages:
- keepassx
- lm-sensors
- hddtemp
It's not a bug. According to the docs about variable precedence, you shouldn't define a variable in multiple places and try to keep it simple. Michael DeHaan (Ansible's lead dev) responded to a similar question on this topic:
Generally I find the purpose of plays though to bind hosts to roles, so the individual roles should contain the package lists.
I would use roles as it's a bit cleaner IMO.
If you really want (and this is NOT the recommended way), you can set the hash_behaviour option in ansible.cfg:
[defaults]
hash_behaviour = merge
This will cause the merging of two values when a hash (dict) is redefined, instead of replacing the old value with the new one. This does NOT work on lists, though, so you'll need to create a hash of lists, like:
group_vars/all/package.yml:
packages:
all: [pkg1, pkg2]
group_vars/servers/package.yml:
packages:
servers: [pkg3, pkg4]
Looping though that in the playbook is a bit more complex though.
If you want to use such scheme. You should set the hash_behaviour option in ansible.cfg:
[defaults]
hash_behaviour = merge
In addition, you have to use dictionaries instead of lists. To prevent duplicates I recommend to use names as keys, for example:
group_vars/servers/packages.yml:
packages:
package_name1:
package_name2:
group_vars/pc/packages.yml:
packages:
package_name3:
package_name4:
And in a playbook task (| default({}) - for an absent "package" variable case):
- name: install host packages
yum: name={{ item.key }} state=latest
with_dict: packages | default({})
Create a dictionary of the variables per group and merge the lists on your own. For example, create a project for testing
shell> tree .
.
├── ansible.cfg
├── group_dict_create.yml
├── group_vars
│   ├── all
│   │   └── group_dict_vars.yml
│   ├── pc
│   │   └── packages.yml
│   └── servers
│   └── packages.yml
├── hosts
└── pb.yml
4 directories, 7 files
shell> cat ansible.cfg
[defaults]
gathering = explicit
collections_path = $HOME/.local/lib/python3.9/site-packages/
inventory = $PWD/hosts
roles_path = $PWD/roles
retry_files_enabled = false
stdout_callback = yaml
shell> cat hosts
[pc]
armen
kerbel
kerzo
[servers]
kernel
shell> cat group_vars/pc/packages.yml
packages:
- keepassx
- lm-sensors
- hddtemp
shell> cat group_vars/servers/packages.yml
packages:
- lftp
- mercurial
shell> cat pb.yml
- hosts: armen,kerbel,kerzo
pre_tasks:
- import_tasks: group_dict_create.yml
tasks:
- debug:
var: my_packages
Declare variables in group_vars/all/group_dict_vars.yml
shell> cat group_vars/all/group_dict_vars.yml
group_vars_dir: "{{ inventory_dir }}/group_vars"
group_names_all: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'group_names')|
flatten|unique }}"
group_dict_str: |
{% for group in group_names_all %}
{{ group }}: {{ lookup('vars', 'groupvars_' ~ group) }}
{% endfor %}
_group_dict: "{{ group_dict_str|from_yaml }}"
my_packages: "{{ group_names|map('extract', group_dict, 'packages')|
flatten|unique }}"
group_vars_dir: The directory group_vars can be used either in the directory where the inventory or the playbook comes from. In this example, these directories are identical and we set it to inventory_dir. In the loop, the task include_vars will read all YAML and JSON files from group_vars/<group> and will store the variables in the dictionary groupvars_<group>, where <group> are items of group_names_all.
group_names_all: This is a list of all groups the hosts are members of. See group_names
group_dict_str: Create the string with the YAML structure of the dictionary
_group_dict: Convert the string to YAML
my_packages: Merge the lists of packages from the groups the host is a member of. If needed, use this variable as a pattern of how to merge other variables.
Create a block of tasks that creates the dictionary and writes the file
shell> cat group_dict_create.yml
- name: Create dictionary group_dict in group_vars/all/group_dict.yml
block:
- name: Create directory group_vars/all
file:
state: directory
path: "{{ group_vars_dir }}/all"
- include_vars:
dir: "{{ group_vars_dir }}/{{ item }}"
name: "groupvars_{{ item }}"
loop: "{{ group_names_all }}"
- debug:
var: _group_dict
when: debug|d(false)|bool
- name: Write group_dict to group_vars/all/group_dict.yml
copy:
dest: "{{ group_vars_dir }}/all/group_dict.yml"
content: |
group_dict:
{{ _group_dict|to_nice_yaml(indent=2)|indent(2) }}
- include_vars:
file: "{{ group_vars_dir }}/all/group_dict.yml"
delegate_to: localhost
run_once: true
when: group_dict is not defined or group_dict_refresh|d(false)|bool
If the dictionary group_dict does not exist (the file group_vars/all/group_dict.yml has not been created yet) it will create the dictionary, write it to the file group_vars/all/group_dict.yml, and include it in the play. You can refresh group_dict by setting group_dict_refresh=true if you change the variables in group_vars/<group>.
shell> cat group_vars/all/group_dict.yml
group_dict:
pc:
packages:
- keepassx
- lm-sensors
- hddtemp
servers:
packages:
- lftp
- mercurial
The results, stored in the variable my_packages, are merged lists of packages by groups
TASK [debug] *********************************************************
ok: [kerbel] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
- lftp
- mercurial
ok: [armen] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
ok: [kerzo] =>
my_packages:
- keepassx
- lm-sensors
- hddtemp
Notes:
Best practice is running group_dict_create.yml separately for all hosts and letting other playbooks use created group_vars/all/group_dict.yml
The framework described here is idempotent.
The framework should be easily extendable to other use cases of merging variables by groups.
Example. Add variable users to group_vars/<group>
shell> cat group_vars/pc/users.yml
users:
- alice
- bob
shell> cat group_vars/servers/users.yml
users:
- carol
- dave
, and add variable my_users to group_vars/all/group_dict_vars.yml
my_users: "{{ group_names|map('extract', group_dict, 'users')|flatten|unique }}"
Refresh the dictionary group_dict for all hosts
shell> ansible-playbook pb.yml -l all -e group_dict_refresh=true
gives
ok: [armen] =>
my_users:
- alice
- bob
ok: [kerbel] =>
my_users:
- alice
- bob
- carol
- dave
ok: [kerzo] =>
my_users:
- alice
- bob

Resources