Subsections of Task Control

Handlers

Using Handlers

  • A task that is triggered and is executed by a successful task.

Working with Handlers

  • Define a notify statement at the level where the task is defined.
  • The notify statement should list the name of the handler that is to be executed
  • Handlers are listed at the end of the play.
  • Make sure the name of the handler matches the name of the item that is called in the notify statement, because that is what the handler is looking for.
  • Handlers can be specified as a list, so one task can call multiple handlers.

Lab

  • Define the file index.html on localhost. Use this file in the second play to set up the web server.
  • The handler is triggered from the task where the copy module is used to copy the index.html file.
  • If this task is successful, the notify statement calls the handler.
  • A second task is defined, which is intended to fail.
    ---
    - name: create file on localhost
      hosts: localhost
      tasks:
      - name: create index.html on localhost
        copy:
          content: "welcome to the webserver"
          dest: /tmp/index.html
    
    - name: set up web server
      hosts: all
      tasks:
        - name: install httpd
          yum:
            name: httpd
            state: latest
        - name: copy index.html
          copy:
            src: /tmp/index.html
            dest: /var/www/html/index.html
          notify:
            - restart_web
        - name: copy nothing - intended to fail
          copy:
            src: /tmp/nothing
            dest: /var/www/html/nothing.html
      handlers:
        - name: restart_web
          service:
            name: httpd
            state: restarted
  • All tasks up to copy index.html run successfully. However, the task copy nothing fails, which is why the handler does not run. The solution seems easy: the handler doesn’t run because the task that copies the file /tmp/nothing fails as the source file doesn’t exist.

  • Create the source file using touch /tmp/nothing on the control host and run the task again.

  • After creating the source file and running the playbook again, the handler still doesn’t run.

  • Handlers run only if the task that triggers them gives a changed status.

Run an ad hoc command to remove the /var/www/html/index.html file on the managed hosts and run the playbook again: ansible ansible2 -m file -a "name=/var/www/html/index.html state=absent"

Run the playbook again and you’ll see the handler runs.

Understanding Handler Execution and Exceptions

When a task fails, none of the following tasks run. How does that make handlers different? A handler runs only on the success of a task, but the next task in the list also runs only if the previous task was successful. What, then, is so special about handlers?

The difference is in the nature of the handler.

  • Handlers are meant to perform an extra action when a task makes a change to a host.
  • Handler should be considered an extension to the regular task.
  • A conditional task that runs only upon the success of a previous task.

Two methods to get Handlers to run even if a subsequent task fails:

force_handlers: true (More specific and preferred)

  • Used in the play header to ensure that the handler will run even if a task fails.

ignore_errors: true

  • Used in the play header to accomplish the same thing.

• Handlers are specified in a handlers section at the end of the play. • Handlers run in the order they occur in the handlers section and not in the order as triggered. • Handlers run only if the task calling them generates a changed status. • Handlers by default will not run if any task in the same play fails, unless force_handlers or ignore_errors are used. • Handlers run only after all tasks in the play where the handler is activated have been processed. You might want to define multiple plays to avoid this behavior.

Lab: Working with Handlers

1. Open a playbook with the name exercise73.yaml.

2. Define the play header:

---
- name: update the kernel
  hosts: all
  force_handlers: true
  tasks:

3. Add a task that updates the current kernel:

---
- name: update the kernel
  hosts: all
  force_handlers: true
  tasks:
  - name: update kernel
    yum:
      name: kernel
      state: latest
    notify: reboot_server

4. Add a handler that reboots the server in case the kernel was successfully updated:

---
- name: update the kernel
  hosts: all
  force_handlers: true
  tasks:
  - name: update kernel
    yum:
      name: kernel
      state: latest
    notify: reboot_server
  handlers:
  - name: reboot_server
    command: reboot

5. Run the playbook using ansible-playbook exercise73.yaml andobserve its result. Notice that the handler runs only if the kernel was updated. If the kernel already was at the latest version, nothing has changed and the handler does not run. Also notice that it wasn’t really necessary to use force_handlers in the play header, but by using it anyway, at least you now know where to use it.

