SubString in Ansible and/or Jinja2 - ansible

I'm trying to unmout all mountpoints, excepted if they are part of the current list:
excluded: ['home', 'cdrom', 'tmpfs', 'sys', 'run', 'dev', 'root']
Sample fstab only devices:
/dev/mapper/vgroot-local_home
devtmpfs
tmpfs
/dev/mapper/vgroot-local_home should be excluded from unmounting because the substring home is present on the array and the same for devtmpfs substring tmpfs. For tmpfs we have a perfect match. The goal is to check against devices.
After checking all Ansible filters and the Jinja2 documentation, I didn't find a solution to this problem. All Ansible facts are collected.
- name: Ensure the mountpoint is on the excluded list
ansible.posix.mount:
path: '{{ mount.device }}'
state: unmounted
when: {{ ??? }}
with_items: '{{ ??? }}'
become: true
tags: mountpoints

To test if a string contains a substring in Jinja, we use the in test, much like Python:
"somestring" in somevariable
In your case, you want to check if a given string contains any substring from the excluded list. Conceptually, what we want is something like the Python expression
if any(x in mount.device for x in excluded)
Using Jinja filters, we need to reverse our logic a little bit. We
can use the select filter to get a list of strings from the
excluded list that are contained in a given target string (such as
mount.device) like this:
excluded|select('in', item)
If item matches anything in the excluded list, the above
expression will result in a non-empty list (which evaluates to true
when used in a boolean context).
Used in a playbook, it would look like this:
- hosts: localhost
gather_facts: false
vars:
excluded: ['home', 'cdrom', 'tmpfs', 'sys', 'run', 'dev', 'root']
mounts:
- /dev/mapper/vgroot-local_home
- devtmpfs
- tmpfs
- something/else
tasks:
- debug:
msg: "unmount {{ item }}"
when: not excluded|select('in', item)
loop: "{{ mounts }}"
The above playbook produces as output:
TASK [debug] *******************************************************************
skipping: [localhost] => (item=/dev/mapper/vgroot-local_home)
skipping: [localhost] => (item=devtmpfs)
skipping: [localhost] => (item=tmpfs)
ok: [localhost] => (item=something/else) => {
"msg": "unmount something/else"
}
That is, it skips the task when the current loop item contains a
substring from the excluded list.
Assuming that your goal is "unmount all filesystems except those for
which the device name contains a substring from the excluded list",
you might write:
- name: Unmount filesystems that aren't excluded
ansible.posix.mount:
path: '{{ mount.device }}'
state: unmounted
when: not excluded|select('in', item.device)
loop: "{{ ansible_mounts }}"
become: true
tags: mountpoints

Iterate basename if you don't want to exclude the items of mounts because of matching the path, e.g. if you don't want to exclude /dev/mapper/vgroot-local_home because of dev in the excluded list
- debug:
msg: "Unmount {{ item }}"
loop: "{{ mounts|map('basename') }}"
when: not excluded|select('in', item)

Related

Ansible Looping with conditional

