Bookmark this page

Chapter 5.  Implementing Task Control

Abstract

Goal

Manage task control and task errors in Ansible Playbooks.

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

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

  • Create a workflow job template in automation controller that launches a branching chain of jobs based on the success or failure of each job in the workflow sequence.

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

  • Handling Task Failure (and Guided Exercise)

  • Building a Workflow Job Template (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

You can use loops 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 References section 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 example, which uses two tasks to ensure that two hosts are reachable:

- name: Ping host (hosta.lab.example.com)
  cisco.ios.ios_ping:
    dest: hosta.lab.example.com
    state: present

- name: Ping host (hostb.lab.example.com)
  cisco.ios.ios_ping:
    dest: hostb.lab.example.com
    state: present

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

- name: Ping hosts
  cisco.ios.ios_ping:
    dest: "{{ item }}"
    state: present
  loop:
    - hosta.lab.example.com
    - hostb.lab.example.com

The loop can use a list provided by a variable. In the following example, the task attempts to reach each host defined by the host_list variable:

- name: Ping hosts
  vars:
    host_list:
      - hosta.lab.example.com
      - hostb.lab.example.com
      - hostc.lab.example.com
      - hostd.lab.example.com
  cisco.ios.ios_ping:
    dest: "{{ item }}"
    state: present
  loop: "{{ host_list }}"

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 a dictionary. Each dictionary contains three keys: name, hash_type, and hashed_password. You can retrieve the value of each key in the current item loop variable with the item['name'], item['hash_type'], and item['hashed_password'] variables.

- name: Users exist with the correct password
  no_log: true
  cisco.ios.ios_user:
    name: "{{ item['name'] }}"
    hashed_password:
      type: "{{ item['hash_type'] }}"
      value: "{{ item['hashed_password'] }}"
    update_password: always
    state: present
  loop:
    - name: ansible
      hash_type: 5
      hashed_password: "$IjvDh1wt$O6ceAhpuYesiPJh.s2weQ1"
    - name: netops
      hash_type: 5
      hashed_password: "$/bqwREWG$0zUXcphlYOll0anjs/tEs1"

The outcome of the preceding task is that the ansible user exists with the $IjvDh1wt$O6ceAhpuYesiPJh.s2weQ1 MD5 hashed password, and that the netops user exists with the $/bqwREWG$0zUXcphlYOll0anjs/tEs1 MD5 hashed password.

Using Register Variables with Loops

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

---
- name: Ping hosts from IOS managed nodes
  hosts: iosxe1.lab.example.com
  gather_facts: false
  vars:
    host_list:
      - hosta.lab.example.com
      - hostb.lab.example.com
  tasks:
    - name: Ping hosts
      cisco.ios.ios_ping:
        dest: "{{ item }}"
      loop: "{{ host_list }}"
      register: ping_results 1

    - name: Show ping_results
      ansible.builtin.debug:
        var: ping_results 2

1

The ping_results variable is registered.

2

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

Running the preceding playbook produces the following output:

TASK [Ping hosts] **************************************************************
ok: [iosxe1.lab.example.com] => (item=hosta.lab.example.com)
ok: [iosxe1.lab.example.com] => (item=hostb.lab.example.com)

TASK [Show ping_results] *******************************************************
ok: [iosxe1.lab.example.com] => {
    "ping_results": { 1
        "changed": false,
        "msg": "All items completed",
        "results": [ 2
            { 3
                "ansible_loop_var": "item",
                "changed": false,
                "commands": "ping ip hosta.lab.example.com",
                "failed": false,
                "invocation": {
                    "module_args": {
                        "afi": "ip",
                        "count": null,
                        "dest": "hosta.lab.example.com",
                        "df_bit": false,
                        "egress": null,
                        "ingress": null,
                        "source": null,
                        "state": "present",
                        "timeout": null,
                        "vrf": null
                    }
                },
                "item": "hosta.lab.example.com",
                "packet_loss": "0%",
                "packets_rx": 5,
                "packets_tx": 5,
                "rtt": {
                    "avg": 2,
                    "max": 3,
                    "min": 2
                }
            },
...output omitted...

1

The ping_results variable is composed of key-value pairs.

2

The results key is a list that contains the results from the previous task.

3

Each item in the ping_results['results'] list contains metadata keys, such as commands, failed, item, and packet_loss keys.

The previous playbook output displayed all the content for the ping_results variable. The next playbook extracts and displays the round-trip time for each item in the ping_results['results'] list:

---
- name: Ping hosts from IOS managed nodes
  hosts: iosxe1.lab.example.com
  gather_facts: false
  vars:
    host_list:
      - hosta.lab.example.com
      - hostb.lab.example.com
  tasks:
    - name: Ping hosts
      cisco.ios.ios_ping:
        dest: "{{ item }}"
      loop: "{{ host_list }}"
      register: ping_results

    - name: Show ping_results round-trip times
      ansible.builtin.debug:
        var: ping_results['results'] | map(attribute='rtt')

The preceding playbook produces the following output:

...output omitted...
TASK [Show ping_results round-trip times] **************************************
ok: [iosxe1.lab.example.com] => {
    "ping_results['results'] | map(attribute='rtt')": [
        {
            "avg": 2,
            "max": 3,
            "min": 2
        },
        {
            "avg": 2,
            "max": 3,
            "min": 1
        }
    ]
}
...output omitted...

Running Tasks Conditionally

Ansible can use conditionals to run tasks or plays when certain conditions are met. 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 conditions in Ansible.

  • Run tasks on managed nodes that match a specific value for the ansible_network_os variable.

  • Stop a playbook run when a variable is not defined or does not have a valid value.

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

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

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.

Note

The YAML 1.1 standard allows you to specify Boolean values in multiple ways, such as true, True, yes, and 1 (for true) and false, False, no, and 0 (for false).

The YAML 1.2 standard specifies that you can only use true or false to set Boolean values.

Although Ansible YAML files are based on the YAML 1.1 standard, you might see gradual standardization toward using only true or false for Boolean values in playbooks and other Ansible files.

For more information on the change to Boolean handling in conditions in Ansible Core 2.12 and later, see https://docs.ansible.com/ansible/latest/porting_guides/porting_guide_5.html#deprecated.

Starting with Ansible Core 2.12, strings are always treated by when conditionals as true Booleans if they contain any content. For example, setting the value of a variable to "false" (quoted) evaluates to the true Boolean value when used by a when conditional.

When using true/false conditions, you must make sure that your variable is treated by Ansible as a Boolean and not as a string. You can do this by passing the variable to the bool filter.

# The value of the my_bool variable is a string.
# Because the string has a value, the string evaluates to true.
# The condition is not met and the task is skipped.
- name: The task is skipped
  vars:
    my_bool: "false"
  ansible.builtin.debug:
    msg: The task runs.
  when: my_bool == false

# The value of the my_bool variable is a string.
# The bool filter converts the string to a boolean.
# The condition is met and the task runs.
- name: The task runs
  vars:
    my_bool: "false"
  ansible.builtin.debug:
    msg: The task runs.
  when: my_bool | bool == false

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).

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

Table 5.1. Example Conditionals

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

The playbook in the following example illustrates how the last entry in the preceding table works:

---
- name: Demonstrate the "in" keyword
  hosts: ios
  gather_facts: false
  vars:
    facility_choices: >- 1
      [auth, cron, daemon, kern, local0, local1, local2, local3, local4,
      local5, local6, local7, lpr, mail, news, sys10, sys11, sys12, sys13,
      sys14, sys9, syslog, user, uucp]
  tasks:
    - name: Fail if 'facility' is not defined
      ansible.builtin.assert:
        that: facility is defined 2
        fail_msg: >-
          The 'facility' variable must be defined such as: -e facility=local5

    - name: Validate 'facility' choice
      ansible.builtin.assert:
        that: facility in facility_choices 3
        fail_msg: >-
          The value of the 'facility' variable ({{ facility }}) is not valid.
          Use one of the following values: {{ facility_choices }}

1

The facility_choices variable contains a list of facilities copied from documentation for the cisco.ios.ios_logging_global module.

2

The first task verifies that the user provided a value for the facility variable.

3

The second task verifies that the value of the facility variable is one of the items in the facility_choices variable list. If the value of the facility variable is an item in the facility_choices variable list, then the condition passes and the playbook continues, otherwise the condition fails and the task displays a failure message.

Although not shown in this example, the playbook might proceed to configure logging settings by using the cisco.ios.ios_logging_global module.

Testing Multiple Conditions

A when statement can evaluate multiple conditions by combining the conditions with the and or or keywords. The following examples show how to evaluate multiple conditionals.

  • With the or keyword, the conditional statement is met if either condition evaluates to true:

    when: ansible_network_os == "cisco.ios.ios" or ansible_network_os == "cisco.iosxr.iosxr"
  • With the and keyword, both conditions have to be true for the entire conditional statement to be met:

    when: ansible_facts['net_model'] == "C8000V" and ansible_facts['net_version'] == "17.06.02"
  • The when keyword also supports using a list to describe a list of conditions. Using a list improves readability, and when a list is provided to the when keyword, all the conditions have to be true for the entire conditional statement to be met:

    when:
      - ansible_facts['net_model'] == "C8000V"
      - ansible_facts['net_version'] == "17.06.02"
  • You can express more complex conditional statements by grouping conditions with parentheses. Using parentheses ensures that the conditional statements are correctly interpreted.

    The following example uses facts gathered from managed nodes and uses the greater-than symbol (>) so that the long condition can be split over multiple lines in the playbook:

    when: >
      (ansible_network_os == "cisco.ios.ios" and
        ansible_facts['net_model'] == "C8000V")
      or
      (ansible_network_os == "cisco.iosxr.iosxr" and
      ansible_facts['net_model'] == "ASR9000")

Revision: do457-2.3-7cfa22a