Bookmark this page

Chapter 4. Implementing Task Control

Abstract

Goal Manage task control, handlers, and task errors in Ansible Playbooks.
Objectives
  • Use loops to write efficient tasks and use conditions to control when to run tasks.

  • Implement a task that runs only when another task changes the managed host.

  • Control what happens when a task fails, and what conditions cause a task to fail.

Sections
  • Writing Loops and Conditional Tasks (and Guided Exercise)

  • Implementing Handlers (and Guided Exercise)

  • Handling Task Failure (and Guided Exercise)

Lab
  • Implementing Task Control

Writing Loops and Conditional Tasks

Objectives

  • Use loops to write efficient tasks and use conditions to control when to run tasks.

Task Iteration with Loops

Using loops makes it possible to avoid writing multiple tasks that use the same module. For example, instead of writing five tasks to ensure that five users exist, you can write one task that iterates over a list of five users to ensure that they all exist.

To iterate a task over a set of items, you can use the loop keyword. You can configure loops to repeat a task using each item in a list, the contents of each of the files in a list, a generated sequence of numbers, or using more complicated structures.

This section covers simple loops that iterate over a list of items. Consult the documentation for more advanced looping scenarios.

Simple Loops

A simple loop iterates a task over a list of items. The loop keyword is added to the task, and takes as a value the list of items over which the task should be iterated. The loop variable item holds the value used during each iteration.

Consider the following snippet that uses the ansible.builtin.service module twice to ensure that two network services are running:

- name: Postfix is running
  ansible.builtin.service:
    name: postfix
    state: started

- name: Dovecot is running
  ansible.builtin.service:
    name: dovecot
    state: started

These two tasks can be rewritten to use a simple loop so that only one task is needed to ensure that both services are running:

- name: Postfix and Dovecot are running
  ansible.builtin.service:
    name: "{{ item }}"
    state: started
  loop:
    - postfix
    - dovecot

The loop can use a list provided by a variable.

In the following example, the mail_services variable contains the list of services that need to be running.

vars:
  mail_services:
    - postfix
    - dovecot

tasks:
  - name: Postfix and Dovecot are running
    ansible.builtin.service:
      name: "{{ item }}"
      state: started
    loop: "{{ mail_services }}"

Loops over a List of Dictionaries

The loop list does not need to be a list of simple values.

In the following example, each item in the list is actually a dictionary. Each dictionary in the example has two keys, name and groups, and the value of each key in the current item loop variable can be retrieved with the item['name'] and item['groups'] variables, respectively.

- name: Users exist and are in the correct groups
  user:
    name: "{{ item['name'] }}"
    state: present
    groups: "{{ item['groups'] }}"
  loop:
    - name: jane
      groups: wheel
    - name: joe
      groups: root

The outcome of the preceding task is that the user jane is present and a member of the group wheel, and that the user joe is present and a member of the group root.

Earlier-style Loop Keywords

Before Ansible 2.5, most playbooks used a different syntax for loops. Multiple loop keywords were provided, which used the with_ prefix, followed by the name of an Ansible look-up plug-in (an advanced feature not covered in detail in this course). This syntax for looping is very common in existing playbooks, but will probably be deprecated at some point in the future.

Some examples are listed in the following table:

Table 4.1. Earlier-style Ansible Loops

Loop keywordDescription
with_items Behaves the same as the loop keyword for simple lists, such as a list of strings or a list of dictionaries. Unlike loop, if lists of lists are provided to with_items, they are flattened into a single-level list. The item loop variable holds the list item used during each iteration.
with_file Requires a list of control node file names. The item loop variable holds the content of a corresponding file from the file list during each iteration.
with_sequence Requires parameters to generate a list of values based on a numeric sequence. The item loop variable holds the value of one of the generated items in the generated sequence during each iteration.

The following playbook shows an example of the with_items keyword:

  vars:
    data:
      - user0
      - user1
      - user2
  tasks:
    - name: "with_items"
      ansible.builtin.debug:
        msg: "{{ item }}"
      with_items: "{{ data }}"

Important

Since Ansible 2.5, the recommended way to write loops is to use the loop keyword.

However, you should still understand the earlier syntax, especially with_items, because it is widely used in existing playbooks. You are likely to encounter playbooks and roles that continue to use with_* keywords for looping.