I'm attempting to loop over a list ( I think) with a conditional which also is a list. According to the documentation the conditional should be re-evaluated with each loop. However, I'm finding this not the case.
Here are my two lists which are being set via a set_fact task:
image_id:
- 4bc0467496b6c7a60543069c570ef0e1be4565d25cb2bc7d524600a5fe0d3b8f
- dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6
- e911c149c0ca46a11a8b6eb604439e972685ec25abfde07eb1cdb272a9c0d1a9
- eb40451959b6c5f4aebb2b687a589a58370faab9b15faa43c0aea8d711155b9e
container_id:
- dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6
Then, I'm attempting to loop through the image_id list and remove any images. However, don't remove the items listed in the container_id list.
- name: remove images no longer used
containers.podman.podman_image:
name: "{{ item }}"
state: absent
loop: "{{ image_id }}"
when: container_id != image_id
I'm expecting all images to be remove except for the one listed in the container_id variable. Here is my output from the task above:
TASK [docker-host : remove images no longer used] *********************************************************************************************************************************************************
changed: [dtest05] => (item=4bc0467496b6c7a60543069c570ef0e1be4565d25cb2bc7d524600a5fe0d3b8f)
failed: [dtest05] (item=dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6) => changed=false
ansible_loop_var: item
item: dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6
msg: |-
Failed to remove image with id dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6. Error: Image used by 099e729245fb339753cc7ad5e86480965faf2f638dbeb7f535336a532af34de0: image is in use by a container
changed: [dtest05] => (item=e911c149c0ca46a11a8b6eb604439e972685ec25abfde07eb1cdb272a9c0d1a9)
changed: [dtest05] => (item=eb40451959b6c5f4aebb2b687a589a58370faab9b15faa43c0aea8d711155b9e)
I was expecting the failed task to be skipped. If I re-run the task, I do see the task being skipped.
TASK [docker-host : Setting Facts for Image IDs] **********************************************************************************************************************************************************
ok: [dtest05]
TASK [docker-host : debug image_id] ***********************************************************************************************************************************************************************
ok: [dtest05] =>
image_id:
- dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6
TASK [docker-host : debug container_id] *******************************************************************************************************************************************************************
ok: [dtest05] =>
container_id:
- dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6
TASK [docker-host : remove images no longer used] *********************************************************************************************************************************************************
skipping: [dtest05] => (item=dbcefaa52e7009f5d9b6179a256e70890d29148add0f77741ca4550ba2e2ffa6)
Am I not understanding the documentation correctly? I'm thinking that my first run should not fail but have an skipped item.
The expression container_id != image_id doesn't make any sense. You're comparing the same two things for every iteration of the loop, and because the two variables are two unequal lists the comparison will always be false.
It looks like you're trying to check if item is contained in the container_id list, which would look something like:
- name: remove images no longer used
containers.podman.podman_image:
name: "{{ item }}"
state: absent
loop: "{{ image_id }}"
when: item not in container_id
Note that you can achieve the same thing with less work by running podman image prune -af (perhaps via command task).
Use the filter difference. For example, the playbook below
- hosts: localhost
vars:
image_id:
- 4bc046
- dbcefa
- e911c1
- eb4045
container_id:
- dbcefa
tasks:
- debug:
msg: "{{ item }}"
loop: "{{ image_id|difference(container_id) }}"
gives (abridged)
msg: 4bc046
msg: e911c1
msg: eb4045

Ansible use Find Module on Registered Variable

