Evaluating return code in ansible conditional - ansible

I'm working on automating a task which needs to append the latest version of software to a file. I don't want it to do this multiple times for the same version.
It looks at the following example file:
var software releases = new Array(
"4.3.0",
"4.4.0",
"4.5.0",
"4.7.0",
"4.8.0",
"4.11.0",
"4.12.1",
"4.14.0",
"4.15.0",
"4.16.0",
);
the defaults main.yml would pass in something like
VERSION: 4.16.2
code
- name: register version check
shell: cat /root/versions.js | grep -q {{VERSION}}
register: current_version
- debug: msg="The registered variable output is {{ current_version.rc }}"
- name: append to versions.js
lineinfile:
dest: /root/versions.js
regexp: '^\);'
insertbefore: '^#\);'
line: " \"{{VERSION}}\",\n);"
owner: root
state: present
when: current_version.rc == 1
problem: the debug message is evaluating current_version.rc and showing me boolean values based on the grep commands output, but I can't re-use this in the when conditional to determine if the task should be run.
Edit: the output:
PLAY [localhost] **************************************************************
GATHERING FACTS ***************************************************************
ok: [localhost]
TASK: [test | register version check] *****************************************
failed: [localhost] => {"changed": true, "cmd": "cat /root/versions.js | grep -q 3.19.2", "delta": "0:00:00.003570", "end": "2015-12-17 00:24:49.729078", "rc": 1, "start": "2015-12-17 00:24:49.725508", "warnings": []}
FATAL: all hosts have already failed -- aborting
PLAY RECAP ********************************************************************
to retry, use: --limit #/root/site.retry
localhost : ok=1 changed=0 unreachable=0 failed=1

As nikobelia pointed out in the comments, grep returns an exit code of 1 when it doesn't match any lines. Ansible then interprets this (actually any status code other than 0 from a shell/command task) as an error and so promptly fails.
You can tell Ansible to ignore the response code from the shell/command task by using ignore_errors. Although with grep this will ignore actual errors (given by a return code of 2) so instead you might want to use failed_when like this:
- name: register version check
shell: cat /root/versions.js | grep -q {{VERSION}}
register: current_version
failed_when: current_version.rc == 2

Related

Ansible command module expecting nonzero result

I am trying to use the Ansible command module to get information from my inventory, but I am expecting the servers to return a non-zero value as a result. The non-zero values are causing the inventory to report as fail.
I was expecting the following sequence to fail when a certain group of process names are present on any of the inventory.
- name: Make sure all expected processes are down
tags: ensure_processes_down
block:
- name: Find list of unexpected processes
command:
argv:
- /usr/bin/pgrep
- --list-name
- unexpected_processes_pattern_here
register: pgrep_status
- name: Ensure no processes were found
# pgrep returns 0 if matches were found
assert:
that: pgrep_status.rc != 0
msg: 'Cannot run while these these processes are up: {{ pgrep_status.stdout }}'
I am new to Ansible and suspect that I am not using the command module correctly.
Is there another way to run a command where the expected return code is non-zero?
If the command in a command task fails, then the entire task fails, which means no more tasks will be executed on that particular host. Your second task in which you check pgrep_status will never run. You have a couple options here.
You can set ignore_errors: true, which will ignore the error condition and move on to the next task. E.g:
- hosts: localhost
gather_facts: false
tasks:
- command:
argv:
- false
register: status
ignore_errors: true
- assert:
that: status.rc != 0
Which will produce this result:
PLAY [localhost] ***************************************************************
TASK [command] *****************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "cmd": "False", "msg": "[Errno 2] No such file or directory: b'False'", "rc": 2, "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
TASK [assert] ******************************************************************
ok: [localhost] => {
"changed": false,
"msg": "All assertions passed"
}
PLAY RECAP *********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
However, rather than running the command task and then checking the result in a second task, you can set a failed_when condition on the command task:
- name: Find list of unexpected processes
command:
argv:
- /usr/bin/pgrep
- --list-name
- unexpected_processes_pattern_here
register: pgrep_status
failed_when: pgrep_status.rc == 0
This command will only fail when the command returns successfully (i.e.,with an exit code of 0).
See the "Defining failure" section of the docs for more information.

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.

How to use 'shell' output stored in a variable in further task in Ansible correctly?