Dealing with Failures

Understanding Task Execution

  • Tasks in Ansible playbooks are executed in the order they are specified.
  • If a task in the playbook fails to execute on a host, the task generates an error and the play does not further execute on that specific host.
  • This also goes for handlers: if any task that follows the task that triggers a handler fails, the handlers do not run.
  • In both of these cases, it is important to know that the tasks that have run successfully still generate their result. Because this can give an unexpected result, it is important to always restore the original situation if that happens.

any_errors_fatal

  • Used in the play header or on a block.
  • Stop executing on all hosts when a failing task is encountered

Managing Task Errors

Generically, tasks can generate three different types of results. ok

  • The tasks has run successfully but no changes were applied changed
  • The task has run successfully and changes have been applied failed
  • While running the task, a failure condition was encountered

ignore_errors: yes

  • Keep running the playbook even if a task fails

force_handlers. If

  • can be used to ensure that handlers will be executed, even if a failing task was encountered.

Lab: ignore_errors

    ---
    - name: restart sshd only if crond is running
      hosts: all
      tasks:
        - name: get the crond server status
          command: /usr/bin/systemctl is-active crond
          ignore_errors: yes
          register: result
        - name: restart sshd based on crond status
          service:
            name: sshd
            state: restarted
          when: result.rc == 0

Lab: Forcing Handlers to Run

    ---
    - name: create file on localhost
      hosts: localhost
      tasks:
      - name: create index.html on localhost
        copy:
          content: "welcome to the webserver"
          dest: /tmp/index.html
    
    - name: set up web server
      hosts: all
      force_handlers: yes
      tasks:
        - name: install httpd
          yum:
            name: httpd
            state: latest
        - name: copy index.html
          copy:
            src: /tmp/index.html
            dest: /var/www/html/index.html
          notify:
            - restart_web
        - name: copy nothing - intended to fail
          copy:
            src: /tmp/nothing
            dest: /var/www/html/nothing.html
      handlers:
        - name: restart_web
          service:
            name: httpd
            state: restarted

Specifying Task Failure Conditions

failed_when

  • conditional used to evaluate some expression.
  • Set a failure condition on a task

Lab: failed_when

    ---
    - name: demonstrating failed_when
      hosts: all
      tasks:
      - name: run a script
        command: echo hello world
        ignore_errors: yes
        register: command_result
        failed_when: "’world’ in command_result.stdout"
      - name: see if we get here
        debug:
          msg: second task executed

fail module

  • specify when a task fails.
  • Using this module makes sense only if when is used to define the exact condition when a failure should occur.

Lab: Using the fail Module

    ---
    - name: demonstrating the fail module
      hosts: all
      ignore_errors: yes
      tasks:
      - name: run a script
        command: echo hello world
        register: command_result
      - name: report a failure
        fail:
          msg: the command has failed
        when: "’world’ in command_result.stdout"
      - name: see if we get here
        debug:
          msg: second task executed
  • The ignore_errors statement has movedfrom the task definition to the play header.
  • Without this move, the message “second task executed” would never be shown because the fail module always generates a failure message.
  • The main advantage of using the fail module instead of using failed_when is that the fail module can easily be used to set a clear failure message, which is not possible when using failed_when.

Managing Changed Status

In Ansible, there are commands that change something and commands that don’t. Some commands, however, are not very obvious in reporting their status.

Lab: Change status

    ---
    - name: demonstrate changed status
      hosts: all
      tasks:
      - name: check local time
        command: date
        register: command_result
    
      - name: print local time
        debug:
          var: command_result.stdout
  • Reports a changed status, even if nothing really was changed!

  • Managing the changed status can be useful in avoiding unexpected results while running a playbook.

changed_when

  • If you set changed_when to false, the playbook reports only an ok or failed status and never reports a changed status.

Lab: Using changed_when