My goal is to create a registered variable that I can use to search through and register other variables based on it. Mostly it is a find module that is searching for files which then, based upon the filename, should be registered in other variables. Here is an example of what I have but it is not filtering out the variables properly:
- name: Register all files
find:
paths: ./files/
patterns: '*.pkg,*.dmg'
file_type: file
register: installers
- name: Find Slack Installers
find:
paths: "{{ item }}"
patterns: "*slack*"
loop: "{{ installers.files|map(attribute='path')|map('basename')|list }}"
register: slack_installers
- debug:
msg: "{{ slack_installers }}"
This then outputs the following:
TASK [Find Slack Installers] *******************************************************************************************************************************
ok: [localhost] => (item=slack.19.5.dmg)
ok: [localhost] => (item=slack.19.6.pkg)
ok: [localhost] => (item=box.1.2.3.dmg)
(ignore the versions of things, these are just fake names/versions but I'm just testing the find functionality)
As you can see the "Find Slack Installers" task is just registering everything within the "installers" variable rather than actually finding only things with the slack pattern.
EDIT FOR extended question:
set_fact:
"{{ item.item|basename|lower }}_installers": "{{ installers.files|map(attribute='path')|map('basename')|list|select('search', '{{ item.item|basename|lower }}') }}"
loop: "{{ selected_installers.results }}"
loop_control:
label: "{{ item.item|basename }}"
when: item.user_input == 'y' and
item.item == ( item.item|basename )
Using the find module twice in a row does not make sense.
The find module is to find files in a folder and its subfolders, but not to filter a list (even if file names are in this list), as you tried here. Have a look at the examples for find in the Ansible docs.
However, you have two possibilities.
Using find with using the search word slack in pattern:
Add the search word slack directly as pattern in the find module: *slack*.pkg,*slack*.dmg.
- name: Register all files
find:
paths: ./files/
patterns: '*slack*.pkg,*slack*.dmg'
file_type: file
register: installers
- debug:
msg: "{{ installers.files|map(attribute='path')|map('basename')|list }}"
Result:
TASK [Register all files] **************
ok: [localhost]
TASK [debug] ***************************
ok: [localhost] => {
"msg": [
"slack.19.5.dmg",
"slack.19.6.pkg"
]
}
Using find to search only for the packages, then filtering your list:
Using select('regex', 'slack') you can filter the list by specific words in regex syntax, in your case specifying slack is enough.
- name: Register all files
find:
paths: ./files/
patterns: '*.pkg,*.dmg'
file_type: file
register: installers
- debug:
msg: "{{ installers.files|map(attribute='path')|map('basename')|list|select('regex', 'slack') }}"
Result:
TASK [Register all files] **************
ok: [localhost]
TASK [debug] ***************************
ok: [localhost] => {
"msg": [
"slack.19.5.dmg",
"slack.19.6.pkg"
]
}
EDIT: Extended Question
Nested jinja expressions are not allowed and not required.
If you want to specify a variable or a filter expression, just write it without using another jinja expression with {{ and }}.
Try the following select:
select('search', item.item|basename|lower )
Tip
If you use several operators and are not sure which operator binds stronger, you can also use parentheses like in mathematics.
The parenthesis should not be necessary at this point, but it does not interfere.
select('search', (item.item|basename|lower) )
But if you want to get e.g. the second element (index=1) of your list, then the term looks like this:
( installers.files|map(attribute='path')|map('basename')|list )[1]
At this point, the parentheses are important so that the inner expression is evaluated first, and then the corresponding element can be accessed.

Ansible Fact - Accessing the additional facts

I am doing a little research on Ansible facts. I am accessing facts in the debug module using something like: ansible_facts['mounts']. I noticed there are additional facts within the dictionary like "fstype" etc. However, when I try to access this like so ansible_facts['mounts']['fstype'] but I seems this is not the proper way to access this. I was testing a conditional with when to check for the fstype. Anyone know how to access this?
With everyone's help, here is the solution I came up with to assist with my research:
---
- name: Conditionals test
hosts: dev
tasks:
- name: Update the kernel if suff space
package:
name: kernel
state: latest
loop: "{{ ansible_facts['mounts'] }}"
when: item.mount == "/boot" and item.size_available > 20000000
I am looping through the ansible_facts list and checking for /boot and measuring the size. Thank you everyone!
To get a better understanding of the data structure of Ansible facts one can use the following example and test playbook.
---
- hosts: localhost
become: false
gather_facts: true
tasks:
- name: Show amount of mounts
debug:
msg:
- "{{ ansible_facts.mounts | type_debug }}"
- "{{ ansible_facts.mounts | length }}"
- name: Show mount type (sequence)
debug:
msg: "{{ ansible_facts.mounts[item | int].fstype }}"
loop: "{{ range(0, ansible_facts.mounts | length) | list }}"
- name: Show mount type (fact list)
debug:
msg: "{{ item.fstype }}"
loop: "{{ ansible_facts.mounts }}"
loop_control:
extended: true
label: "{{ ansible_loop.index0 }}"
it shows
That ansible_facts.mounts is mostly a list, how to get the data type and how to get the length of it
How to loop over a fact list with_sequence numbers
How to access the list element by index number (ansible_facts.mounts[item | int].fstype)
How to loop over a fact list and control the label
and resulting into an output of
TASK [Show mount type (sequence)] ******
ok: [localhost] => (item=0) =>
msg: ext4
ok: [localhost] => (item=1) =>
msg: ext4
ok: [localhost] => (item=2) =>
msg: ext4
ok: [localhost] => (item=3) =>
msg: vfat
TASK [Show mount type (fact list)] ******
ok: [localhost] => (item=0) =>
msg: ext4
ok: [localhost] => (item=1) =>
msg: ext4
ok: [localhost] => (item=2) =>
msg: ext4
ok: [localhost] => (item=3) =>
msg: vfat
Further Documentation
Ansible facts
with_sequence
Adding control to loops
I think your issue here is that you are trying to acces a list of mounts, you need to get one item from that list and get his fstype something like:
ansible_facts['mounts'][0]['fstype']
Or using a loop
- name: Print fstypes
debug:
var: "{{ item }}.fstype"
loop: "{{ ansible_facts.mounts }}"
or you can do this...
- name: Update the kernel if suff space
package:
name: kernel
state: latest
when: ansible_facts['mounts']|selectattr('mount','equalto','/boot')|map(attribute='size_available')|first > 20000000
rather than looping through the mounts at all.

Ansible when all item in loop is true

Let say, I have this directory structure:
# ls /root/ansible_test/
one two
And the playbooks looks like this:
- name: gathering all dirs
stat:
path: /root/ansible_test/{{ item }}/
register: dir_check
changed_when: false
check_mode: no
loop:
- "one"
- "two"
- "three"
- name: check all of the dirs are created
set_fact:
all_dirs_created: true
when: item.stat.exists == true
loop: "{{ dir_check.results }}"
- debug:
msg: "Not all dirs are created!"
when: all_dirs_created is not defined
My problem is that the "one" and "two" dirs are created, so the fact will be defined because if one dir exists, then the loop will return true. I also tried opposite and checked item.stat.exists == false but if one dir is not created (three) then fact will be created also.
I would like to play the task set_fact only if all of the item in the loop is true or if one of them is false. How do I achieve this in this case?
Q: set_fact only if all of the items in the loop are true or if one of them is false
A: Count the items. For example
- set_fact:
dirs_missing: "{{ _all|int - _exist|int }}"
vars:
_all: "{{ dir_check.results|length }}"
_exist: "{{ dir_check.results|
map(attribute='stat.exists')|
select|length }}"
gives (in your case)
dirs_missing: '1'
Now, you can set whatever you want, e.g.
- name: check all of the dirs are created
set_fact:
all_dirs_created: true
when: dirs_missing|int == 0
- debug:
msg: "Not all dirs are created!
(Exactly {{ dirs_missing }} missing.)"
when: all_dirs_created is not defined
gives
TASK [check all of the dirs are created] ********************************
skipping: [localhost]
TASK [debug] ************************************************************
ok: [localhost] =>
msg: Not all dirs are created! (Exactly 1 missing.)
You can simplify the code by using the filter counter from the latest collection Community.General. See Counting elements in a sequence, e.g.
- name: check all of the dirs are created
set_fact:
all_dirs_created: true
when: _counts[false] is not defined
vars:
_counts: "{{ dir_check.results|
map(attribute='stat.exists')|
community.general.counter }}"
The solutions above counted the items of the list to enable the evaluation of the option if one of them is false. The code can be simplified further if the counting of the items is not necessary. For example, test if all items are true
- name: check all of the dirs are created
set_fact:
all_dirs_created: true
when: dir_check.results|map(attribute='stat.exists') is all
However, then you have to test the existence of the variable all_dirs_created. More practical is setting both values. Ultimately, the expected functionality of your last two tasks can be reduced to the code below
- name: check all of the dirs are created
set_fact:
all_dirs_created: "{{ dir_check.results|
map(attribute='stat.exists') is all }}"
- debug:
msg: Not all dirs are created!
when: not all_dirs_created

