Ansible when all item in loop is true - ansible

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

Related

Run an Ansible task to all hosts if at least one of the hosts has a variable

I want to run a task to all hosts if at least one of the hosts has a variable "new_node".
For example i have inventory
[all]
host1.example.net
host2.example.net
host3.example.net new_node=True
And if in one of the hosts has variable "new_node=True"
then run this task on all hosts
---
- hosts: all
tasks:
- name: Create file yep at all hosts
file:
path: /tmp/yep
state: file
What condition or filter should i apply? Any ideas. Thanks
extract the variables, select true items and evaluate the test is any, e.g.
- set_fact:
semaphore: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'new_node')|
select is any }}"
run_once: true
gives true if any variable new_node is true and gives false otherwise
semaphore: true
Then use the variable semaphore in the conditions e.g.
when: semaphore|bool
Filter select explained
Quoting from select
"If no test is specified, each object will be evaluated as a boolean."
Given the inventory
shell> cat hosts
host1.example.net
host2.example.net new_node=False
host3.example.net new_node=True
The task extracts the variables
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'new_node')|
list }}"
msg: '[Undefined, False, True]'
We don't have to care about Undefined because select() evaluates Undefined to False
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'new_node')|
select|list }}"
msg:
- true
You can test the evaluation of Undefined to False separately, e.g.
- debug:
msg: "{{ ansible_play_hosts_all|
map('extract', hostvars, 'new_node')|
map('bool')|
list }}"
gives
msg:
- false
- false
- true
If no variable is defined
shell> cat hosts
host1.example.net
host2.example.net
host3.example.net
select returns an empty list
msg: '[Undefined, Undefined, Undefined]'
msg: []
You can create a variable, that will be set to true when one of the hosts in [all] group has the variable new_node defined. Then this variable can be used to conditionally run the task.
Example:
tasks:
- set_fact:
run_on_all: true
when: hostvars[item]['new_node']|default(false)
with_items: "{{ groups['all'] }}"
- file:
path: /tmp/yep
state: touch
when: run_on_all|default(false)
The first task, sets the variable run_on_all to true if any one of the hosts has the variable new_node=True. Then the next task will execute if the previous task set the run_on_all variable to true. I am using the default() filter to avoid the chances of the variables being "undefined".
You did not actually specify what should happen if this property was not present, so I will assume you would simply want to abort the play in that case.
Simplest solution that I could think of is to set the playbook to run against the group [all], and check if this property is present on any of the hosts. If it is not present on any hosts, you abort the play.
Example
---
- hosts: all
tasks:
- name: Check if the hostvar 'new_node' is present on any hosts
set_fact:
has_new_node: "{{ groups['all'] | map('extract', hostvars, 'new_node') | list | select('defined') | length | bool }}"
delegate_to: localhost
- name: Abort play if not 'has_new_node'
meta: end_play
when: not has_new_node
- debug:
msg: new_node found

detect file difference (change) with `ansible`