---
- name: demonstrate changed status
  hosts: all
  tasks:
  - name: check local time
    command: date
    register: command_result
    changed_when: false
    
  - name: print local time
    debug:
      var: command_result.stdout

Using Blocks

  • Useful when working with conditional statements.
  • A group of tasks to which a when statement can be applied.
  • As a result, if a single condition is true, multiple tasks can be executed.
  • To do so, between the tasks: statement in the play header and the actual tasks that run the specific modules, you can insert a block: statement.

Lab: Using Blocks

---
- name: simple block example
  hosts: all
  tasks:
  - name: setting up http
    block:
    - name: installing http
      yum:
        name: httpd
        state: present
    - name: restart httpod
      service:
        name: httpd
        state: started
    when: ansible_distribution == "CentOS"
  • The when statement is applied at the same level as the block definition.
  • When you define it this way, the tasks in the block are executed only if the when statement is true.

Using Blocks with rescue and always Statements

  • Blocks can be used for simple error handling as well, in such a way that if any task that is defined in the block statement fails, the tasks that are defined in the rescue section are executed.
  • Besides that, an always section can be used to define tasks that should always run, regardless of the success or failure of the tasks in the block.

Lab: Using Blocks, rescue, and always

- name: using blocks
  hosts: all
  tasks: 
  - name: intended to be successful
    block:
    - name: remove a file
      shell:
        cmd: rm /var/www/html/index.html
    - name: printing status
      debug:
        msg: block task was operated
    rescue:
    - name: create a file
      shell:
        cmd: touch /tmp/rescuefile
    - name: printing rescue status
      debug:
        msg: rescue task was operated
    always:
    - name: always write a message to logs
      shell:
        cmd: logger hello
    - name: always printing this message
      debug:
        msg: this message is always printed
  • Run this twice to see the rescue. (The file is already created so a task in the block fails)

command_warnings=False

  • Setting in ansible.cfg to avoid seeing command module warning message.

  • you cannot use a block on a loop.

  • If you need to iterate over a list of values, think of using a different solution.

Labs

Using Loops and Items

Using Loops and Items

  • Some modules enable you to provide a list that needs to be processed.
  • Many modules don’t, and in these cases, it makes sense to use a loop mechanism to iterate over a list of items.
  • Take, for instance, the yum module. While specifying the names of packages, you can use a list of packages.
  • If, however, you want to do something similar for the service module, you find out that this is not possible.
  • That is where loops come in.

Working with Loops

Install software packages using the yum module and then ensures that services installed from these packages are started using the service module:

    ---
    - name: install and start services
      hosts: ansible1
      tasks:
      - name: install packages
        yum:
          name:
          - vsftpd
          - httpd
          - samba
          state: latest
      - name: start the services
        service:
          name: "{{ item }}"
          state: started
          enabled: yes
        loop:
        - vsftpd
        - httpd
        - smb
  • A loop is defined at the same level as the service module.

  • The loop has a list of services in a list (array) statement

  • Items in the loop can be accessed by using the system internal variable item.

  • At no place in the playbook is there a definition of the variable item; the loop takes care of this.

  • When considering whether to use a loop, you should first investigate whether a module offers support for providing lists as values to the keys that are used.

  • If this is the case, just provide a list, as all items in the list can be considered in one run of the module.

  • If not, define the list using loop and provide "{{ item }}" as the value to the key.

  • When using loop, the module is activated again on each iteration.

Using Loops on Variables

  • Although it’s possible to define a loop within a task, it’s not the most elegant way.
  • To create a flexible environment where static code is separated from dynamic site-specific parameters, it’s a much better idea to define loops outside the static code, in variables.
  • When you define loops within a variable, all the normal rules for working with variables apply: The variables can be defined in the play header, using an include file, or as host/hostgroup variables.

Include the loop from a variable:

    ---
    - name: install and start services
      hosts: ansible1
      vars:
        services:
        - vsftpd
        - httpd
        - smb
      tasks:
      - name: install packages
        yum:
          name:
          - vsftpd
          - httpd
          - samba
          state: latest
      - name: start the services
        service:
          name: "{{ item }}"
          state: started
          enabled: yes
        loop: "{{ services }}"