In Ansible, how to query hostvars to get a specific value of a key from a list item based on the value of a different key?

EDIT-UPDATE:
I found a way to achieve what was trying to do, using the index_of plugin. The following code outputs what I need.
---
- hosts: CASPOSR1BDAT003
connection: local
gather_facts: no
become: false
tasks:
- ansible.builtin.set_fact:
mac_address: "{{ hostvars[inventory_hostname]['interfaces'][int_idx|int]['mac_address'] }}"
vars:
int_name: 'PCI1.1'
int_idx: "{{ lookup('ansible.utils.index_of', hostvars[inventory_hostname]['interfaces'], 'eq', int_name, 'name') }}"
- debug:
var: mac_address
Output:
PLAY [CASPOSR1BDAT003] ***********************************************************************************************************************************************************************************************
TASK [ansible.builtin.set_fact] **************************************************************************************************************************************************************************************
ok: [CASPOSR1BDAT003]
TASK [debug] *********************************************************************************************************************************************************************************************************
ok: [CASPOSR1BDAT003] =>
mac_address: 20:67:7C:00:36:A0
What I am trying to do:
Use the Netbox dynamic inventory plugin (this works, brings back all the info I need)
Query hostvars for a particular host, and get the value of the MAC address for a particular interface called PCI1.1
What I have tried:
Converting the hostvars to JSON and using json_query: this hasn't worked, and having looked at some issues on GitHub, hostvars isn't a "normal" dictionary. I've logged a couple of issues anyway (https://github.com/ansible/ansible/issues/76289 and https://github.com/ansible-collections/community.general/issues/3706).
Use a sequence loop and conditional "when" to get the value - this sort of works when using the debug module, but still not just returning the value
What works:
I have tried the following, which outputs the mac_address variable as expected. The length of the list is found, and then the conditional matches the name. I do get an warning about using jinja2 templating delimiters but that's not the target of this question.
---
- hosts: CASPOSR1BDAT003
connection: local
gather_facts: no
become: false
tasks:
- debug:
var: hostvars[inventory_hostname]['interfaces'][{{ item }}]['mac_address']
with_sequence: start=0 end="{{ end_at }}"
vars:
- end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
The result is:
TASK [debug] *************************************************************************************************************************************
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found:
hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
skipping: [CASPOSR1BDAT003] => (item=0)
skipping: [CASPOSR1BDAT003] => (item=1)
skipping: [CASPOSR1BDAT003] => (item=2)
skipping: [CASPOSR1BDAT003] => (item=3)
skipping: [CASPOSR1BDAT003] => (item=4)
ok: [CASPOSR1BDAT003] => (item=5) =>
ansible_loop_var: item
hostvars[inventory_hostname]['interfaces'][5]['mac_address']: 20:67:7C:00:36:A0
item: '5'
skipping: [CASPOSR1BDAT003] => (item=6)
skipping: [CASPOSR1BDAT003] => (item=7)
skipping: [CASPOSR1BDAT003] => (item=8)
skipping: [CASPOSR1BDAT003] => (item=9)
I'm trying to use set_fact to store this mac_address variable as I need to use it in a couple of different ways. However, I am unable to use set_fact on this (or any other hostvars data, it seems). For example, the following:
---
- hosts: CASPOSR1BDAT003
connection: local
gather_facts: no
become: false
tasks:
- ansible.builtin.set_fact:
interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item]['mac_address'] }}"
with_sequence: start=0 end="{{ end_at }}"
vars:
- end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
- debug:
var: interfaces
results in:
fatal: [CASPOSR1BDAT003]: FAILED! =>
msg: |-
The task includes an option with an undefined variable. The error was: 'list object' has no attribute '5'
The error appears to be in '/Users/kivlint/Documents/GitHub/vmware-automation/ansible/prepare-pxe.yml': line 19, column 7, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
# when: hostvars[inventory_hostname]['interfaces'][{{ item }}]['name'] == "PCI1.1"
- ansible.builtin.set_fact:
^ here
If I hard-code the number 5 in, it works fine:
TASK [ansible.builtin.set_fact] ******************************************************************************************************************
ok: [CASPOSR1BDAT003]
TASK [debug] *************************************************************************************************************************************
ok: [CASPOSR1BDAT003] =>
interfaces: 20:67:7C:00:36:A0
If I use '5' as a var for the task, it also works.
---
- hosts: CASPOSR1BDAT003
connection: local
gather_facts: no
become: false
tasks:
- ansible.builtin.set_fact:
interfaces: "{{ hostvars[inventory_hostname]['interfaces'][int_index]['mac_address'] }}"
vars:
- int_index: 5
So I'm wondering, is this a "bug/feature" in how set_fact does or doesn't work with loops (meaning, the same loop worked fine with debug? Or do I need to re-think the approach and consider trying to use set_fact to set a variable with the index of the list (e.g. 5 in the above example)? Or something else?
There's a lot going on in your code, and achieving the result you want is simpler than you've made it.
Firstly, don't use hostvars[inventory_hostname]; plain variables are the ones belonging to the current host, and going through hostvars introduces some exciting opportunities for things to go wrong. hostvars is for accessing variables belonging to other hosts.
Secondly, using Jinja's built-in filtering capabilities avoids the need to worry about the index of the item that you want.
- hosts: CASPOSR1BDAT003
connection: local
gather_facts: no
become: false
vars:
int_name: PCI1.1
mac_address: "{{ interfaces | selectattr('name', 'eq', int_name) | map(attribute='mac_address') | first }}"
tasks:
- debug:
var: mac_address
there is a confusion between the [5] (6th item of a list) and ['5'] (a key named "5") ,
you see in your error: The error was: 'list object' has no attribute '5'.
with the module debug you have not error because [{{item}}] is replaced by [5] and not by ['5']. Its not the same thing with set_fact.
its the reason you have to use filter int to clarify the situation.
- ansible.builtin.set_fact:
interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item|int]['mac_address'] }}"
with_sequence: start=0 end="{{ end_at }}"
vars:
end_at: "{{ (hostvars[inventory_hostname]['interfaces'] | length) - 1 }}"
when: hostvars[inventory_hostname]['interfaces'][item|int]['name'] == "PCI1.1"
so i suggest you to use loop instead with_sequence:
- ansible.builtin.set_fact:
interfaces: "{{ hostvars[inventory_hostname]['interfaces'][item]['mac_address'] }}"
loop: "{{ range(0, end_at|int, 1)|list }}"
vars:
end_at: "{{ hostvars[inventory_hostname]['interfaces'] | length }}"
when: hostvars[inventory_hostname]['interfaces'][item]['name'] == "PCI1.1"
set_fact works with loops, but not in a way you expect.
This example constructs list with loop from lists of dicts:
- set_fact:
foo: '{{ foo|d([]) + [item.value] }}'
loop:
- value: 1
- value: 2
Basically, each execution of set_fact creates a fact. You may refer to the same fact in jinja expression for set_fact, but you can't expect it to automatically build lists or something like that.

Resources