Force ansible to fail (return non-zero code) if there are any 'changed' tasks

Viewed 1082

I want to a perform convergence test for a playbook. Convergence test checks if second run of the same playbook make no changes.

Is there a way to say Ansible "make exit code non-zero if there was a task in 'changed' state?". I can parse stdout, but it's ugly.

Example:

set -e
ansible-playbook -i inventory.yaml site.yaml  # normal run
ansible-playbook --help-me-here -i inventory.yaml site.yaml  # should return non-zero if anything changed.

2 Answers

Another option than the one I put into the comments is to use molecule. It automatically sets up linters as well as syntax-, unit- and idempotence tests. I found it to be most useful when developing ansible-role-sssh, although molecule has dropped vagrant support since then.

$ python3 -m venv ./role-devel
$ source ./role-devel/bin/activate
$ pip install "molecule[lint,docker]"
[...]
$ molecule init role  yourrole
--> Initializing new role yourrole...
Initialized role in /path/to/yourrole successfully.

Now edit your tasks/main.yml:

---
# tasks file for yourrole
- debug:
    var=ansible_date_time.iso8601
- name: Update motd
  template:
    src: "motd.j2"
    dest: "/etc/motd"

and create templates/motd.j2:

THIS SYSTEM IS PROUDLY MANAGED BY ANSIBLE SINCE {{ ansible_date_time.iso8601 }}

Now, when you run your tests, molecule will run an idempotence check and recognize that Update motd has changed both in the first and second run. Thus, the idempotence test will fail:

$ molecule test

[...]

--> Scenario: 'default'
--> Action: 'converge'

    PLAY [Converge] ****************************************************************

    TASK [Gathering Facts] *********************************************************
    ok: [instance]

    TASK [Include yourrole] ********************************************************

    TASK [yourrole : debug] ********************************************************
    ok: [instance] => {
        "ansible_date_time.iso8601": "2020-03-21T11:31:05Z"
    }

    TASK [yourrole : Update motd] **************************************************
    changed: [instance]

    PLAY RECAP *********************************************************************
    instance                   : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

--> Scenario: 'default'
--> Action: 'idempotence'
ERROR: Idempotence test failed because of the following tasks:
* [instance] => yourrole : Update motd
An error occurred during the test sequence action: 'idempotence'. Cleaning up.

[...]

$ echo $?
1

As per the documentation there is no straightforward option to do this,

You can do the following,

Create a boolean variable that tracks if changed should be considered a failure. Evaluate the control inside the playbook.

For example, in your playbook you can handle the flag like so,

- hosts: localhost
  gather_facts: no
  tasks:
  - debug:
      msg: "Test message"
#   This task reports 'changed' always because we set changed_when to True
    changed_when: True
    register: result
#   We can control whether or not to fail the task if task reports changed. 
#   The task fails if 
#     it actually fails or 
#     when it changes and when we ask it to consider change as a failure
#   Note: Defaults to understand changed just means changed and not failed
    failed_when:
      - result is failed or (result.changed and changed_means_failure|default(False)|bool)

Execute your playbook

set -e

ansible-playbook -i inventory.yaml site.yaml  # normal run
ansible-playbook --extra-vars "changed_means_failure=True" -i inventory.yaml site.yaml  # should return non-zero if anything changed.
Related