Chapter 5: Implementing Task Control

Exercise: Handling task failure

You will make a task fail on purpose, then handle that failure four different ways: ignore it, rescue it with a block, stop a read-only command from reporting changes, and invent your own failure condition.

The inventory in ~/control-errors puts servera.lab.example.com in the databases group.

Handling task failure

  • Ignore failed commands during playbook execution.
  • Override what counts as failed or changed for a task.
  • Implement block, rescue and always.

Prerequisites: Complete control-node setup and the preceding lessons in this chapter. Run commands as student on workstation unless a step names another machine.

From the project directory, run the relevant home-lab check. For an exercise with several checkpoints, grade each state before changing or removing it:

lab grade control-errors

The grader reads project files and host state. A passing report covers the listed checks; also perform the task’s independent connection, repeat-run, and reboot checks where requested.

Remove the artificial failure condition and compare the recap with the previous recovered run.

Record the published result first. The grader checks the original requirements, so a changed name or value may intentionally fail those checks.

Explore failure reporting in playbook.yml without confusing it with the underlying host state.

  • Use the intentionally nonexistent web package http to demonstrate an ordinary failure, then an ignored failure.
  • Demonstrate recovery that installs MariaDB after the web task fails and a final action that starts MariaDB for both successful and recovered paths.
  • Correct the package to httpd and prove that the recovery path is skipped.
  • A read-only time check must capture and display its result without reporting a change. Finally, make the successful web task report failure deliberately and explain why recovery runs.
  • Keep evidence of each stage before advancing to the next checkpoint.

Write a play with a deliberate mistake

In ~/control-errors, create playbook.yml. The value http is intentionally wrong: there is no package with that name.

---
- name: Task Failure Exercise
  hosts: databases
  vars:
    web_package: http
    db_package: mariadb-server
    db_service: mariadb

  tasks:
    - name: Install {{ web_package }} package
      ansible.builtin.dnf:
        name: "{{ web_package }}"
        state: present

    - name: Install {{ db_package }} package
      ansible.builtin.dnf:
        name: "{{ db_package }}"
        state: present

Run it and watch the play stop

[student@workstation control-errors]$ ansible-navigator run -m stdout playbook.yml
...output omitted...
TASK [Install http package] ****************************************************
fatal: [servera.lab.example.com]: FAILED! => {"changed": false, "failures":
["No package http available."], "msg": "Failed to install some of the specified
packages", "rc": 1, "results": []}

PLAY RECAP *********************************************************************
servera.lab.example.com    : ok=1    changed=0    unreachable=0    failed=1  ...

The second task never ran.

Ignore the error

Add ignore_errors: true to the first task and run again. The failure is reported as ...ignoring, the second task runs, and the recap shows ignored=1.

    - name: Install {{ web_package }} package
      ansible.builtin.dnf:
        name: "{{ web_package }}"
        state: present
      ignore_errors: true

Rescue the failure with a block

Remove ignore_errors. Put the web package task in a block, the database package task in rescue, and add an always section that starts the database service.

  tasks:
    - name: Attempt to set up a webserver
      block:
        - name: Install {{ web_package }} package
          ansible.builtin.dnf:
            name: "{{ web_package }}"
            state: present
      rescue:
        - name: Install {{ db_package }} package
          ansible.builtin.dnf:
            name: "{{ db_package }}"
            state: present
      always:
        - name: Start {{ db_service }} service
          ansible.builtin.service:
            name: "{{ db_service }}"
            state: started

Run it: block fails, rescue and always run

[student@workstation control-errors]$ ansible-navigator run -m stdout playbook.yml
...output omitted...
TASK [Install http package] ****************************************************
fatal: [servera.lab.example.com]: FAILED! => {"changed": false, ...}

TASK [Install mariadb-server package] ******************************************
ok: [servera.lab.example.com]

TASK [Start mariadb service] ***************************************************
changed: [servera.lab.example.com]

PLAY RECAP *********************************************************************
servera.lab.example.com    : ok=3    changed=1    unreachable=0    failed=0
skipped=0    rescued=1    ignored=0

Note failed=0 and rescued=1: the rescue handled the failure.

Fix the package name

Change web_package to httpd and run again. The block succeeds, so rescue is skipped, and always still runs.

Add a read-only command

Before the block, add a task that runs date and registers the output, and a task that prints it.

    - name: Check local time
      ansible.builtin.command: date
      register: command_result

    - name: Print local time
      ansible.builtin.debug:
        var: command_result.stdout

Run the playbook twice: Check local time reports changed every time, although date changes nothing on the host. The command module cannot tell the difference between reading and changing.

Tell Ansible the command changes nothing

Add changed_when: false to Check local time and run again. It now reports ok, and the recap shows changed=0, while the time is still captured and printed.

Invent a failure condition

Add failed_when: web_package == "httpd" to the task inside the block, and run the playbook.

      block:
        - name: Install {{ web_package }} package
          ansible.builtin.dnf:
            name: "{{ web_package }}"
            state: present
          failed_when: web_package == "httpd"
TASK [Install httpd package] ***************************************************
fatal: [servera.lab.example.com]: FAILED! => {"changed": false,
"failed_when_result": true, "msg": "Nothing to do", "rc": 0, "results": []}

TASK [Install mariadb-server package] ******************************************
ok: [servera.lab.example.com]
...output omitted...
servera.lab.example.com    : ok=5    changed=0    unreachable=0    failed=0
skipped=0    rescued=1    ignored=0

dnf actually succeeded ("Nothing to do", rc: 0), but failed_when changed how it was reported, and that report was enough to send the play into rescue.

Finish

Check your work with lab grade control-errors, then put the exercise away with lab finish control-errors.