Lately I've been tinkering with Ansible and want to achieve following on my test node using Ansible:
Clone a repo to the Remote Node
grep a packagelist from a textfile within the git repo
Load the tools from the packagelist into an Ansible variable
Install these tools via the Ansible package module or apt module.
So far I've got this:
---
- hosts: all
become: true
vars:
username: foobar
tasks:
- name: Install git package
package:
name: git
state: present
- name: Clone GIT Repo
git:
repo: https://github.com/somerepo/.dotfiles.git
dest: /home/{{ username }}/.dotfiles
clone: yes
version: master
update: yes
force: yes
- name: Set permission on folder ~/.dotfiles
file:
dest: /home/{{ username }}/.dotfiles
recurse: yes
owner: "{{ username }}"
group: "{{ username }}"
mode: "0775"
- name: Extract list of needed tools from install.sh
shell:
cmd: grep "^tools=" /home/{{ username }}/.dotfiles/install.sh | tr '"' " " | cut -c7-
register: grep_output
- name: Install following packages "{{ grep_output }}"
apt:
name: "{{ grep_output }}"
state: present
update_cache: yes
The shell command gathers a list with tools:
grep "^tools=" install.sh |tr '"' " " | cut -c7-
with the STDOUT output
zsh neovim vim tmux ranger stow wget curl git fzf
Those values I want to store in an Ansible variable and install it via package plugin.
When I run the playbook on my test VM
PLAY [all] *******************************************************************************************************************
TASK [Gathering Facts] *******************************************************************************************************
[WARNING]: Platform linux on host vm-mint21 is using the discovered Python interpreter at /usr/bin/python3.10, but future
installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-
core/2.12/reference_appendices/interpreter_discovery.html for more information.
ok: [vm-mint21]
TASK [Install git package] ***************************************************************************************************
ok: [vm-mint21]
TASK [Clone GIT Repo] ********************************************************************************************************
changed: [vm-mint21]
TASK [Set permission on folder ~/.dotfiles] **********************************************************************************
changed: [vm-mint21]
TASK [Extract list of needed tools from install.sh] **************************************************************************
changed: [vm-mint21]
TASK [Install following packages "{'changed': True, 'stdout': ' zsh neovim vim tmux ranger stow wget curl git fzf ', 'stderr': '', 'rc': 0, 'cmd': 'grep "^tools=" /home/soeren/.dotfiles/install.sh | tr \'"\' " " | cut -c7-', 'start': '2022-09-12 22:33:37.056914', 'end': '2022-09-12 22:33:37.061264', 'delta': '0:00:00.004350', 'msg': '', 'stdout_lines': [' zsh neovim vim tmux ranger stow wget curl git fzf '], 'stderr_lines': [], 'failed': False}"] ***
fatal: [vm-mint21]: FAILED! => {"changed": false, "msg": "argument 'package' is of type <class 'dict'> and we were unable to convert to list: <class 'dict'> cannot be converted to a list"}
PLAY RECAP *******************************************************************************************************************
vm-mint21 : ok=5 changed=3 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
I get following error:
argument 'package' is of type <class 'dict'> and we were unable to convert to list: <class 'dict'> cannot be converted to a list"
To narrow down such issue one may have a look into the following example
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Create STDOUT output
command: 'echo "1 2 3 4 5 6"'
register: result
- name: Show full result
debug:
var: result
- name: Show '.stdout'
debug:
msg: "The result in '.stdout': {{ result.stdout }} is of type {{ result.stdout | type_debug }}"
resulting into an output of
TASK [Show full result] ****************************************************************************
ok: [localhost] =>
result:
changed: true
cmd:
- echo
- 1 2 3 4 5 6
delta: '0:00:00.009634'
end: '2022-09-12 23:00:00.788265'
failed: false
rc: 0
start: '2022-09-12 23:00:00.778631'
stderr: ''
stderr_lines: []
stdout: 1 2 3 4 5 6
stdout_lines:
- 1 2 3 4 5 6
TASK [Show '.stdout'] ****************************************************************************
ok: [localhost] =>
msg: 'The result in ''.stdout'': 1 2 3 4 5 6 is of type AnsibleUnsafeText'
In this way one can debug the Return Values of an Ansible module
Ansible modules normally return a data structure that can be registered into a variable, or seen directly when output by the ansible program. Each module can optionally document its own unique return values (visible through ansible-doc and on the main docsite).
in a variable which became registered.
Registered variables may be simple variables, list variables, dictionary variables, or complex nested data structures. The documentation for each module includes a RETURN section describing the return values for that module.
For your specific case you can see that the shell module has a <yourVarName>.stderr and <yourVarName>.stdout, whereby you are interested in .stdout only. The type of the registered output is text marked as unsafe so it will not become templated.
To feed the registered output to the next module package or apt, which will require a list, you can just use the from Zeittounator within the comments mentioned name: "{{ grep_output.stdout.split(' ') }}" line. It will split the string into a list based on a delimiter.
You can modify your last task as below. Using "split" function you can convert string to a list.
- name: Install following packages "{{ grep_output }}"
apt:
name: "{{ item }}"
state: present
update_cache: yes
loop: "{{ grep_output.stdout.split() }}"

Ansible fail task but continue running

