Ansible-lint - Rule 306

If you are like me you like to keep your scripting as safe as possible when running against a production system. Sometimes my bash scripts can be a bit overkill, but it’s better to be safe than sorry.

It is therefore worth mentioning an update to ansible-lint shipped in 4.1.0 which has added the following rule:

[306] Shells that use pipes should set the pipefail option

Without the pipefail option set, a shell command that implements a pipeline can fail and still return 0. If any part of the pipeline other than the terminal command fails, the whole pipeline will still return 0, which may be considered a success by Ansible. Pipefail is available in the bash shell.

– Source: https://docs.ansible.com/ansible-lint/rules/default_rules.html

This is actually a really good idea when automating shell tasks with the use of pipes. In Ansible we should try and avoid using shell tasks where possible, but it is sometimes unavoidable. So what does that actually mean in real life?

Let’s consider a simple shell script that demonstrates the problem:

#!/usr/bin/env bash

false | echo "This task should fail..."
[[ "${?}" -gt 0 ]] && echo "Task: FAIL" || echo "Task: PASS"

true | echo "This task should pass..."
[[ "${?}" -gt 0 ]] && echo "Task: FAIL" || echo "Task: PASS"

When we run it, we would expect the false to cause a “FAIL”, however…

[ xmanning@dreadfort:/tmp ] $ ./pipefail_test.sh
This task should fail...
Task: PASS
This task should pass...
Task: PASS

We get two “PASS” results! Why you ask? This is because whilst the false command produces a return code of 1, the echo after the pipe exits with a return of 0.

Ansible will see this as a successful shell task, and really it probably is not.

So we use the bash builtin option set -o pipefail.

#!/usr/bin/env bash
set -o pipefail

false | echo "This task should fail..."
[[ "${?}" -gt 0 ]] && echo "Task: FAIL" || echo "Task: PASS"

true | echo "This task should pass..."
[[ "${?}" -gt 0 ]] && echo "Task: FAIL" || echo "Task: PASS"

Now lets test again:

[ xmanning@dreadfort:/tmp ] $ ./pipefail_test.sh
This task should fail...
Task: FAIL
This task should pass...
Task: PASS

Ahhh, that’s better!

So how do we put that into our Ansible? The annoying thing about some of these lint rules is that your playbooks and roles will fail but there are no examples of how best to fix them.

Here is one of the ways of passing the test and making your shell tasks safer, ensure that you add set -o pipefail to the beginning of all shell tasks that use pipes.

---

- name: Ensure a list of IPs is captured from /etc/hosts
  shell: >
    set -o pipefail && \
      grep -E "^[0-9a-f]" /etc/hosts | awk '{ print $1 }'    
  register: etc_hosts_iplist
  args:
    executable: /bin/bash

Take note that you need to specify the executable: /bin/bash as Ansible will likely use /bin/sh which does not support the pipefail option and the task will just fail.

Having used set -o pipefail && at the beginning of the command, should there be any problem with the grep or awk command then the task will now show as Failed whereas previously the task would show as Changed.

And just like that you have explored safer shell scripting through Ansible.

To read more about safer bash scripts, see Tom Van Eyck’s article