Using Loops on Multivalued Variables

An item can be a simple list, but it can also be presented as a multivalued variable, as long as the multivalued variable is presented as a list.

Use variables that are imported from the file vars/users-list:

users:
  - username: linda
    homedir: /home/linda
    shell: /bin/bash
    groups: wheel
  - username: lisa
    homedir: /home/lisa
    shell: /bin/bash
    groups: users
  - username: anna
    homedir: /home/anna
    shell: /bin/bash
    groups: users

Use the list in a playbook:

    ---
    - name: create users using a loop from a list
      hosts: ansible1
      vars_files: vars/users-list
      tasks:
      - name: create users
        user:
          name: "{{ item.username }}"
          state: present
          groups: "{{ item.groups }}"
          shell: "{{ item.shell }}"
        loop: "{{ users }}"
  • Working with multivalued variables is possible, but the variables in that case must be presented as a list; using dictionaries is not supported.
  • The only way to loop over dictionaries is to use the dict2items filter.
  • Use of filters is not included in the RHCE topics and for that reason is not explained further here.
  • You can look up “Iterating over a dictionary” in the Ansible documentation for more information.

Understanding with_items

  • Since Ansible 2.5, using loop has been the command way to iterate over the values in a list.
  • In earlier versions of Ansible, the with_keyword statement was used instead.
  • In this approach, the keyword is replaced with the name of an Ansible look-up plug-in, but the rest of the syntax is very common.
  • Will be deprecated in a future version of Ansible.

With_keyword Options Overview with_items

  • Used like loop to iterate over values in a list with_file
  • Used to iterate over a list of filenames on the control node with_sequence
  • Used to generate a list of values based on a numeric sequence

Loop over a list using with_keyword:

    ---
    - name: install and start services
      hosts: ansible1
      vars:
        services:
        - vsftpd
        - httpd
        - smb
      tasks:
      - name: install packages
        yum:
          name:
          - vsftpd
          - httpd
          - samba
          state: latest
      - name: start the services
        service:
          name: "{{ item }}"
          state: started
          enabled: yes
        with_items: "{{ services }}"

Lab: Working with loop

1. Use your editor to define a variables file with the name vars/packages and the following contents:

packages:
- name: httpd
  state: absent
- name: vsftpd
  state: installed
- name: mysql-server
  state: latest

2. Use your editor to define a playbook with the name exercise71.yaml and create the play header:

- name: manage packages using a loop from a list
  hosts: ansible1
  vars_files: vars/packages
  tasks:

3. Continue the playbook by adding the yum task that will manage the packages, using the packages variable as defined in the vars/packages variable include file:

- name: manage packages using a loop from a list
  hosts: ansible1
  vars_files: vars/packages
  tasks:
  - name: install packages
    yum:
      name: "{{ item.name }}"
      state: "{{ item.state }}"
    loop: "{{ packages }}"

4. Run the playbook using ansible-playbook exercise71.yaml, and observe the results. In the results you should see which packages it is trying to manage and in which state it is trying to get the packages.

Using when to Run Tasks Conditionally

Using when to Run Tasks Conditionally

  • Use a when statement to run tasks conditionally.
  • you can test whether:
    • a variable has a specific value
    • whether a file exists
    • whether a minimal amount of memory is available
    • etc.

Working with when

Install the right software package for the Apache web server, based on the Linux distribution that was found in the Ansible facts. Notice that

  • when used in when statements, the variable that is evaluated is not placed between double curly braces.
    ---
    - name: conditional install
      hosts: all
      tasks:
      - name: install apache on Red Hat and family
        yum:
          name: httpd
          state: latest
        when: ansible_facts[’os_family’] == "RedHat"
      - name: install apache on Ubuntu and family
        apt:
          name: apache2
          state: latest
        when: ansible_facts[’os_family’] == "Debian"
  • not a part of any properties of the modules on which it is used

  • must be indented at the same level as the module itself.

  • For a string test, the string itself must be between double quotes.

  • Without the double quotes, it would be considered an integer test.