Any task using the earlier syntax can be converted to use loop in conjunction with Ansible filters. You do not need to know how to use Ansible filters to do this. The Ansible documentation contains a good reference on how to convert the earlier loops to the new syntax, as well as examples of how to loop over items that are not simple lists. See the "Migrating from with_X to loop" section of the Ansible User Guide.

You might encounter tasks from earlier playbooks that contain with_* keywords.

Advanced looping techniques are beyond the scope of this course. All iteration tasks in this course can be implemented with either the with_items or the loop keyword.

Using Register Variables with Loops

The register keyword can also capture the output of a task that loops. The following snippet shows the structure of the register variable from a task that loops:

[student@workstation loopdemo]$ cat loop_register.yml
---
- name: Loop Register Test
  gather_facts: no
  hosts: localhost
  tasks:
    - name: Looping Echo Task
      ansible.builtin.shell: "echo This is my item: {{ item }}"
      loop:
        - one
        - two
      register: echo_results1

    - name: Show echo_results variable
      ansible.builtin.debug:
        var: echo_results2

1

The echo_results variable is registered.

2

The contents of the echo_results variable are displayed to the screen.

Running the preceding playbook yields the following output:

[student@workstation loopdemo]$ ansible-navigator run -m stdout loop_register.yml

PLAY [Loop Register Test] ******************************************************

TASK [Looping Echo Task] *******************************************************
changed: [localhost] => (item=one)
changed: [localhost] => (item=two)

TASK [Show echo_results variable] **********************************************
ok: [localhost] => {
    "echo_results": {1
        "changed": true,
        "msg": "All items completed",
        "results": [2
            {3
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": "echo This is my item: one",
                "delta": "0:00:00.004519",
                "end": "2022-06-29 17:32:54.065165",
                "failed": false,
                ...output omitted...
                "item": "one",
                "msg": "",
                "rc": 0,
                "start": "2022-06-29 17:32:54.060646",
                "stderr": "",
                "stderr_lines": [],
                "stdout": "This is my item: one",
                "stdout_lines": [
                    "This is my item: one"
                ]
            },
            {4
                "ansible_loop_var": "item",
                "changed": true,
                "cmd": "echo This is my item: two",
                "delta": "0:00:00.004175",
                "end": "2022-06-29 17:32:54.296940",
                "failed": false,
                ...output omitted...
                "item": "two",
                "msg": "",
                "rc": 0,
                "start": "2022-06-29 17:32:54.292765",
                "stderr": "",
                "stderr_lines": [],
                "stdout": "This is my item: two",
                "stdout_lines": [
                    "This is my item: two"
                ]
            }
        ],5
        "skipped": false
    }
}
...output omitted...

1

The { character indicates that the start of the echo_results variable is composed of key-value pairs.

2

The results key contains the results from the previous task. The [ character indicates the start of a list.

3

The start of task metadata for the first item (indicated by the item key). The output of the echo command is found in the stdout key.

4

The start of task result metadata for the second item.

5

The ] character indicates the end of the results list.

In the preceding example, the results key contains a list. In the next example, the playbook is modified so that the second task iterates over this list:

[student@workstation loopdemo]$ cat new_loop_register.yml
---
- name: Loop Register Test
  gather_facts: no
  hosts: localhost
  tasks:
    - name: Looping Echo Task
      ansible.builtin.shell: "echo This is my item: {{ item }}"
      loop:
        - one
        - two
      register: echo_results

    - name: Show stdout from the previous task.
      ansible.builtin.debug:
        msg: "STDOUT from previous task: {{ item['stdout'] }}"
      loop: "{{ echo_results['results'] }}"

After running the preceding playbook, you see the following output:

PLAY [Loop Register Test] ******************************************************

TASK [Looping Echo Task] *******************************************************
changed: [localhost] => (item=one)
changed: [localhost] => (item=two)

TASK [Show stdout from the previous task.] *************************************
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: one', 'stderr': '', 'rc': 0, 'cmd': 'echo This is my item: one', 'start': '2022-06-29 17:41:15.558529', 'end': '2022-06-29 17:41:15.563615', 'delta': '0:00:00.005086', 'msg': '', 'invocation': {'module_args': {'_raw_params': 'echo This is my item: one', '_uses_shell': True, 'warn': False, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['This is my item: one'], 'stderr_lines': [], 'failed': False, 'item': 'one', 'ansible_loop_var': 'item'}) => {
    "msg": "STDOUT from previous task: This is my item: one"
}
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: two', 'stderr': '', 'rc': 0, 'cmd': 'echo This is my item: two', 'start': '2022-06-29 17:41:15.810566', 'end': '2022-06-29 17:41:15.814932', 'delta': '0:00:00.004366', 'msg': '', 'invocation': {'module_args': {'_raw_params': 'echo This is my item: two', '_uses_shell': True, 'warn': False, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['This is my item: two'], 'stderr_lines': [], 'failed': False, 'item': 'two', 'ansible_loop_var': 'item'}) => {
    "msg": "STDOUT from previous task: This is my item: two"
}
...output omitted...

