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:

- 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_itemsBehaves 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_fileRequires 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_sequenceRequires 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 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:

[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:

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

---
- 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, or 1

  • False, no, or 0

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.

  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 thanmin_memory < 128
Greater thanmin_memory > 256
Less than or equal tomin_memory <= 256
Greater than or equal tomin_memory >= 512
Not equal tomin_memory != 512
Variable existsmin_memory is defined
Variable does not existmin_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 listansible_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: 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:

    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 
      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
# 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:

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:

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 by notify 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 under tasks 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 a changed result (for example, a package is already installed and the task reports ok), the handler is not notified. Ansible notifies handlers only if the task reports the changed 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
# 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.

- 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:

---
- 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:

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:

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

- 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:

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

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

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

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:

- 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 the block clause fail.
  • always: Defines the tasks that always run independently of the success or failure of tasks defined in the block and rescue clauses.

The following example shows how to implement a block in a playbook.

  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
---
- 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:

  1. Under the #Fail fast message comment, add a task that uses the ansible.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. The min_ram_mb variable is defined in the vars.yml file and has a value of 256.
    • Is running Red Hat Enterprise Linux.
  2. Under the #Install all packages comment, add a task named Ensure required packages are present to install the latest version of any missing packages. Required packages are specified by the packages variable, which is defined in the vars.yml file.

  3. Under the #Enable and start services comment, add a task to start services. All services specified by the services variable, which is defined in the vars.yml file, should be started and enabled. Provide an appropriate name for the task.

  4. 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 the web_config_files variable in the vars.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.

  5. The play configures the remote host to listen for standard HTTPS requests. Under the #Configure the firewall comment, add a task to configure firewalld.

    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.

  6. Define the Restart web service handler.

    When triggered, this task should restart the web service defined by the web_service variable, defined in the vars.yml file.

# 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
# 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…

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部