Registering variables from a looped task and conditionals for skipped tasks - ansible

I'm trying to figure out a port test task in a playbook. I have two different sets of ports that need to be checked, but only checked if the server hostname has dev/test/stg/eng in it. Then everything else will be considered a production host and the production ports will be checked.
Here is my playbook:
- name: Filebeat Firewall port check | Dev
wait_for:
host: "{{ item.name }}"
port: "{{ item.port }}"
state: started
delay: 0
timeout: 5
loop: "{{ dev_ports }}"
register: devresults
when: ansible_facts['hostname'] | regex_search('dev|tst|test|eng')
- name: Filebeat Firewall port check | Prod
wait_for:
host: "{{ item.name }}"
port: "{{ item.port }}"
state: started
delay: 0
timeout: 5
loop: "{{ prod_ports }}"
when: devresults is skipped
The first task runs as expected when ran against a production server (is skipped) and a development server (ports are checked). However, the second task is skipped when ran against a production server, as well as a development server (this should happen).
Basically, if the first task is skipped the second task should run, and if the first ran the second task should be skipped, this isn't happening. I'm not sure what I'm missing, I'm thinking its the when statement in the second task, or if the loop is causing my issue, any help would be welcomed.

Regarding your question
Basically, if the first task is skipped the second task should run, and if the first ran the second task should be skipped, this isn't happening.
I've setup a short first example
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Create result set
shell:
cmd: echo 'I am not skipped.'
register: result
when: not ansible_check_mode
- name: Show result when not skipped
debug:
var: result
when: not result.skipped | default('false') | bool
- name: Show result when skipped
debug:
var: result
when: result.skipped | default('false') | bool
and found it working when running with ansible-playbook skip.yml and ansible-playbook skip.yml --check.
Depending on the task and used modules, the reason seems to be when a task becomes skipped the result set is
TASK [Show result when skipped] **************************
ok: [localhost] =>
result:
changed: false
skip_reason: Conditional result was False
skipped: true
whereby when not skipped the result set is
TASK [Show result when not skipped] *********************
ok: [localhost] =>
result:
changed: true
cmd: echo 'I am not skipped.'
delta: '0:00:00.123456'
end: '2022-02-16 20:00:00.123456'
failed: false
rc: 0
start: '2022-02-16 20:00:00.000000'
stderr: ''
stderr_lines: []
stdout: I am not skipped.
stdout_lines:
- I am not skipped.
and don't has the skipped flag. Therefore you may introduce filter to Provide default values and to Force the data type.
Regarding your objection
"the example doesn't really applies as it isn't doing any loops"
you may have a look into the result set of the following second example with loop and Extended loop variables.
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Create result set
shell:
cmd: echo 'I am not skipped.'
register: result
when: item is even
loop: "{{ [1,2] }}"
loop_control:
extended: true
label: "{{ item }}"
- name: Show result
debug:
var: result
which produce an output of
TASK [Create result set] *******
changed: [localhost] => (item=2)
TASK [Show result] ******************************
ok: [localhost] =>
result:
changed: true
msg: All items completed
results:
- ansible_loop:
allitems:
- 1
- 2
first: true
index: 1
index0: 0
last: false
length: 2
nextitem: 2
revindex: 2
revindex0: 1
ansible_loop_var: item
changed: false
item: 1
skip_reason: Conditional result was False
skipped: true
- ansible_loop:
allitems:
- 1
- 2
first: false
index: 2
index0: 1
last: true
length: 2
previtem: 1
revindex: 1
revindex0: 0
ansible_loop_var: item
changed: true
cmd: echo 'I am not skipped.'
delta: '0:00:00.123456'
end: '2022-02-16 22:15:00.123456'
failed: false
invocation:
module_args:
_raw_params: echo 'I am not skipped.'
_uses_shell: true
argv: null
chdir: null
creates: null
executable: null
removes: null
stdin: null
stdin_add_newline: true
strip_empty_ends: true
warn: true
item: 2
rc: 0
start: '2022-02-16 22:15:00.000000'
stderr: ''
stderr_lines: []
stdout: I am not skipped.
stdout_lines:
- I am not skipped.
As you can see it is a data structure containing a list of items and it does contain the information about the first skipped item. To access it you may enhance the second example with
- name: Show result when skipped
debug:
msg: "{{ item.item }}"
when: item.skipped | default('false') | bool
loop: "{{ result.results }}"
loop_control:
extended: true
label: "{{ item }}"
and have a look into the output. It also works according the examples of Conditions based on registered variables.
- name: Show result when skipped
debug:
msg: "{{ item.item }}"
when: item is skipped
loop: "{{ result.results }}"
Further Reading
Troubleshoot Ansible Playbooks
Ansible - Check variable type
Discovering the data type

