The handler is a built-in ansible feature that offers controlled task execution. The tasks defined under handlers are called handler tasks and they run only when notified to run.
Let’s say you installed nginix in one of the managed hosts and made some configuration changes. For the changes to be effective you have to restart the nginix service. You can create a handler task to restart the service. Here the handler task will only be triggered if there are any changes to the configuration.
Ansible Handlers Syntax Representation
The handler task is comprised of two directives.
- Notify – Think of this directive as a way to tell the handler to run the task. Notify directive will send a signal to the handler task.
- Handler – All the tasks are grouped under this directive.
Take a look at the below playbook snippet. It contains two tasks where the first task creates a file in the home directory and the second task under the handlers section removes the file.
In the first task notify directive is added. If you look at the name passed to the notify directive and handler task name both will be the same. Notify directive sends a signal using the task name. So keep the task name short and descriptive.
NOTE: Handler task will only run when the caller task returns “changed: True”. |
---
- name: Handler Test
hosts: localhost
gather_facts: false
vars:
USER: "{{ lookup('ansible.builtin.env', 'USER') }}"
PATH: "/home/{{ USER }}/samplefile.txt"
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: remove_file
handlers:
- name: remove_file
ansible.builtin.file:
path: "{{ PATH }}"
state: absent
When I run this playbook, the first task creates an empty file and set the task status as changed. The first task then sends a signal to run the handler task.
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]
RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]
Ansible Handlers Order Of Execution
Handler tasks will get executed only after all other regular tasks are completed. To demonstrate this I have added one more regular task which will change the file permission.
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: remove_file
- name: Changing file permission
ansible.builtin.file:
path: "{{ PATH }}"
mode: "0700"
handlers:
- name: remove_file
ansible.builtin.file:
path: "{{ PATH }}"
state: absent
If you see the below output
- The first task ran fine which creates an empty file.
- The second task ran after the first task instead of the handler task.
- The handler task ran as the final task.
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]
TASK [Changing file permission] ******************************************************************************************
changed: [localhost]
RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]
If you wish to run the handler task immediately after the parent task you can use the built-in meta module. In the following playbook snippet, the second task is using the meta module with “flush_handler”.
The third task will fail because the handler task will run and delete the file before the third task. I have added the ignore_errors directive to ignore the failure.
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: remove_file
- name: Flush handlers and run task
ansible.builtin.meta: flush_handlers
- name: Changing file permission
ansible.builtin.file:
path: "{{ PATH }}"
mode: "0700"
ignore_errors: True
handlers:
- name: remove_file
ansible.builtin.file:
path: "{{ PATH }}"
state: absent
In the below output you can see after the first task, the flush hander task is submitted followed by the handler task.
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]
TASK [Flush handlers and run task] ***************************************************************************************
RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]
TASK [Changing file permission] ******************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "file (/home/ansuser/samplefile.txt) is absent, cannot continue", "path": "/home/ansuser/samplefile.txt", "state": "absent"}
...ignoring
Ansible Handlers Calling Multiple Handler Tasks
Sometimes you may want to run multiple handler tasks after a regular task gets completed. This is possible by passing multiple handler task names to the notify directive.
In the following playbook snippet, a new handler task is added which will just print a message to stdout. if you look at the notify directive both the task names are passed in the python list format notation.
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: ["remove_file", "final_task"]
handlers:
- name: remove_file
ansible.builtin.file:
path: "{{ PATH }}"
state: absent
- name: final_task
ansible.builtin.debug:
msg: "This is the final task"
There are two supported formats to pass a list of task names to notify the directive. You can use yaml notation or python notation.
# yaml notation
notify:
- remove_file
- final_task
# python notation
notify: ["remove_file", "final_task"]
Ansible Handlers Duplicate Tasks
When you create two handler task with the same name, ansible will only consider the first interpreted task and executes it ignoring the subsequent duplicate task.
I am running the same playbook from the previous section but changed both the handler task name to “final_tasks”. In the below output, you can see only the file removal task ran but not the task that prints the message to stdout.
PLAY [Handler Test] ******************************************************************************************************
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]
RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]
NOTE: Always give descriptive and meaningful names to the tasks to eliminate duplication issue. |
Ansible Handlers – Handling Failures
If a task gets failed in a host, the handler task for that particular host will not run even though notify signal is already sent. There are a couple of options to handle failures.
1. Use the meta module to run the handler task after the parent task. We have already seen the meta module in the previous section.
2. Use “ignore_errors: yes” which will ignore the failure and run the handler task. To know more about the ignore_errors directive, look at the following article.
3. Set the property “force_handler: true” which will run the handler task irrespective of failure. I am using the same playbook but the second task is set to fail purposely.
---
- name: Handler Test
hosts: localhost
gather_facts: false
force_handlers: true
vars:
USER: "{{ lookup('ansible.builtin.env', 'USER')|default('nobody', True) }}"
PATH: "/home/{{ USER }}/samplefile.txt"
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: final_task
- name: Task to be failed
ansible.builtin.shell: /bin/false
ignore_errors: True
handlers:
- name: final_task
ansible.builtin.debug:
msg: "Running final task."
In the below output you can see the handler task ran fine even though the second task got failed.
PLAY [Handler Test] ******************************************************************************************************
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]
TASK [Task to be failed] *************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": "/bin/false", "delta": "0:00:00.003097", "end": "2023-02-06 18:54:12.060197", "msg": "non-zero return code", "rc": 1, "start": "2023-02-06 18:54:12.057100", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring
RUNNING HANDLER [final_task] *********************************************************************************************
ok: [localhost] => {
"msg": "Running final task."
}
You can also set “force_handler = true” in ansible.cfg file or pass –force-handlers flag when running the ansible-playbook command.
Run Group Of Handler Tasks Using Listen Directive
The notify directive accepts the task name as input. Alternatively, you can use the “listen” directive and pass the name to the notify directive. The main advantage of the listen directive is task grouping.
Take a look at the below handler task where the listen directive is set to “run all”. The same value is passed to notify directive which sends the signal to run both the handler tasks.
Instead of passing different task names to a single notify directive, listen directive groups multiple tasks, and only one value is passed to the notify directive.
tasks:
- name: Creating a empty file
ansible.builtin.file:
path: "{{ PATH }}"
state: touch
notify: "run all"
handlers:
- name: first handler task
ansible.builtin.debug:
msg: "processing some data from file"
listen: "run all"
- name: second handler task
ansible.builtin.file:
path: "{{ PATH }}"
state: absent
listen: "run all"
Wrap Up
In this article, we have seen what notify directive is and its usage. We have also seen how handlers behave in different cases. Finally, we have also seen how to group handler tasks using the listen directive.