Getting changed/failed hosts list from a previous task | Ansible - 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.

Related

How do I add a custom message if a task does not meet the 'when' condition and is skipped?

In this task, I am searching for a file. If there is no file, the task will be skipped.
My question is, how do I write a custom message to output when the task is skipped?
- name: Search for files
win_find:
paths: C:\dataset\
register: data
- debug:
msg: "Data exists"
when: data | json_query('files[*].exists')
- name: set_fact
set_fact:
exists: "{{ data | json_query('files[*].exists') }}"
In a different playbook:
- name: Run if file exists
block:
- name: read content from file
win_shell: C:\day.txt
register: day
when: hostvars['10.11.18.190']['exists']
- name: print message
debug:
msg: "{{ hostvars['10.12.201.20']['day'] }}"
As there is no file, the task is skipped:
TASK [Run if file exists] *********************
skipping: [10.11.18.190] => {
"changed": false,
"skip_reason": "Conditional result was False"
}
TASK [print message] **************************************************************************************
ok: [10.11.18.190] => {
"msg": {
"changed": false,
"skip_reason": "Conditional result was False",
"skipped": true
}
}
As you can see from the output, the variable hostvars['10.12.201.20']['day'] is showing "changed": false, skip_reason, etc. But I do not want this, I want it to output a message like, "File does not exist".
How can I create a custom message for this variable hostvars['10.12.201.20']['day']?
A: Use the 'Do It Yourself' callback plugin community.general.diy. See
shell> ansible-doc -t callback community.general.diy
(or the online documentation)
For example, if the file /tmp/day.txt does not exist the playbook
shell> cat pb.yml
- hosts: localhost
tasks:
- stat:
path: /tmp/day.txt
register: stat_day
- command: cat /tmp/day.txt
register: day
when: stat_day.stat.exists
vars:
ansible_callback_diy_runner_on_skipped_msg: |
skipping: [{{ inventory_hostname }}]
msg: File does not exist.
ansible_callback_diy_runner_on_skipped_msg_color: green
will display the custom message
shell> ANSIBLE_STDOUT_CALLBACK=community.general.diy ansible-playbook pb.yml
PLAY [localhost] *****************************************************************************
TASK [stat] **********************************************************************************
ok: [localhost]
TASK [command] *******************************************************************************
skipping: [localhost]
msg: File does not exist.
PLAY RECAP ***********************************************************************************
localhost: ok=1 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Optionally, you can use the block/rescue construct. See Handling errors with blocks.
For example, in Linux (I don't have access to Windows atm) when you try to access a nonexistent file by the module command on the localhost
- command: cat /tmp/day.txt
register: day
the command will fail
fatal: [localhost]: FAILED! => changed=true
ansible_facts:
discovered_interpreter_python: /usr/bin/python3
cmd:
- cat
- /tmp/day.txt
delta: '0:00:00.010884'
end: '2023-02-14 07:21:50.664051'
msg: non-zero return code
rc: 1
start: '2023-02-14 07:21:50.653167'
stderr: 'cat: /tmp/day.txt: No such file or directory'
stderr_lines: <omitted>
stdout: ''
stdout_lines: <omitted>
Put the command into the block and use the section rescue
- block:
- command: cat /tmp/day.txt
register: day
- debug:
var: day.stdout
rescue:
- debug:
var: ansible_failed_result
Now, if the command fails you'll see
ansible_failed_result:
ansible_facts:
discovered_interpreter_python: /usr/bin/python3
changed: true
cmd:
- cat
- /tmp/day.txt
delta: '0:00:01.007972'
end: '2023-02-14 07:24:43.791343'
failed: true
invocation:
module_args:
_raw_params: cat /tmp/day.txt
_uses_shell: false
argv: null
chdir: null
creates: null
executable: null
removes: null
stdin: null
stdin_add_newline: true
strip_empty_ends: true
msg: non-zero return code
rc: 1
start: '2023-02-14 07:24:42.783371'
stderr: 'cat: /tmp/day.txt: No such file or directory'
stderr_lines:
- 'cat: /tmp/day.txt: No such file or directory'
stdout: ''
stdout_lines: []
You can reduce the output to the standard error
rescue:
- debug:
var: ansible_failed_result.stderr
If the file exists
shell> echo 'This is the content of /tmp/day.txt' > /tmp/day.txt
The next task in the block will display the standard output of the command
day.stdout: This is the content of /tmp/day.txt
Of course, there will be differences in the error messages among the operating systems. Take a look at the data you get and fit the messages to your needs.
I see not direct solution for this problem. But you could do the following:
disable the output of skipped tasks at all, with
[defaults]
display_skipped_hosts = true
In the ansbile.cfg
For details see https://docs.ansible.com/ansible/latest/collections/ansible/builtin/default_callback.html
create a debug task with the message you want to display with an opposite when condition. So it will only run if the other task is skipped.

Registering variables from a looped task and conditionals for skipped tasks

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

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'

How to display values of two different task register variables in ansible

Below is my playbook,
---
- hosts: all
tasks:
- name: asyn task 1 use time command to see diff use when hostname to get faster output
command: sleep 15
async: 2
poll: 0
register: result1
- name: asyn task
command: sleep 2
register: result2
- name: showing result1
debug:
var: result1
var1: result2
- name: debugging output
debug: msg=this is the {‌{ result1 }} and {‌{ result2 }}
# with_items:
# - {‌{ result1 }}
# - {‌{ result2 }}
getting below error,
changed: [vishwa]
TASK [showing result1] **************************************************************************************************
fatal: [rudra]: FAILED! => {"changed": false, "msg": "'var1' is not a valid option in debug"}
fatal: [arya]: FAILED! => {"changed": false, "msg": "'var1' is not a valid option in debug"}
fatal: [vishwa]: FAILED! => {"changed": false, "msg": "'var1' is not a valid option in debug"}
to retry, use: --limit #/home/admin/ansibledemo/asynch.retry
First point, debug module doesn't have an option var1 and so the error from showing result1 task. You probably have figured that out and written debugging output task with msg option.
This brings to the second point, registering status of an asynchronous task. Since you are using poll: 0, the task would be executing in asynchronous mode so the result may not be immediately available to the registered variable. Use async_status to check the result as described here. Also, in your scenario you should use async value more than sleep period.
An example,
- name: Wait for asynchronous job to end
async_status:
jid: '{{ result1.ansible_job_id }}'
register: job_result
until: job_result.finished
retries: 5
Maybe below playbook file could help you to fulfill your requirement. If not then I have some other alternatives too.
---
- hosts: all
gather_facts: false
name: "[ Debug Senario Test ]"
become: true
become_method: sudo
tasks:
- name: "[[ Command Task 1 ]]"
command: "echo Command Task 1"
register: register1
- name: "[[ Command Task 2 ]]"
command: "echo Command Task 2"
register: register2
- name: "[[ Display Registers ]]"
command: "echo {{ item.stdout }}"
with_items:
- "{{ register1 }}"
- "{{register2}}"
register: register3
- debug:
msg: "{{ register3 }}"
debug does not have a parameter var1, or var. You can't just create a parameter for a module. The error message is clear.
If you would like to store the result1 or result2 then use set_fact module.
You are executing a task in async running in the background. Async task follows a fire and forget execution. That task will be triggered and in running state while the other is being triggered. Use 2 https://docs.ansible.com/ansible/latest/modules/async_status_module.html
for fetching the result of the async task.
Ref:https://docs.ansible.com/ansible/2.7/user_guide/playbooks_async.html

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