In this task I found a roundabout method to compare two files (dconfDump and dconfDumpLocalCurrent) and to set a variable (previously defined as false) to true if the two files differ.
The solution seem to work, but it looks ugly and, as a beginner with ansible, I have the impression a better solution should be existing.
---
# vars file for dconfLoad
local_changed : false
target_changed : false
---
- name: local changed is true when previous target different then local current
shell: diff /home/frank/dconfDump /home/frank/dconfDumpLocalCurrent
register: diff_oldtarget_localCurrent
register: local_changed
ignore_errors: true
- debug:
msg: CHANGED LOCALLY
when: local_changed
Some background to the task, which is an attempt to synchronize files: A file LocalCurrent is compared with LocalOld and CurrentTarget, to determine if the LocalCurrent is changed and if it is different than currentTarget. If LocalCurrent is not changed and CurrentTarget is changed, then apply the change (and set LocalOld to CurrentTarget); if LocalCurrent is changed then upload to controller.
What is the appropriate approach with ansible? Thank you for help!
You can use stat to get the checksum and then compare it. Please see below.
tasks:
- name: Stat of dconfDump
stat:
path : "/tmp/dconfDump"
register: dump
- name: SHA1 of dconfDump
set_fact:
dump_sha1: "{{ dump.stat.checksum }}"
- name: Stat of dconfDumpLocalCurrent
stat:
path : "/tmp/dconfDumpLocalCurrent"
register: dump_local
- name: SHA1 of dconfDumpLocalCurrent
set_fact:
local_sha1: "{{ dump_local.stat.checksum }}"
- name: Same
set_fact:
val: "False"
when: dump_sha1 != local_sha1
- name: Different
set_fact:
val: "True"
when: dump_sha1 == local_sha1
- name: Print
debug:
msg: "{{val}}"
Use stat and create dictionary of checksums. For example
- stat:
path: "{{ item }}"
loop:
- LocalOld
- LocalCurrent
- CurrentTarget
register: result
- set_fact:
my_files: "{{ dict(paths|zip(chkms)) }}"
vars:
paths: "{{ result.results|map(attribute='stat.path')|list }}"
chkms: "{{ result.results|map(attribute='stat.checksum')|list }}"
- debug:
var: my_files
gives (abridged) if all files are the same
my_files:
CurrentTarget: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
LocalCurrent: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
LocalOld: 7c73e9f589ca1f0a1372aa4cd6944feec459c4a8
Then use the dictionary to compare the checksums and copy files. For example
# If LocalCurrent is not changed and CurrentTarget is changed,
# then apply the change (and set LocalOld to CurrentTarget)
- debug:
msg: Set LocalOld to CurrentTarget
when:
- my_files['LocalCurrent'] == my_files['LocalOld']
- my_files['LocalCurrent'] != my_files['CurrentTarget']
- debug:
msg: Do not copy anything
when:
- my_files['LocalCurrent'] == my_files['LocalOld']
- my_files['LocalCurrent'] == my_files['CurrentTarget']
gives
TASK [debug] ****
skipping: [localhost]
TASK [debug] ****
ok: [localhost] =>
msg: Do not copy anything

Ansible: Skip loop when list is undefined

Example playbook -
---
- hosts: localhost
vars:
lesson:
name: Physics
students:
- Bob
- Joe
tasks:
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: item | default("")
The above playbook works well to output the student names.
However, if the input changes (as per below) such that no student names have been defined, then an error occurs. Is there a simple way to have the playbook skip this task if the list is undefined as per the input below? I realize it would work if the input specifies students: [], but as this input is coming from simple users, they're not going to know this. Much Thanks!
vars:
lesson:
name: Physics
students:
Error: fatal: [localhost]: FAILED! =>
msg: 'Invalid data passed to ''loop'', it requires a list, got this instead: . Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup.
Update - I've tried the below variations but still get the same error -
---
- hosts: localhost
vars:
lesson:
name: Physics
students:
tasks:
- name: Display student names variation 1
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: lesson.students is iterable
- name: Display student names variation 2
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: lesson.students is not none
- name: Display student names variation 3
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: ( item | default("") ) or ( item is not none )
The real problem is that loop requires a list, even if it is an empty list.
If your var is undefined/None/empty string, it exists but is not a list and your when condition will never get evaluated because loop will fire an error before it is ever reached.
You have to default your var to an empty list in such cases, which will lead to a 0 size loop equivalent to skipping the task.
Since your var is defined but None you need to use the second optional parameter to default so that empty/false values are replaced as well
Note: I used the short alias d to default in my below examples
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students | d([], true) }}"
A good practice here that would have nipped that error in the bud would be to have a coherent data declaration by either:
not declaring the key at all and use a simple default i.e.
# ... #
vars:
lesson:
name: Physics
# ... #
loop: "{{ lesson.students | d([]) }}"
declare an empty list for the key rather than a None value i.e.
# ... #
vars:
lesson:
name: Physics
students: []
# ... #
loop: "{{ lesson.students }}"
My first proposition is the safest in this case anyway and will work in for all the above vars declarations.
There is a difference between an undefined variable, and variable having None value.
When you set variable name, but leave the right hand side empty. The variable is defined, but it is set to NoneType.
So your when: condition should have additional check for NoneType:
- hosts: localhost
vars:
lesson:
name: Physics
students:
tasks:
- name: Display student names
debug:
msg: '{{ item }}'
loop: "{{ lesson.students }}"
when: ( item | default("") ) or ( item is not none )
This will give:
skipping: [localhost] => (item=None)

iteration using with_items and register

