Handlers are tasks that respond to a notification triggered by other tasks. Each handler has a globally-unique name, and is triggered at the end of a block of tasks in a playbook. If no task notifies the handler by name, it will not run. If one or more tasks notify the handler, it will run exactly 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 for any other task.
Normally, handlers are used to reboot hosts and restart services.
Handlers can be seen 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 task when a configuration file is updated and notifies it:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
tasks: - name: copy demo.example.conf configuration template (1) copy: src: /var/lib/templates/demo.example.conf.template dest: /etc/httpd/conf.d/demo.example.conf notify: (2) - restart_apache (3) handlers: (4) - name: restart_apache (5) service: (6) name: httpd state: restarted |
1. The task that notifies the handler.
2. The notify statement indicates the task needs to trigger a handler.
3. The name of the handler to run.
4. The statement starts the handlers section.
5. The name of the handler invoked by tasks.
6. The module to use for the handler.
In the previous example, the restart_apache handler will trigger when notified by the copy task that a change happened. A task may call more than one handler in their notify section. Ansible treats the notify statement as an array and iterates over the handler names:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
tasks: - name: copy demo.example.conf configuration template copy: 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 service: name: mariadb state: restarted - name: restart_apache service: name: httpd state: restarted |
Using handlers
As discussed in the Ansible documentation, there are some important things to remember about using handlers:
- Handlers are always run in the order in which the handlers section is written in the play, not in the order in which they are listed by the notify statement on a particular task.
- Handlers run after all other tasks in the play complete. A handler called by a task in the tasks: part of the playbook will not run until all of the tasks under tasks: have been processed.
- Handler names live in a global namespace. If two handlers are incorrectly given the same name, only one will run.
- Handlers defined inside an include can not be notified.
- Even if more than one task notifies a handler, the handler will only run once. If no tasks notify it, a handler will not run.
- If a task that includes a notify does not execute (for example, a package is already installed), the handler will not be notified. The handler will be skipped unless another task notifies it. Ansible notifies handlers only if the task acquires the CHANGED status.
Important
Handlers are meant to perform an action upon the execution of a task; they should not be used as a replacement for tasks.
Use Conditionals to Control Play Execution
The playbook below search for the regular expression DocumentRoot.*$
in the apache conf file /etc/httpd/conf/httpd.conf
. The old conf fiile is backuped. This will triger the apache restart, which handler "restart apache"
do.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--- - hosts: managedhost1 become: yes handlers: - name: restart apache service: name="httpd" state="restarted" listen: "restart web" tasks: - name: change config replace: path: /etc/httpd/conf/httpd.conf regexp: '^DocumentRoot.*$' replace: 'DocumentRoot "/opt/www"' backup: yes notify: "restart web" |
Running playbook2.yml:
1 2 3 4 5 6 7 8 9 10 11 12 |
[miro@controlnode playbooks]$ ansible-playbook playbook2.yml PLAY [managedhost1] *************************************************************************************** TASK [Gathering Facts] ************************************************************************************ ok: [managedhost1] TASK [change config] ************************************************************************************** ok: [managedhost1] PLAY RECAP ************************************************************************************************ managedhost1 : ok=2 changed=0 unreachable=0 failed=0 |
Let’s check on the managedhost1
if the playbook2.yml
works:
1 2 3 4 5 6 7 8 9 10 11 |
[root@managedhost1 /]# cd /etc/httpd/conf [root@managedhost1 conf]# ll razem 40 -rw-r--r--. 1 root root 11748 01-22 19:53 httpd.conf -rw-r--r--. 1 root root 11753 08-06 15:44 httpd.conf.4262.2020-01-22@19:53:38~ -rw-r--r--. 1 root root 13077 08-08 13:42 magic [root@managedhost1 conf]# diff httpd.conf httpd.conf.4262.2020-01-22@19:53:38~ 119c119 < DocumentRoot "/opt/www" --- > DocumentRoot "/var/www/html" |
httpd.conf.4262.2020-01-22@19:53:38~
is backup file of old httpd.conf
.
Example 1.
Create the configure_db.yml
playbook file. This file will install a database server and create some users. When the database server is installed, the playbook restarts the service. Start the playbook with the initialization of some variables: db_packages
, which defines the name of the packages to install for the database service; db_service
, which defines the name of the database service; src_file
for the URL of the configuration file to install; and dst_file
for the location of the installed configuration file on the managed hosts. Define a task that uses the yum module to install the required database packages as defined by the db_packages
variable. Notify the handler start_service
in order to start the service. Add a task to download my.cnf.template
to /etc/my.cnf
on the managed host, using the get_url
module. Add a condition that notifies the handler restart_service
as well as set_password
, to restart the database service and set the administrative password.
Define the three handlers the tasks needs. The start_service
handle will start the mariadb service; the restart_service
handler will restart the mariadb service; and the set_password
handler will set the administrative password for the database service. Define the start_service
handler. Define the second handler, restart_service
.
Finally, define the handler that sets the administrative password. The handler will use the mysql_user
module to perform the command.
When completed, the playbook should look like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
--- - hosts: databases vars: db_packages: - mariadb-server - MySQL-python db_service: mariadb src_file: "http://materials.example.com/task_control/my.cnf.template" dst_file: /etc/my.cnf tasks: - name: Install {{ db_packages }} package yum: name: "{{ item }}" state: latest with_items: "{{ db_packages }}" notify: - start_service - name: Download and install {{ dst_file }} get_url: url: "{{ src_file }}" dest: "{{ dst_file }}" owner: mysql group: mysql force: yes notify: - restart_service - set_password handlers: - name: start_service service: name: "{{ db_service }}" state: started - name: restart_service service: name: "{{ db_service }}" state: restarted - name: set_password mysql_user: name: root password: redhat |
Run the configure_db.yml
playbook. Notice the output shows the handlers are being executed.
1 2 3 4 5 6 7 8 9 |
[student@workstation dev-handlers]# ansible-playbook configure_db.yml PLAY ************************************************************************ ... Output omitted ... RUNNING HANDLER [start_service] ************************************************ changed: [servera.lab.example.com] RUNNING HANDLER [restart_service] ********************************************** changed: [servera.lab.example.com] RUNNING HANDLER [set_password] ************************************************* changed: [servera.lab.example.com] |
Run the playbook again. This time the handlers are skipped.
1 2 3 4 5 |
[student@workstation dev-handlers]# ansible-playbook configure_db.yml PLAY *********************************************************************** ... Output omitted ... PLAY RECAP ***************************************************************** servera.lab.example.com : ok=3 changed=0 unreachable=0 failed=0 |
Because handlers are executed once, they can help prevent failures. Update the playbook to add a task after installing /etc/my.cnf
that sets the MySQL admin password like the set_password handler. This will show you why using a handler in this situation is better than a simple task. The task should read as follows:
1 2 3 4 |
- name: Set the MySQL password mysql_user: name: root password: redhat |
Run the playbook again. The task should fail since the MySQL password has already been set.
1 2 3 4 5 6 7 8 9 10 |
[student@workstation dev-handlers]# ansible-playbook configure_db.yml TASK [Set the MySQL password] ************************************************** fatal: [servera.lab.example.com]: FAILED! => {"changed": false, "failed": true, "msg": "unable to connect to database, check login_user and login_password are correct or /root/.my.cnf has the credentials. Exception message: (1045, \"Access denied for user 'root'@'localhost' (using password: NO)\")"} NO MORE HOSTS LEFT ************************************************************* to retry, use: --limit @configure_db.retry PLAY RECAP ********************************************************************* servera.lab.example.com : ok=3 changed=0 unreachable=0 failed=1 |