Running Tasks Conditionally

Ansible can use conditionals to run tasks or plays when certain conditions are met. For example, you can use a conditional to determine available memory on a managed host before Ansible installs or configures a service.

Conditionals help you to differentiate between managed hosts and assign them functional roles based on the conditions that they meet. Playbook variables, registered variables, and Ansible facts can all be tested with conditionals. Operators to compare strings, numeric data, and Boolean values are available.

The following scenarios illustrate the use of conditionals in Ansible.

  • Define a hard limit in a variable (for example, min_memory) and compare it against the available memory on a managed host.

  • Capture the output of a command and evaluate it to determine whether a task completed before taking further action. For example, if a program fails, then a batch is skipped.

  • Use Ansible facts to determine the managed host network configuration and decide which template file to send (for example, network bonding or trunking).

  • Evaluate the number of CPUs to determine how to properly tune a web server.

  • Compare a registered variable with a predefined variable to determine if a service changed. For example, test the MD5 checksum of a service configuration file to see if the service is changed.

Conditional Task Syntax

The when statement is used to run a task conditionally. It takes as a value the condition to test. If the condition is met, the task runs. If the condition is not met, the task is skipped.

One of the simplest conditions that can be tested is whether a Boolean variable is true or false. The when statement in the following example causes the task to run only if run_my_task is true.

---
- name: Simple Boolean Task Demo
  hosts: all
  vars:
    run_my_task: true

  tasks:
    - name: httpd package is installed
      ansible.builtin.dnf:
        name: httpd
      when: run_my_task

Note

Boolean variables can have the value true or false.

In Ansible content, you can express those values in other ways: True, yes, or 1 are also accepted for true; and False, no, or 0 are also accepted for false. You might see true and yes, or false and no used interchangeably to express Boolean values in existing Ansible content.

Ansible YAML files are based on the YAML 1.1 standard, but the YAML 1.2 standard specifies that you can only use true or false to set Boolean values. For this reason, you might see gradual standardization toward using only true or false for Boolean values in playbooks and other Ansible files, even though the equivalent ways to express those values are still valid. Whether Ansible should eventually use only those ways of expressing Boolean values is an open question and an ongoing discussion in the Ansible community.

Important

When using true/false conditions such as in the preceding example, you must be very careful to make sure that your variable is treated by Ansible as a Boolean and not a string.

Starting with Ansible Core 2.12, strings are always treated by when conditionals as true Booleans if they contain any content. (The default automation execution environment in Ansible Automation Platform 2.2 uses Ansible Core 2.13.)

Therefore, if the run_my_task variable in the preceding example were written as shown in the following example then it would be treated as a string with content and have the Boolean value true, and the task would run. This is probably not the behavior that you want.

  run_my_task: "false"

If it had been written as shown in the next example, however, it would be treated as the Boolean value false and the task would not run:

  run_my_task: false

To ensure that this is the case, you could rewrite the previous when condition to convert an accidental string value to a Boolean and to pass Boolean values unchanged:

      when: run_my_task | bool

The next example is a bit more sophisticated, and tests whether the my_service variable has a value. If it does, the value of my_service is used as the name of the package to install. If the my_service variable is not defined, then the task is skipped without an error.

---
- name: Test Variable is Defined Demo
  hosts: all
  vars:
    my_service: httpd

  tasks:
    - name: "{{ my_service }} package is installed"
      ansible.builtin.dnf:
        name: "{{ my_service }}"
      when: my_service is defined

The following table shows some operations that you can use when working with conditionals:

Table 4.2. Example Conditionals

OperationExample
Equal (value is a string) ansible_facts['machine'] == "x86_64"
Equal (value is numeric) max_memory == 512
Less than min_memory < 128
Greater than min_memory > 256
Less than or equal to min_memory <= 256
Greater than or equal to min_memory >= 512
Not equal to min_memory != 512
Variable exists min_memory is defined
Variable does not exist min_memory is not defined
Boolean variable is true. The values of 1, True, or yes evaluate to true. memory_available
Boolean variable is false. The values of 0, False, or no evaluate to false. not memory_available
First variable's value is present as a value in second variable's list ansible_facts['distribution'] in supported_distros