Looking for help with a problem I've been struggling with for a few hours. I want to iterate over a list, run a command, register the output for each command and then iterate with debug over each unique registers {{ someregister }}.stdout
For example, the following code will spit out "msg": "1" and "msg": "2"
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: result
with_items: "{{ numbers }}"
- debug: msg={{ item.stdout }}
with_items: "{{ result.results }}"
If however, I try and capture the output of a command in a register variable that is named using with_list, I am having trouble accessing the list or the elements within it. For example, altering the code slightly to:
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: "{{ item.name }}"
with_items: "{{ numbers }}"
- debug: var={{ item.name.stdout }}
with_items: "{{ numbers }}"
Gives me:
TASK [debug]
> ******************************************************************* fatal: [localhost]: FAILED! => {"failed": true, "msg": "'unicode
> object' has no attribute 'stdout'"}
Is it not possible to dynamically name the register the output of a command which can then be called later on in the play? I would like each iteration of the command and its subsequent register name to be accessed uniquely, e.g, given the last example I would expect there to be variables registered called "first" and "second" but there aren't.
Taking away the with_items from the debug stanza, and just explicitly defining the var or message using first.stdout returns "undefined".
Ansible version is 2.0.2.0 on Centos 7_2.
Thanks in advance.
OK so I found a post on stackoverflow that helped me better understand what is going on here and how to access the elements in result.results.
The resultant code I ended up with was:
---
- hosts: localhost
gather_facts: false
vars:
numbers:
- name: "first"
int: "1"
- name: "second"
int: "2"
tasks:
- name: Register output
command: "/bin/echo {{ item.int }}"
register: echo_out
with_items: "{{ numbers }}"
- debug: msg="item.item={{item.item.name}}, item.stdout={{item.stdout}}"
with_items: "{{ echo_out.results }}"
Which gave me the desired result:
"msg": "item.item=first, item.stdout=1"
"msg": "item.item=second, item.stdout=2"
I am not sure if I understand the question correctly, but maybe this can help:
- debug: msg="{{ item.stdout }}"
with_items: echo_out.results
Please note that Ansible will print each item and the msg both - so you need to look carefully for a line that looks like "msg": "2".

Skip certain items on condition in ansible with_items loop

Is it possible to skip some items in Ansible with_items loop operator, on a conditional, without generating an additional step?
Just for example:
- name: test task
command: touch "{{ item.item }}"
with_items:
- { item: "1" }
- { item: "2", when: "test_var is defined" }
- { item: "3" }
in this task I want to create file 2 only when test_var is defined.
The other answer is close but will skip all items != 2. I don't think that's what you want. here's what I would do:
- hosts: localhost
tasks:
- debug: msg="touch {{item.id}}"
with_items:
- { id: 1 }
- { id: 2 , create: "{{ test_var is defined }}" }
- { id: 3 }
when: item.create | default(True) | bool
The when: conditional on the task is evaluated for each item. So in this case, you can just do:
...
with_items:
- 1
- 2
- 3
when: item != 2 and test_var is defined
I had a similar problem, and what I did was:
...
with_items:
- 1
- 2
- 3
when: (item != 2) or (item == 2 and test_var is defined)
Which is simpler and cleaner.
I recently ran into this problem and none of the answers I found were exactly what I was looking for. I wanted a way to selectively include a with_item based on another variable.
Here is what I came up with:
- name: Check if file exists
stat:
path: "/{{item}}"
with_items:
- "foo"
- "bar"
- "baz"
- "{% if some_variable == 'special' %}bazinga{% endif %}"
register: file_stat
- name: List files
shell: echo "{{item.item | basename}}"
with_items:
- "{{file_stat.results}}"
when:
- item.stat | default(false) and item.stat.exists
When the above plays are run, the list of items in file_stat will only include bazinga if some_variable == 'special'
What you want is that file 1 and file 3 always gets created but file 2 is created only when test_var is defined. If you use ansible's when condition it works on complete task and not on individual items like this :
- name: test task
command: touch "{{ item.item }}"
with_items:
- { item: "1" }
- { item: "2" }
- { item: "3" }
when: test_var is defined
This task will check the condition for all three line items 1,2 and 3.
However you can achieve this by two simple tasks :
- name: test task
command: touch "{{ item }}"
with_items:
- 1
- 3
- name: test task
command: touch "{{ item }}"
with_items:
- 2
when: test_var is defined

Resources