Abstract
| Goal |
Manage task control and task errors in Ansible Playbooks. |
| Objectives |
|
| Sections |
|
| Lab |
|
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.
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: presentThese 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.comThe 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 }}"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.
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
- name: Show ping_results
ansible.builtin.debug:
var: ping_results 
The | |
The contents of the |
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": {
"changed": false,
"msg": "All items completed",
"results": [
{
"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...The | |
The | |
Each item in the |
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...
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.
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 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 == falseObserve 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
| Operation | Example |
|---|---|
| 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: >-
[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
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
fail_msg: >-
The value of the 'facility' variable ({{ facility }}) is not valid.
Use one of the following values: {{ facility_choices }}The | |
The first task verifies that the user provided a value for the | |
The second task verifies that the value of the |
Although not shown in this example, the playbook might proceed to configure logging settings by using the cisco.ios.ios_logging_global module.
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")