Related

Multiple 'and' operators in Ansible 'when' condition

This is the code that I am trying to run
- name: Read and Register Contents of .bash_profile
shell: grep -E 'AB_AG_HOME|AB_AG_LOCAL_ROOT|AB_AG_LOCAL_DIR|AB_AG_CONFIG_DIR|AB_AG_LOG_DIR' /home/username/.bash_profile
register: output
- debug:
msg: "{{ output.stdout_lines }}"
- name: Append AG environment variables in .bash_profile
shell: cat /home/{{ admin_user }}/tmp_bash.profile >> /home/{{ admin_user }}/.bash_profile
when: "'AB_AG_HOME' and 'AB_AG_LOCAL_ROOT' and 'AB_AG_LOCAL_DIR' and 'AB_AG_CONFIG_DIR' and 'AB_AG_LOG_DIR' not in item"
with_items: "{{ output.stdout_lines }}"
- name: Delete the temporary tmp_bash.profile"
file:
path: /home/{{ abinitio_admin_user }}/tmp_bash.profile
state: absent
when I run this code, all the values are repeated 4 times.
Is there anything that is missing?
By looping over the stdout_lines via
with_items: "{{ output.stdout_lines }}"`
the condition
when: "'AB_AG_HOME' and 'AB_AG_LOCAL_ROOT' and 'AB_AG_LOCAL_DIR' and 'AB_AG_CONFIG_DIR' and 'AB_AG_LOG_DIR' not in item"
would only be true if all strings are in one line (item) together.
You may have a look into the following minimal example
---
- hosts: localhost
become: false
gather_facts: false
vars:
result:
stdout_lines:
- "A1"
- "A2"
- "A3"
- "A4"
- "A5"
tasks:
- name: Debug one-line conditional
debug:
msg: "Not in stdout_lines"
when: "'A1' and 'A2' and 'A3' and 'A4' and 'A5' not in result.stdout_lines"
Whereby the first given example get skipped because of the condition, the second example
- name: Debug loop conditional
debug:
msg: "Not in {{ result.stdout_lines[ansible_loop.index0] }}"
when: "'A1' and 'A2' and 'A3' and 'A4' and 'A5' not in item"
loop: "{{ result.stdout_lines }}"
loop_control:
extended: true
will result into an output of
TASK [Debug loop conditional] ******
ok: [localhost] => (item=A1) =>
msg: Not in A1
ok: [localhost] => (item=A2) =>
msg: Not in A2
ok: [localhost] => (item=A3) =>
msg: Not in A3
ok: [localhost] => (item=A4) =>
msg: Not in A4
For further debugging you could also use the assert module – Asserts given expressions are true.

Loop over list and evaluate element property in Ansible

I have a list with shell lines that I want to execute on inventory hosts so I can determine if the database is working. For the test purposes I have 1 server with PostgreSQL and 1 with MySQL.
This is my playbook so far:
- name: Check db statuses
shell: "{{ item }}"
loop:
- ps -fp $(pgrep -u postgres) | grep /usr/lib/postgresql
- ps -fp $(pgrep -u mysql) | grep mysqld
register: http
ignore_errors: yes
changed_when: item.failed == false
this is failing with:
{
"http": {
"failed": true,
"msg": "The conditional check 'item.failed == false' failed. The error was: error while evaluating conditional (item.failed == false): 'ansible.parsing.yaml.objects.AnsibleUnicode object' has no attribute 'failed'"
}
}
I want to assign only the item.failed==false result in the register variable (http) but ignore the failed ones.
You can't select what will be registered in a loop. Instead, you'll have to evaluate the registered results in the next task(s), e.g.
- hosts: localhost
tasks:
- command: "{{ item }}"
loop:
- /bin/true
- /bin/false
register: http
ignore_errors: true
- debug:
msg: "{{ item.item }} failed: {{ item.failed }}"
loop: "{{ http.results }}"
loop_control:
label: "{{ item.cmd }}"
gives
ok: [localhost] => (item=['/bin/true']) =>
msg: '/bin/true failed: False'
ok: [localhost] => (item=['/bin/false']) =>
msg: '/bin/false failed: True'

Complex with_subelements construct

I have the list:
final_list:
- result:
name: "val1"
status: true
- result:
name: "val2"
status: true
- skipped:
path: "path1"
- result:
results:
- result:
name: "val4"
status: true
- result:
name: "val5"
status: true
- skipped:
path: "path2"
I would like check if any status in my list is false.
I started with:
- set_fact:
any_false: true
when: (item.0.result is defined and item.0.result.status == false) or (item.1.result is defined and item.1.result.status == false)
with_subelements:
- "{{ final_list }}"
- result.results"
- skip_missing: True
but this task ommit elements from finally_list without results in result.
I would expect iterate by all elements which contain status variable.
I tried also:
with_subelements:
- "{{ final_list }}"
- result.results | default([])
but get error:
msg: msg: the key result should point to a dictionary, got 'None'
I suggest to do this a bit differently: get all individual result elements in a single flattened list, extract the status and check that all elements are true (or not).
The following playbook should be self explanatory (run with -v to see intermediate debug)
---
- hosts: localhost
gather_facts: false
vars:
final_list:
- result:
name: "val1"
status: true
- result:
results:
- result:
name: "valA"
status: false
- result:
name: "valB"
status: true
- skipped:
path: "path2"
- result:
name: "val2"
status: true
- skipped:
path: "path1"
- result:
results:
- result:
name: "val4"
status: true
- result:
name: "val5"
status: true
- skipped:
path: "path2"
# List of all top level elements without nested elements
single_results: "{{ final_list | rejectattr('result.results', 'defined') }}"
# Flattened list of all nested result elements.
# Will work with several nested elements (as you can see from my example data)
nested_results: "{{ final_list | selectattr('result.results', 'defined') | map(attribute='result.results') | flatten }}"
# Single list containing top level + nested result elements
all_results: "{{ single_results + nested_results }}"
# List of all existing status values in the list of result elements
all_status: "{{ all_results | selectattr('result.status', 'defined') | map(attribute='result.status') }}"
tasks:
- name: Show entire list
debug:
var: all_results
verbosity: 1
- name: Show list of status
debug:
var: all_status
verbosity: 1
- name: Make sure all exsiting status are true
assert:
that:
- all_status is all
fail_msg: "At least one status is false"
success_msg: "All status are true"
which gives:
PLAY [localhost] ***********************************************************************************************************************************************************************************************************************
TASK [Show entire list] ****************************************************************************************************************************************************************************************************************
skipping: [localhost]
TASK [Show list of status] *************************************************************************************************************************************************************************************************************
skipping: [localhost]
TASK [Make sure all exsiting status are true] ******************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {
"assertion": "all_status is all",
"changed": false,
"evaluated_to": false,
"msg": "At least one status is false"
}
PLAY RECAP *****************************************************************************************************************************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=2 rescued=0 ignored=0
And if you modify the data to all true (only pasting the last task...)
TASK [Make sure all exsiting status are true] ******************************************************************************************************************************************************************************************
ok: [localhost] => {
"changed": false,
"msg": "All status are true"
}
Q: "Check if any status is false."
A: Simple brute force
- set_fact:
status: "{{ status|d([]) + [item.split(':')|last|trim|bool] }}"
loop: "{{ (final_list|to_nice_yaml).splitlines() }}"
when: item is match('^.*\\s+status:\\s+.*$')
gives the list of the statuses
status:
- true
- true
- true
- true
See Testing if a list value is True, e.g.
- debug:
msg: No status is False.
when: status is all

Getting changed/failed hosts list from a previous task | Ansible

All,
Example: If i've got 20 hosts for a playbook and running them with Serial:10, below shell command runs on 10 hosts at a time. Once done handler task is called, wherein the task which creates dict (_dict) doesn't give a dictionary output thus the second task - Failed host - failed with mentioned error.
- name: Run some shell command
shell: "echo 2 > /abcd/abcd.txt"
when: random condition is satisfied
register: update2
ignore_errors: yes
notify: abc_handler
- handler:
- name: abcd_handler
set_fact:
_dict: "{{ dict(ansible_play_hosts|zip(
ansible_play_hosts|map('extract', hostvars, 'update2'))) }}"
run_once: true
- name: Find failed hosts
set_fact:
_failed: "{{ _dict|dict2items|json_query('[?value.failed].key') }}"
run_once: true
Handler First task output:
"changed: false"
"ansible_facts": {
"_dict": "{u'host1': {'stderr_lines': [], u'changed': True,...u'host2':.....u'host10'}"
2nd handler task gives the mentioned error when the dict2items is run for above values.
Thank you.
Q: "List of hosts where a certain task executed, changed something, or got failed."
A: For example, the command makes no changes at test_11 changes the file at test_12, and fails at test_13
- hosts: test_11,test_12,test_13
tasks:
- shell:
cmd: "echo 2 > /tmp/test/abcd.txt"
creates: /tmp/test/abcd.txt
register: update1
ignore_errors: true
TASK [shell] ***********************************************************
changed: [test_12]
fatal: [test_13]: FAILED! => changed=true
cmd: echo 2 > /tmp/test/abcd.txt
delta: '0:00:00.045992'
end: '2021-04-25 23:22:31.623804'
msg: non-zero return code
rc: 2
start: '2021-04-25 23:22:31.577812'
stderr: '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
stderr_lines: <omitted>
stdout: ''
stdout_lines: <omitted>
...ignoring
ok: [test_11]
Let's create a dictionary with the data first, e.g.
- set_fact:
_dict: "{{ dict(ansible_play_hosts|
zip(ansible_play_hosts|
map('extract', hostvars, 'update1'))) }}"
run_once: true
gives
_dict:
test_11:
changed: false
cmd: echo 2 > /tmp/test/abcd.txt
failed: false
rc: 0
stdout: skipped, since /tmp/test/abcd.txt exists
stdout_lines:
- skipped, since /tmp/test/abcd.txt exists
test_12:
changed: true
cmd: echo 2 > /tmp/test/abcd.txt
delta: '0:00:00.032474'
end: '2021-04-25 23:14:36.361510'
failed: false
rc: 0
start: '2021-04-25 23:14:36.329036'
stderr: ''
stderr_lines: []
stdout: ''
stdout_lines: []
test_13:
changed: true
cmd: echo 2 > /tmp/test/abcd.txt
delta: '0:00:00.054980'
end: '2021-04-25 23:14:35.565811'
failed: true
msg: non-zero return code
rc: 2
start: '2021-04-25 23:14:35.510831'
stderr: '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
stderr_lines:
- '/bin/sh: cannot create /tmp/test/abcd.txt: Permission denied'
stdout: ''
stdout_lines: []
Note that test_11 is reported ok not skipped despite the registered variable showing "stdout: skipped, since /tmp/test/abcd.txt exists".
The analysis is now trivial, e.g.
- set_fact:
_failed: "{{ _dict|dict2items|json_query('[?value.failed].key') }}"
run_once: true
gives the list of the failed hosts
_failed:
- test_13
and the next task
- set_fact:
_changed: "{{ (_dict|dict2items|json_query('[?value.changed].key'))|
difference(_failed) }}"
_ok: "{{ _dict|dict2items|json_query('[?value.changed == `false`].key') }}"
run_once: true
gives
_changed:
- test_12
_ok:
- test_11
Note that
The failed hosts need to be subtracted from the changed hosts because failed hosts are also reported as changed.
There will be no registered variable if a task is skipped.
Serial
Split the playbook into 2 plays if serial is used. e.g.
shell> cat playbook.yml
- hosts: all
serial: 10
tasks:
- shell:
cmd: "echo 2 > /tmp/test/abcd.txt"
creates: /tmp/test/abcd.txt
register: update1
ignore_errors: true
- hosts: all
tasks:
- set_fact:
_dict: "{{ dict(ansible_play_hosts|zip(
ansible_play_hosts|map('extract', hostvars, 'update1'))) }}"
run_once: true
It seems you would like to get the hosts on which the command task (shown in the question) failed or changed, and then target them for some other tasks.
There are two things required for this:
If the command task fails, playbook execution will stop and hence none of the following tasks will run. So we need to add ignore_errors flag to the task
add_host module to create a new group of hosts when the task failed or changed
So finally tasks like below should do the trick:
- hosts: some_group
serial: 1
- name: update file count
shell: "echo 2 > /home/ec2-user/abcd.txt"
when:
- count.stdout == "1"
register: update1
ignore_errors: true
- name: conditionally add the hosts from current play hosts to a new group
add_host:
groups:
- new_group
host: "{{ ansible_hostname }}"
when: >
cmd_stat is failed or
cmd_stat is changed
# Then have a play targeting the new group
- hosts: new_group
tasks:
# Tasks to be performed
Though the use of serial might make the whole playbook run longer if there are lot of hosts.

How to print command output in Ansible when there is a condition?

My ansible code looks something like the following. Problem is this only works when my inventory has dev,qa, perf and prod servers. For example, if my inventory has only dev servers then it fails. Is there any way to avoid this failure ?
I tried changing both cmd_dev_out and cmd_qa_out to cmd_out but that did not help either.
- name: Execute
hosts: all
tasks:
- name: Execute against dev
shell: some command
register: cmd_dev_out
when: ( servers are dev )
- debug: msg="{{ cmd_dev_out }}"
- name: Execute against qa
shell: some command
register: cmd_qa_out
when: ( servers are qa )
- debug: msg="{{ cmd_qa_out }}"
....... More conditions below .....
The best would be to use a block in here to logically group the execution of the command and the print of the output of the said command.
Because, if there is no command run, well, obviously you won't get anything as a result.
This is an example using block
- name: Execute
hosts: all
tasks:
- block:
- name: Execute against dev
shell: 'echo "dev"'
register: cmd_dev_out
- debug:
msg: "{{ cmd_dev_out }}"
when: "'dev' in inventory_hostname"
- block:
- name: Execute against qa
shell: 'echo "qa"'
register: cmd_qa_out
- debug:
msg: "{{ cmd_qa_out }}"
when: "'qa' in inventory_hostname"
Mind that this means that the condition when is now tied to the block and both the command and the debug will be skipped when the condition is false.
Example of recap:
PLAY [localhost] **************************************************
TASK [Execute against dev] ****************************************
skipping: [localhost]
TASK [debug] ******************************************************
skipping: [localhost]
TASK [Execute against qa] *****************************************
changed: [localhost]
TASK [debug] ******************************************************
ok: [localhost] =>
msg:
changed: true
cmd: echo "qa"
delta: '0:00:00.002641'
end: '2023-01-25 20:51:04.049532'
failed: false
msg: ''
rc: 0
start: '2023-01-25 20:51:04.046891'
stderr: ''
stderr_lines: []
stdout: qa
stdout_lines:
- qa
Another, but maybe less elegant solution would be to use the default Jinja filter on your command result in case the command was skipped.
- name: Execute
hosts: all
tasks:
- name: Execute against dev
shell: 'echo "dev"'
register: cmd_dev_out
when: "'dev' in inventory_hostname"
- debug:
msg: "{{ cmd_dev_out | default('cmd_dev was skipped') }}"
- name: Execute against qa
shell: 'echo "qa"'
register: cmd_qa_out
when: "'qa' in inventory_hostname"
- debug:
msg: "{{ cmd_qa_out | default('cmd_qa was skipped') }}"
Figured out another solution. Its given below (add the when condition to the debug statement) :
- name: Execute
hosts: all
tasks:
- name: Execute against dev
shell: some command
register: cmd_dev_out
when: ( servers are dev )
- debug: msg="{{ cmd_dev_out }}"
when: ( servers are dev )
- name: Execute against qa
shell: some command
register: cmd_qa_out
when: ( servers are qa )
- debug: msg="{{ cmd_qa_out }}"
when: ( servers are qa )
....... More conditions below .....

Resources