ONLY FOR SELF STUDY, NO COMMERCIAL USAGE!!!
Chapter 4. Implementing Task Control
Writing Loops and Conditional Tasks
Task Iteration with Loops
Using loops makes it possible to avoid writing multiple tasks that use the same module.
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.
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:
yaml
- 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:
yaml
- 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.
yaml
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.
yaml
- 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 keyword | Description |
---|---|
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:
yaml
vars:
data:
- user0
- user1
- user2
tasks:
- name: "with_items"
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ data }}"
Important note:
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.
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.
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:
yaml
[student@workstation loopdemo]$ cat loop_register.yml
---
- name: Loop Register Test
gather_facts: false
hosts: localhost
tasks:
- name: Looping Echo Task
ansible.builtin.shell: "echo This is my item: {{ item }}"
loop:
- one
- two
register: echo_results
- name: Show echo_results variable
ansible.builtin.debug:
var: echo_results
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": {
"changed": true,
"msg": "All items completed",
"results": [
{
"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"
]
},
{
"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"
]
}
],
"skipped": false
}
}
...output omitted...
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:
yaml
# new_loop_register.yml
---
- name: Loop Register Test
gather_facts: false
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.
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.
yaml
---
- 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(NTOE: the newest YAML only allow to use true/false
for boolean ):
-
True
,yes
, or1
-
False
,no
, or0
Starting with Ansible Core 2.12, strings are always treated by when
conditionals as true
Booleans if they contain any content.
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.
yaml
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:
yaml
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:
yaml
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.
yaml
---
- 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
Operation | Example |
---|---|
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.
yaml
---
- name: Demonstrate the "in" keyword
hosts: all
gather_facts: true
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
Importantyaml
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.
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:yamlwhen: 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:yamlwhen: 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 thewhen
keyword, all the conditionals are combined using theand
operation. The example below demonstrates another way to combine multiple conditional statements using theand
operator:yamlwhen: - 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.
yamlwhen: > ( 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.
yaml
- 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:
yaml
---
- name: Restart HTTPD if Postfix is Running
hosts: all
tasks:
- name: Get Postfix server status
ansible.builtin.command: /usr/bin/systemctl is-active postfix
register: result
- name: Restart Apache HTTPD based on Postfix status
ansible.builtin.service:
name: httpd
state: restarted
when: result.rc == 0
References
Loops --- Ansible Documentation
Tests --- Ansible Documentation
Conditionals --- Ansible Documentation
What Makes A Valid Variable Name --- Variables --- Ansible Documentation
For more information on the change to Boolean handling in conditionals in community Ansible 5 (and Ansible Core 2.12) and later, see https://docs.ansible.com/ansible/latest/porting_guides/porting_guide_5.html#deprecated
Example
yaml
# playbook.yml
# - The first task installs the MariaDB required packages only when hosts are Redhat, and the second task ensures that the MariaDB service is running.
---
- name: Test example for control flow
hosts: database_prod
vars:
mariadb_packages:
- mariadb-server
- python3-PyMySQL
tasks:
- name: Installing MariaDB
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop: "{{ mariadb_packages }}"
when: ansible_facts['distribution'] == "RedHat"
- name: Starting MariaDB
ansible.builtin.service:
name: mariadb
state: started
enabled: true
Implementing Handlers
Ansible Handlers
Handlers are tasks that respond to a notification triggered by other tasks. Tasks only notify their handlers when the task changes something on a managed host. Each handler is triggered by its name after the play's block of tasks.
If no task notifies the handler by name then the handler does not run. If one or more tasks notify the handler, the handler runs once after all other tasks in the play have completed. Because handlers are tasks, administrators can use the same modules in handlers that they would use for any other task.
Normally, handlers are used to reboot hosts and restart services.
Important:
Always use unique names for your handlers. You might have unexpected results if more than one handler uses the same name.
Handlers can be considered as inactive tasks that only get triggered when explicitly invoked using a notify
statement. The following snippet shows how the Apache server is only restarted by the restart apache
handler when a configuration file is updated and notifies it:
yaml
tasks:
- name: copy demo.example.conf configuration template
ansible.builtin.template:
src: /var/lib/templates/demo.example.conf.template
dest: /etc/httpd/conf.d/demo.example.conf
notify:
- restart apache
handlers:
- name: restart apache
ansible.builtin.service:
name: httpd
state: restarted
In the previous example, the restart apache
handler is triggered when notified by the template
task that a change happened. A task might call more than one handler in its notify
section. Ansible treats the notify
statement as an array and iterates over the handler names:
yaml
tasks:
- name: copy demo.example.conf configuration template
ansible.builtin.template:
src: /var/lib/templates/demo.example.conf.template
dest: /etc/httpd/conf.d/demo.example.conf
notify:
- restart mysql
- restart apache
handlers:
- name: restart mysql
ansible.builtin.service:
name: mariadb
state: restarted
- name: restart apache
ansible.builtin.service:
name: httpd
state: restarted
Describing the Benefits of Using Handlers
As discussed in the Ansible documentation, there are some important things to remember about using handlers:
- Handlers always run in the order specified by the
handlers
section of the play. They do not run in the order in which they are listed bynotify
statements in a task, or in the order in which tasks notify them. - Handlers normally run after all other tasks in the play complete. A handler called by a task in the
tasks
part of the playbook does not run until all tasks undertasks
have been processed. (Some minor exceptions to this exist.) - Handler names exist in a per-play namespace. If two handlers are incorrectly given the same name, only one of them runs.
- Even if more than one task notifies a handler, the handler runs one time. If no tasks notify it, the handler does not run.
- If a task that includes a
notify
statement does not report achanged
result (for example, a package is already installed and the task reportsok
), the handler is not notified. Ansible notifies handlers only if the task reports thechanged
status.
Important:
Handlers are meant to perform an extra action when a task makes a change to a managed host. They should not be used as a replacement for normal tasks.
References
Handlers: running operations on change --- Ansible Documentation
Example
yml
# Handlers example: the configure_webapp.yml playbook file. This playbook installs and configures a web application server. When the web application server configuration changes, the playbook triggers a restart of the appropriate service.
---
- name: Web application server is deployed
hosts: webapp
vars:
packages:
- nginx
- php-fpm
- firewalld
web_service: nginx
app_service: php-fpm
firewall_service: firewalld
firewall_service_rules:
- http
web_config_src: files/nginx.conf.standard
web_config_dst: /etc/nginx/nginx.conf
app_config_src: files/php-fpm.conf.standard
app_config_dst: /etc/php-fpm.conf
tasks:
- name: Installing Web application server
ansible.builtin.dnf:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
- name: Starting servcices...
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop:
- "{{ web_service }}"
- "{{ app_service }}"
- "{{ firewall_service }}"
- name: Going through firewall
ansible.posix.firewalld:
service: "{{ item }}"
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_service_rules }}"
- name: Downlaoding config files and restart web services
ansible.builtin.copy:
src: "{{ web_config_src }}"
dest: "{{ web_config_dst }}"
mode: "0644"
notify:
- restart web service
- name: Downlaoding config files and restart web services
ansible.builtin.copy:
src: "{{ app_config_src }}"
dest: "{{ app_config_dst }}"
mode: "0644"
notify:
- restart app service
handlers:
- name: restart web service
ansible.builtin.service:
name: "{{ web_service }}"
state: restarted
- name: restart app service
ansible.builtin.service:
name: "{{ app_service }}"
state: restarted
Results:
# First RUN:
[student@workstation control-handlers]$ ansible-navigator run -m stdout configure_webapp.yml
PLAY [Web application server is deployed] **************************************
TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
TASK [Installing Web application server] ***************************************
ok: [servera.lab.example.com] => (item=nginx)
ok: [servera.lab.example.com] => (item=php-fpm)
ok: [servera.lab.example.com] => (item=firewalld)
TASK [Starting servcices...] ***************************************************
ok: [servera.lab.example.com] => (item=nginx)
ok: [servera.lab.example.com] => (item=php-fpm)
ok: [servera.lab.example.com] => (item=firewalld)
TASK [Going through firewall] **************************************************
ok: [servera.lab.example.com] => (item=http)
TASK [Downlaoding config files and restart web services] ***********************
changed: [servera.lab.example.com]
TASK [Downlaoding config files and restart web services] ***********************
changed: [servera.lab.example.com]
RUNNING HANDLER [restart web service] ******************************************
changed: [servera.lab.example.com]
RUNNING HANDLER [restart app service] ******************************************
changed: [servera.lab.example.com]
PLAY RECAP *********************************************************************
servera.lab.example.com : ok=8 changed=4 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
#Second RUN: (No handler running as no files changed during 2nd time run)
[student@workstation control-handlers]$ ansible-navigator run -m stdout configure_webapp.yml
PLAY [Web application server is deployed] **************************************
TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
TASK [Installing Web application server] ***************************************
ok: [servera.lab.example.com] => (item=nginx)
ok: [servera.lab.example.com] => (item=php-fpm)
ok: [servera.lab.example.com] => (item=firewalld)
TASK [Starting servcices...] ***************************************************
ok: [servera.lab.example.com] => (item=nginx)
ok: [servera.lab.example.com] => (item=php-fpm)
ok: [servera.lab.example.com] => (item=firewalld)
TASK [Going through firewall] **************************************************
ok: [servera.lab.example.com] => (item=http)
TASK [Downlaoding config files and restart web services] ***********************
ok: [servera.lab.example.com]
TASK [Downlaoding config files and restart web services] ***********************
ok: [servera.lab.example.com]
PLAY RECAP *********************************************************************
servera.lab.example.com : ok=6 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
[student@workstation control-handlers]$
Handling Task Failure
Ansible evaluates the return code of each task to determine whether the task succeeded or failed. Normally, when a task fails Ansible immediately skips all subsequent tasks.
However, sometimes you might want to have play execution continue even if a task fails. For example, you might expect that a particular task could fail, and you might want to recover by conditionally running some other task. A number of Ansible features can be used to manage task errors.
Ignoring Task Failure
By default, if a task fails, the play is aborted. However, this behavior can be overridden by ignoring failed tasks. You can use the ignore_errors
keyword in a task to accomplish this.
The following snippet shows how to use ignore_errors
in a task to continue playbook execution on the host even if the task fails. For example, if the notapkg
package does not exist then the ansible.builtin.dnf
module fails, but having ignore_errors
set to true
allows execution to continue.
yaml
- name: Latest version of notapkg is installed
ansible.builtin.dnf:
name: notapkg
state: latest
ignore_errors: true
Forcing Execution of Handlers After Task Failure
Normally when a task fails and the play aborts on that host, any handlers that had been notified by earlier tasks in the play do not run. If you set the force_handlers: true
keyword on the play, then notified handlers are called even if the play aborted because a later task failed.
Important:
If you have ignore_errors: true
set on a task or for the task's play, if that task fails the failure is ignored. In that case, the play keeps running and handlers still run, even if you have force_handlers: false
set, unless some other error causes the play to fail.
The following snippet shows how to use the force_handlers
keyword in a play to force execution of the notified handler even if a subsequent task fails:
yaml
---
- hosts: all
force_handlers: true
tasks:
- name: a task which always notifies its handler
ansible.builtin.command: /bin/true
notify: restart the database
- name: a task which fails because the package doesn't exist
ansible.builtin.dnf:
name: notapkg
state: latest
handlers:
- name: restart the database
ansible.builtin.service:
name: mariadb
state: restarted
Important:
Remember that handlers are notified when a task reports a changed
result but are not notified when it reports an ok
or failed
result.
If you set force_handlers: true
on the play, then any handlers that have been notified are run even if a later task failure causes the play to fail. Otherwise, handlers are not run at all when a play fails.
Setting force_handlers: true
on a play does not cause handlers to be notified for tasks that report ok
or failed
; it only causes the handlers to run that have already been notified before the point at which the play failed.
Specifying Task Failure Conditions
You can use the failed_when
keyword on a task to specify which conditions indicate that the task has failed. This is often used with command modules that might successfully execute a command, but where the command's output indicates a failure.
For example, you may check for failure by searching for a word or phrase in the output of a command. The following example shows one way that you can use the failed_when
keyword in a task:
yaml
tasks:
- name: Run user creation script
ansible.builtin.shell: /usr/local/bin/create_users.sh
register: command_result
failed_when: "'Password missing' in command_result.stdout"
The ansible.builtin.fail
module can also be used to force a task failure. You could instead write that example as two tasks:
yaml
tasks:
- name: Run user creation script
ansible.builtin.shell: /usr/local/bin/create_users.sh
register: command_result
ignore_errors: true
- name: Report script failure
ansible.builtin.fail:
msg: "The password is missing in the output"
when: "'Password missing' in command_result.stdout"
You can use the ansible.builtin.fail
module to provide a clear failure message for the task. This approach also enables delayed failure, which means that you can run intermediate tasks to complete or roll back other changes.
or it can also based on the return code
yaml
- name: Fail task when both files are identical
ansible.builtin.raw: diff foo/file1 bar/file2
register: diff_cmd
failed_when: diff_cmd.rc == 0 or diff_cmd.rc >= 2
You can also combine multiple conditions for failure. This task will fail if both conditions are true:
yaml
- name: Check if a file exists in temp and fail task if it does
ansible.builtin.command: ls /tmp/this_should_not_be_here
register: result
failed_when:
- result.rc == 0
- '"No such" not in result.stdout'
If you want the task to fail when only one condition is satisfied, change the failed_when
definition to
failed_when: result.rc == 0 or "No such" not in result.stdout
If you have too many conditions to fit neatly into one line, you can split it into a multi-line YAML value with >
.
yaml
- name: example of many failed_when conditions with OR
ansible.builtin.shell: "./myBinary"
register: ret
failed_when: >
("No such file or directory" in ret.stdout) or
(ret.stderr != '') or
(ret.rc == 10)
Specifying When a Task Reports "Changed" Results
When a task makes a change to a managed host, it reports the changed
state and notifies handlers. When a task does not need to make a change, it reports ok
and does not notify handlers.
Use the changed_when
keyword to control how a task reports that it has changed something on the managed host. For example, the ansible.builtin.command
module in the next example validates the httpd
configuration on a managed host.
This task validates the configuration syntax, but nothing is actually changed on the managed host. Subsequent tasks can use the value of the httpd_config_status
variable.
It normally would always report changed
when it runs. To suppress that change report, changed_when: false
is set so that it only reports ok
or failed
.
yaml
- name: Validate httpd configuration
ansible.builtin.command: httpd -t
changed_when: false
register: httpd_config_status
The following example uses the ansible.builtin.shell
module and only reports changed
if the string "Success" is found in the output of the registered variable. If it does report changed
, then it notifies the handler.
yaml
tasks:
- ansible.builtin.shell:
cmd: /usr/local/bin/upgrade-database
register: command_result
changed_when: "'Success' in command_result.stdout"
notify:
- restart_database
handlers:
- name: restart_database
ansible.builtin.service:
name: mariadb
state: restarted
Ansible Blocks and Error Handling
In playbooks, blocks are clauses that logically group tasks, and can be used to control how tasks are executed. For example, a task block can have a when
keyword to apply a conditional to multiple tasks:
yaml
- name: block example
hosts: all
tasks:
- name: installing and configuring DNF versionlock plugin
block:
- name: package needed by dnf
ansible.builtin.dnf:
name: python3-dnf-plugin-versionlock
state: present
- name: lock version of tzdata
ansible.builtin.lineinfile:
dest: /etc/yum/pluginconf.d/versionlock.list
line: tzdata-2016j-1
state: present
when: ansible_distribution == "RedHat"
Blocks also allow for error handling in combination with the rescue
and always
statements. If any task in a block
fails, then rescue
tasks are executed to recover.
After the tasks in the block
clause run, as well as the tasks in the rescue
clause if there was a failure, then tasks in the always
clause run.
To summarize:
block
: Defines the main tasks to run.rescue
: Defines the tasks to run if the tasks defined in theblock
clause fail.always
: Defines the tasks that always run independently of the success or failure of tasks defined in theblock
andrescue
clauses.
The following example shows how to implement a block in a playbook.
yaml
tasks:
- name: Upgrade DB
block:
- name: upgrade the database
ansible.builtin.shell:
cmd: /usr/local/lib/upgrade-database
rescue:
- name: revert the database upgrade
ansible.builtin.shell:
cmd: /usr/local/lib/revert-database
always:
- name: always restart the database
ansible.builtin.service:
name: mariadb
state: restarted
The when
condition on a block
clause also applies to its rescue
and always
clauses if present.
References
Error Handling in Playbooks --- Ansible Documentation
Error Handling --- Blocks --- Ansible Documentation
Example
yaml
---
- name: How to handle errors example
hosts: databases
vars:
web_package: httpd
db_package: mariadb-server
db_service: mariadb
tasks:
- name: Running date cmd
ansible.builtin.command: /usr/bin/date
register: command_result
changed_when: false # the above 'date' command task will not reported 'changed' if we set this to false
- name: Display the cmd result
ansible.builtin.debug:
var: command_result.stdout
- name: Doing block things
block:
- name: Installing web package
ansible.builtin.dnf:
name: "{{ web_package }}"
state: present
failed_when: web_package == "httpd" # Force to report failure if the 'web_package' name is httpd
rescue: # this will run only there is failure in above 'block'
- name: Installing db package
ansible.builtin.dnf:
name: "{{ db_package }}"
state: present
always:
- name: Anyway Starting DB server
ansible.builtin.service:
name: "{{ db_service }}"
state: started
enabled: true
Result : Look carefully at the output. The failed_when
keyword changes the status that the task reports after the task runs; it does not change the behavior of the task itself.
However, the reported failure might change the behavior of the rest of the play. Because that task was in a block and reported that it failed, the Install mariadb-server package
task in the block's rescue
section was run.
[student@workstation control-errors]$ ansible-navigator run -m stdout playbook.yml
PLAY [How to handle errors example] ********************************************
TASK [Gathering Facts] *********************************************************
ok: [servera.lab.example.com]
TASK [Running date cmd] ********************************************************
>>ok: [servera.lab.example.com]<<
TASK [Display the cmd result] **************************************************
ok: [servera.lab.example.com] => {
"command_result.stdout": "Wed Aug 28 10:42:15 AM EDT 2024"
}
TASK [Installing web package] **************************************************
fatal: [servera.lab.example.com]: FAILED! => {"changed": false, >>"failed_when_result": true<<, "msg": "Nothing to do", "rc": 0, "results": []}
TASK [Installing db package] ***************************************************
ok: [servera.lab.example.com]
TASK [Anyway Starting DB server] ***********************************************
ok: [servera.lab.example.com]
PLAY RECAP *********************************************************************
servera.lab.example.com : ok=5 changed=0 unreachable=0 >>failed=0<< skipped=0 >>rescued=1<< ignored=0
Chapter4 TEST
Requests:
-
Under the
#Fail fast message
comment, add a task that uses theansible.builtin.fail
module. Provide an appropriate name for the task.This task should only be executed when the remote system does not meet the following minimum requirements:
- Has at least the amount of RAM specified by the
min_ram_mb
variable. Themin_ram_mb
variable is defined in thevars.yml
file and has a value of256
. - Is running Red Hat Enterprise Linux.
- Has at least the amount of RAM specified by the
-
Under the
#Install all packages
comment, add a task namedEnsure required packages are present
to install the latest version of any missing packages. Required packages are specified by thepackages
variable, which is defined in thevars.yml
file. -
Under the
#Enable and start services
comment, add a task to start services. All services specified by theservices
variable, which is defined in thevars.yml
file, should be started and enabled. Provide an appropriate name for the task. -
Under the
#Block of config tasks
comment, add a task block to the play. This block contains two tasks:-
A task to ensure that the directory specified by the
ssl_cert_dir
variable exists on the remote host. This directory stores the web server's certificates. -
A task to copy all files specified by the
web_config_files
variable to the remote host. Examine the structure of theweb_config_files
variable in thevars.yml
file. Configure the task to copy each file to the correct destination on the remote host.This task should trigger the
Restart web service
handler if any of these files are changed on the remote server.
Additionally, a debug task is executed if either of the two tasks above fail. In this case, the task prints the following message:
One or more of the configuration changes failed, but the web service is still active.
Provide an appropriate name for all tasks.
-
-
The play configures the remote host to listen for standard HTTPS requests. Under the
#Configure the firewall
comment, add a task to configurefirewalld
.Ensure that the task configures the remote host to accept standard HTTP and HTTPS connections. The configuration changes must be effective immediately and persist after a reboot. Provide an appropriate name for the task.
-
Define the
Restart web service
handler.When triggered, this task should restart the web service defined by the
web_service
variable, defined in thevars.yml
file.
yaml
# vars.yml
min_ram_mb: 256
web_service: httpd
web_package: httpd
ssl_package: mod_ssl
fw_service: firewalld
fw_package: firewalld
services:
- "{{ web_service }}"
- "{{ fw_service }}"
packages:
- "{{ web_package }}"
- "{{ ssl_package }}"
- "{{ fw_package }}"
ssl_cert_dir: /etc/httpd/conf.d/ssl
web_config_files:
- src: server.key
dest: "{{ ssl_cert_dir }}"
- src: server.crt
dest: "{{ ssl_cert_dir }}"
- src: ssl.conf
dest: /etc/httpd/conf.d
- src: index.html
dest: /var/www/html
yaml
# playbook.yml
---
- name: Playbook Control Lab
hosts: webservers
vars_files: vars.yml
tasks:
#Fail fast message
- name: Show failed system requirements message
ansible.builtin.fail:
msg: "The {{ inventory_hostname }} did not meet minimum reqs."
when: >
ansible_facts['memtotal_mb'] < min_ram_mb or
ansible_facts['distribution'] != "RedHat"
#Install all packages
- name: Ensure required packages are present
ansible.builtin.dnf:
name: "{{ packages }}"
state: latest
#Enable and start services
- name: Ensure services are started and enabled
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop: "{{ services }}"
#Block of config tasks
- name: Setting up the SSL cert directory and config files
block:
- name: Create SSL cert directory
ansible.builtin.file:
path: "{{ ssl_cert_dir }}"
state: directory
- name: Copy config files
ansible.builtin.copy:
src: "{{ item['src'] }}"
dest: "{{ item['dest'] }}"
loop: "{{ web_config_files }}"
notify: Restart web service
rescue:
- name: Configuration error message
ansible.builtin.debug:
msg: >
One or more of the configuration
changes failed, but the web service
is still active.
#Configure the firewall
- name: Ensure web server ports are open
ansible.posix.firewalld:
service: "{{ item }}"
immediate: true
permanent: true
state: enabled
loop:
- http
- https
#Add handlers
handlers:
- name: Restart web service
ansible.builtin.service:
name: "{{ web_service }}"
state: restarted
output:
[student@workstation control-review]$ ansible-navigator run \
> -m stdout playbook.yml
PLAY [Playbook Control Lab] ****************************************************
TASK [Gathering Facts] *********************************************************
ok: [serverb.lab.example.com]
TASK [Show failed system requirements message] *********************************
skipping: [serverb.lab.example.com]
TASK [Ensure required packages are present] ************************************
changed: [serverb.lab.example.com]
TASK [Ensure services are started and enabled] *********************************
changed: [serverb.lab.example.com] => (item=httpd)
ok: [serverb.lab.example.com] => (item=firewalld)
TASK [Create SSL cert directory] ***********************************************
changed: [serverb.lab.example.com]
TASK [Copy config files] *******************************************************
changed: [serverb.lab.example.com] => (item={'src': 'server.key', 'dest': '/etc/httpd/conf.d/ssl'})
changed: [serverb.lab.example.com] => (item={'src': 'server.crt', 'dest': '/etc/httpd/conf.d/ssl'})
changed: [serverb.lab.example.com] => (item={'src': 'ssl.conf', 'dest': '/etc/httpd/conf.d'})
changed: [serverb.lab.example.com] => (item={'src': 'index.html', 'dest': '/var/www/html'})
TASK [Ensure web server ports are open] ****************************************
changed: [serverb.lab.example.com] => (item=http)
changed: [serverb.lab.example.com] => (item=https)
RUNNING HANDLER [Restart web service] ******************************************
changed: [serverb.lab.example.com]
PLAY RECAP *********************************************************************
serverb.lab.example.com : ok=7 changed=6 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
Verify Web service
[student@workstation control-review]$ curl -k -vvv https://serverb.lab.example.com
* Trying 172.25.250.11:443...
* Connected to serverb.lab.example.com (172.25.250.11) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* CAfile: /etc/pki/tls/certs/ca-bundle.crt
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Unknown (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
* subject: C=US; L=Default City; O=Red Hat; OU=Training; CN=serverb.lab.example.com
* start date: Nov 13 15:52:18 2018 GMT
* expire date: Aug 9 15:52:18 2021 GMT
* issuer: C=US; L=Default City; O=Red Hat; OU=Training; CN=serverb.lab.example.com
* SSL certificate verify result: self-signed certificate (18), continuing anyway.
* TLSv1.2 (OUT), TLS header, Unknown (23):
> GET / HTTP/1.1
> Host: serverb.lab.example.com
> User-Agent: curl/7.76.1
> Accept: */*
>
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Unknown (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* old SSL session ID is stale, removing
* TLSv1.2 (IN), TLS header, Unknown (23):
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Fri, 30 Aug 2024 15:21:47 GMT
< Server: Apache/2.4.51 (Red Hat Enterprise Linux) OpenSSL/3.0.1
< Last-Modified: Fri, 30 Aug 2024 14:35:03 GMT
< ETag: "24-620e77f28279e"
< Accept-Ranges: bytes
< Content-Length: 36
< Content-Type: text/html; charset=UTF-8
<
Configured for both HTTP and HTTPS.
* Connection #0 to host serverb.lab.example.com left intact
TO BE CONTINUED...