The last entry in the preceding table might be confusing at first. The following example illustrates how it works.

In the example, the ansible_facts['distribution'] variable is a fact determined during the Gathering Facts task, and identifies the managed host's operating system distribution. The supported_distros variable was created by the playbook author, and contains a list of operating system distributions that the playbook supports. If the value of ansible_facts['distribution'] is in the supported_distros list, the conditional passes and the task runs.

---
- name: Demonstrate the "in" keyword
  hosts: all
  gather_facts: yes
  vars:
    supported_distros:
      - RedHat
      - Fedora
  tasks:
    - name: Install httpd using dnf, where supported
      ansible.builtin.dnf:
        name: http
        state: present
      when: ansible_facts['distribution'] in supported_distros

Important

Observe the indentation of the when statement. Because the when statement is not a module variable, it must be placed outside the module by being indented at the top level of the task.

A task is a YAML dictionary, and the when statement is one more key in the task, just like the task's name and the module it uses. A common convention places any when keyword that might be present after the task's name and the module (and module arguments).

Testing Multiple Conditions

One when statement can be used to evaluate multiple conditionals. To do so, conditionals can be combined with either the and or or keywords, and grouped with parentheses.

The following snippets show some examples of how to express multiple conditions.

  • If a conditional statement should be met when either condition is true, then use the or statement. For example, the following condition is met if the machine is running either Red Hat Enterprise Linux or Fedora:

    when: ansible_facts['distribution'] == "RedHat" or ansible_facts['distribution'] == "Fedora"
  • With the and operation, both conditions have to be true for the entire conditional statement to be met. For example, the following condition is met if the remote host is a Red Hat Enterprise Linux 9.0 host, and the installed kernel is the specified version:

    when: ansible_facts['distribution_version'] == "9.0" and ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"

    The when keyword also supports using a list to describe a list of conditions. When a list is provided to the when keyword, all the conditionals are combined using the and operation. The example below demonstrates another way to combine multiple conditional statements using the and operator:

    when:
      - ansible_facts['distribution_version'] == "9.0"
      - ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"

    This format improves readability, a key goal of well-written Ansible Playbooks.

  • You can express more complex conditional statements by grouping conditions with parentheses. This ensures that they are correctly interpreted.

    For example, the following conditional statement is met if the machine is running either Red Hat Enterprise Linux 9 or Fedora 34. This example uses the greater-than character (>) so that the long conditional can be split over multiple lines in the playbook, to make it easier to read.

    when: >
        ( ansible_facts['distribution'] == "RedHat" and
          ansible_facts['distribution_major_version'] == "9" )
        or
        ( ansible_facts['distribution'] == "Fedora" and
        ansible_facts['distribution_major_version'] == "34" )

Combining Loops and Conditional Tasks

You can combine loops and conditionals.

In the following example, the ansible.builtin.dnf module installs the mariadb-server package if there is a file system mounted on / with more than 300 MiB free. The ansible_facts['mounts'] fact is a list of dictionaries, each one representing facts about one mounted file system. The loop iterates over each dictionary in the list, and the conditional statement is not met unless a dictionary is found that represents a mounted file system where both conditions are true.

- name: install mariadb-server if enough space on root
  ansible.builtin.dnf:
    name: mariadb-server
    state: latest
  loop: "{{ ansible_facts['mounts'] }}"
  when: item['mount'] == "/" and item['size_available'] > 300000000

Important

When you use when with loop for a task, the when statement is checked for each item.

The following example also combines conditionals and register variables. This playbook restarts the httpd service only if the postfix service is running:

---
- name: Restart HTTPD if Postfix is Running
  hosts: all
  tasks:
    - name: Get Postfix server status
      ansible.builtin.command: /usr/bin/systemctl is-active postfix 1
      register: result2

    - name: Restart Apache HTTPD based on Postfix status
      ansible.builtin.service:
        name: httpd
        state: restarted
      when: result.rc == 03

1

Is Postfix running?

2

Save information on the module's result in a variable named result.

3

Evaluate the output of the Postfix task. If the exit code of the systemctl command is 0, then Postfix is active and this task restarts the httpd service.

Revision: rh294-9.0-c95c7de