I'm currently writing a bunch of checks for Ansible.
It involves contacting remote machines and performing a check.
Based on the result of this check, I make a decision whether it failed or not.
This is the important bit: the task itself never fails. It merely returns a message. I register the result and then analyze it. And based on that I decide if it's a failure or not.
The thing is, I want to add a flag that allows tests to keep running instead of failing.
So the code looks like this:
- name: Check fail.
fail:
msg: "Test failed"
when: "failfast_flag and <the actual check>"
The problem there is, if I make failfast_flag false, it doesn't output red anymore.
I want it to continue with the next tests, in that case, but I also want it to color red, indicating it's an error/fail.
How do I accomplish this?
EDIT: Thanks for the suggestions, I'll give them a try in a bit.
Not sure i fully understood your question, but you could use a - block and rescue structure in order to treat the failures and continue the playbook in failure scenarios:
example:
- block:
- name: Check fail.
fail:
msg: "Test failed"
when: "failfast_flag and <the actual check>"
rescue:
- name: do somthing when the task above fails
command: <do somthing>
This is a combination of #edrupado's answer and #Jack's comment.
The idea is to move the error up to the task where you register the value and use a rescue block to fail with an informative message, skipping the fail with a flag.
I have no idea what your actual task to collect data is. I used a dummy example cheking for the presence of a dir/file. You should be able to adapt to you actual scenario.
---
- name: Dummy POC just to test feasability
hosts: localhost
gather_facts: false
tasks:
- block:
- name: Make sure /whatever dir exists
stat:
path: /whatever
register: whatever
failed_when: not whatever.stat.exists | bool
- debug:
msg: "/whatever exists: Good !"
rescue:
- fail:
msg: /whatever dir must exist.
ignore_errors: "{{ ignore_flag | default(false) | bool }}"
- block:
- name: Make sure /tmp dir exists
stat:
path: /tmp
register: tmpdir
failed_when: not tmpdir.stat.exists | bool
- debug:
msg: "/tmp exists: Good !"
rescue:
- fail:
msg: /tmp dir must exist.
ignore_errors: "{{ ignore_flag | default(false) | bool }}"
Which gives:
$ ansible-playbook /tmp/test.yml
PLAY [Dummy POC just to test feasability] *************************************************************************************************************************************************************************
TASK [Make sure /whatever dir exists] *****************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "failed_when_result": true, "stat": {"exists": false}}
TASK [fail] *******************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "/whatever dir must exist."}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0
$ ansible-playbook /tmp/test.yml -e ignore_flag=true
PLAY [Dummy POC just to test feasability] *************************************************************************************************************************************************************************
TASK [Make sure /whatever dir exists] *****************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "failed_when_result": true, "stat": {"exists": false}}
TASK [fail] *******************************************************************************************************************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "/whatever dir must exist."}
...ignoring
TASK [Make sure /tmp dir exists] **********************************************************************************************************************************************************************************
ok: [localhost]
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "/tmp exists: Good !"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=1 ignored=1

How to use Ansible fail modules to print multiple custom messages

I want my playbook to fail under two conditions.
If the SQL query's results is not returned by the database
If the SQL query's result returned has the string "775"
Below Playbook works fine and does the job.
- name: "Play 1"
hosts: localhost
- name: "Search for Number"
command: >
mysql --user="{{ DBUSER }}" --password="{{ DBPASS }}" deployment
--host=localhost --batch --skip-column-names
--execute="SELECT num FROM deploy_tbl"
register: command_result
failed_when: command_result.stdout is search('775') or command_result.rc != 0
when: Layer == 'APP'
I now wanted to print custom messages for both the conditions so I added the below lines after the above playbook code.
- name: Make Sure the mandatory input values are passed
fail:
msg: "This REQ is already Deployed. Hence retry with a Unique Number."
when: command_result.stdout is search('775')
- name: Make Sure the mandatory input values are passed
fail:
msg: "Database is not reachable."
when: command_result.rc != 0
This give me syntax error when running the playbook.
I'm on the latest version of Ansible.
Can you please tell me how can we update my plabook to print either of the custom message based on condition check ?
You should probably post your entire playbook at once in a single code block because there are absolutely no syntax problems in the bits you pasted out.
Meanwhile, if you want to achieve your requirement, you first have to understand that, by default, your playbook will stop as soon as an error is raised on a task. So in your case, the fail tasks will never be played as they are after the failing task.
One way to go around this problem would be to use ignore_error on your task.
In most cases, I find that blocks and their error handling feature are better suited to handle such scenarios.
The example below uses a block. I faked the sql call with a playbook var (that you can modify to play around) and a debug task using the same when conditions as yours.
I also modified the way to call fail so that you only have to do it once with the ternary filter.
If you are not familiar with the >- notation in yaml, have a look at a definition of scalar blocks. I used that to limit line length and ease readability.
---
- hosts: localhost
gather_facts: false
vars:
command_result:
stdout: "775"
rc: "0"
tasks:
- block:
- name: Fake task to mimic sql query
debug:
var: command_result
failed_when: command_result.stdout is search('775') or command_result.rc != 0
rescue:
- name: Make Sure the mandatory input values are passed
fail:
msg: >-
{{
command_result.stdout is search('775') |
ternary(
"This REQ is already Deployed. Hence retry with a Unique Number.",
"Database is not reachable."
)
}}
which gives:
PLAY [localhost] *********************************************************************
TASK [Fake task to mimic sql query] **************************************************
fatal: [localhost]: FAILED! => {
"command_result": {
"rc": "0",
"stdout": "775"
},
"failed_when_result": true
}
TASK [Make Sure the mandatory input values are passed] *******************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "This REQ is already Deployed. Hence retry with a Unique Number."}
PLAY RECAP ***************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0

Resources