Using Conditional Test Statements

Common conditional tests that you can perform with the when statement:

Variable exists

  • variable is defined Variable does not exist

  • variable is not defined First variable is present in list mentioned as second

  • ansible_distribution in distributions Variable is true, 1 or yes

  • variable Variable is false, 0 or no

  • not variable Equal (string)

  • key == “value” Equal (numeric)

  • key == value Less than

  • key < value Less than or equal to

  • key <= value Greater than

  • key > value Greater than or equal to

  • key >= value Not equal to

  • key != value

  • Look for “Tests” in the Ansible documentation, and use the item that is found in Templating (Jinja2).

  • When referring to variables in when statements, you don’t have to use curly brackets because items in a when statement are considered to be variables by default.

  • So you can write when: text == “hello” instead of when: “{{ text }}” == “hello”.

There are roughly four types of when conditional tests: • Checks related to variable existence • Boolean checks • String comparisons • Integer comparisons

The first type of test checks whether a variable exists or is a part of another variable, such as a list.

Checks for the existence of a specific disk device, using variable is defined and variable is not defined. All failing tests result in the message “skipping.”

    ---
    - name: check for existence of devices
      hosts: all
      tasks:
      - name: check if /dev/sda exists
        debug:
          msg: a disk device /dev/sda exists
        when: ansible_facts[’devices’][’sda’] is defined
      - name: check if /dev/sdb exists
        debug:
          msg: a disk device /dev/sdb exists
        when: ansible_facts[’devices’][’sdb’] is defined
      - name: dummy test, intended to fail
        debug:
          msg: failing
        when: dummy is defined
      - name: check if /dev/sdc does not exist
        debug:
          msg: there is no /dev/sdc device
        when: ansible_facts[’devices’][’sdc’] is not defined

Lab: Check that finds whether the first variable value is present in the second variable’s list.

  • executes the debug task if the variable my_answer is in supported_packages.
  • vars_prompt is used. This stops the playbook, asks the user for input, and stores the input in a variable with the name my_answer.
    ---
    - name: test if variable is in another variables list
      hosts: all
      vars_prompt:
      - name: my_answer
        prompt: which package do you want to install
      vars:
        supported_packages:
        - httpd
        - nginx
      tasks:
      - name: something
        debug:
          msg: you are trying to install a supported package
        when: my_answer in supported_packages

Boolean check

  • Works on variables that have a Boolean value (not very common) T
  • Should not be defined with the check for existence.
  • Used to check whether a variable is defined.

string comparisons and integer comparisons

  • Ie: Check if more than 1 GB of disk space is available.
  • When doing checks on available disk space and available memory, carefully look at the expected value.
  • Memory is shown in megabytes, by default, whereas disk space is expressed in bytes.

Lab: integer check, install vsftpd if more than 50 MB of memory is available.

    ---
    - name: conditionals test
      hosts: all
      tasks:
      - name: install vsftpd if sufficient memory available
        package:
          name: vsftpd
          state: latest
        when: ansible_facts[’memory_mb’][’real’][’free’] > 50

Testing Multiple Conditions

  • when statements can also be used to evaluate multiple conditions.
  • To do so, you can group the conditions with parentheses and combine them with and and or keywords.
  • and runs if both conditionals are ture
  • or runs if one of the conditions are true

Lab: and is used and runs the task only if both conditions are true.

    ---
    - name: testing multiple conditions
      hosts: all
      tasks:
      - name: showing output
        debug:
          msg: using CentOS 8.1
        when: ansible_facts[’distribution_version’] == "8.1" and ansible_facts[’distribution’] == "CentOS"
  • You can make more complex statements by grouping conditions together in parentheses.
  • group the when statement starts with a > sign to wrap the statement over the next five lines for readability.

Lab: Combining complex statements

    ---
    - name: using multiple conditions
      hosts: all
      tasks:
      - package:
          name: httpd
          state: removed
        when: >
          ( ansible_facts[’distribution’] == "RedHat" and
            ansible_facts[’memfree_mb’] < 512 )
          or
          ( ansible_facts[’distribution’] == "CentOS" and
            ansible_facts[’memfree_mb’] < 256 )

Combining loop and when

Lab: Combining loop and when, Perform a kernel update only if /boot is on a dedicated mount point and at least 200 MBis available in the mount.

    ---
    - name: conditionals test
      hosts: all
      tasks:
      - name: update the kernel if sufficient space is available in /boot
        package:
          name: kernel
          state: latest
        loop: "{{ ansible_facts[’mounts’] }}"
        when: item.mount == "/boot" and item.size_available > 200000000

Combining loop and register

Lab: Combining register and loop

    ---
    - name: test register
      hosts: all
      tasks:
        - shell: cat /etc/passwd
          register: passwd_contents
        - debug:
            msg: passwd contains user lisa
          when: passwd_contents.stdout.find(’lisa’) != -1

passwd_contents.stdout.find,

  • passwd_contents.stdout does not contain any item with the name find.
  • Construction that is used here is variable.find, which enables a task to search a specific string in a variable. (thefind function in Python is used)
  • When the Python find function does not find a string, it returns a value of −1.
  • If the requested string is found, the find function returns an integer that returns the position where the string was found.
  • For instance, if the string lisa is found in /etc/passwd, it returns an unexpected value like 2604, which is the position in the file, expressed as a byte offset from the beginning, where the string is found for the first time.
  • Because of the behavior of the Python find function, variable.find needs not to be equal to −1 to have the task succeed. So don’t write passwd_contents.stdout.find(’lisa’) = 0 (because it is not a Boolean), but instead write passwd_contents.stdout.find(’lisa’) != -1.

Lab: Practice working with conditionals using register.

  • When using register, you might want to define a task that runs a command that will fail, just to capture the return code of that command, after which the playbook should continue. If that is the case, you must ensure that ignore_errors: yes is used in the task definition.

1. Use your editor to create a new file with the name exercise72.yaml. Start writing the play header as follows:

---
- name: restart sshd service if httpd is running
  hosts: ansible1
  tasks:

2. Add the first task, which checks whether the httpd service is running, using command output that will be registered. Notice the use of ignore_errors: yes. This line makes sure that if the service is not running, the play is still executed further.

---
- name: restart sshd service if httpd is running
  hosts: ansible1
  tasks:
  - name: get httpd service status
    command: systemctl is-active httpd
    ignore_errors: yes
    register: result

3. Add a debug task that shows the output of the command so that you can analyze what is currently in the registered variable:

---
- name: restart sshd service if httpd is running
  hosts: ansible1
  tasks:
  - name: get httpd service status
    command: systemctl is-active httpd
    ignore_errors: yes
    register: result
  - name: show result variable contents
    debug:
      msg: printing contents of the registered variable {{ result }}

4. Complete the playbook by including the service task, which is started only if the value stored in result.rc (which is the return code of the command that was registered) contains a 0. This is the case if the previous command executed successfully.

---
- name: restart sshd service if httpd is running
  hosts: ansible1
  tasks:
  - name: get httpd service status
    command: systemctl is-active httpd
    ignore_errors: yes
    register: result
  - name: show result variable contents
    debug:
      msg: printing contents of the registered variable {{ result }}
  - name: restart sshd service
    service:
      name: sshd
      state: restarted
    when: result.rc == 0

5. Use an ad hoc command to make sure the httpd service is installed: ansible ansible1 -m yum -a "name=httpd state=latest".

6. Use an ad hoc command to make sure the httpd service is stopped: ansible ansible1 -m service -a "name=httpd state=stopped".

7. Run the playbook using ansible-playbook exercise72.yaml and analyze the result. You should see that the playbook skips the service task.

8. Type ansible ansible1 -m service -a "name=httpd state=started" and run the playbook again, using ansible-playbook exercise72.yaml. Playbook execution at this point should be successful.