Using block for handlers in Ansible

George Shuklin
OpsOps
Published in
3 min readMay 16, 2018

Sometimes you want to run few operations for your handler instead of just one. In my case, a handler was designed to restart a main service, but with few conditions:

  • This service may create a ‘lock file’ which says ‘do not restart me now’. It was designed to protect that service during execution of a very important task. Ansible should try again and again until the lock is released before restarting the service.
  • After restart of the service there is a delay before the service become operational. Ansible shouldn’t try to change anything until the service is become operational. The service has a special URL to check if it done initialization or not. A playbook should try this URL few times until it succeeded before continue to the next task.

And all that is a single ‘restart foo’ handler. It was clearly a candidate for a ‘block’ in a handler. Unfortunately, at this moment (2.5.3), Ansible have no support for block in the handler section.

Fortunately, it supports import_tasks as a valid handler operation.

Proof of concept

play.yaml:

- hosts: all
gather_facts: no
tasks:
- command: id
notify: restart test
- command: id
notify: restart test
- command: id
notify: restart test
- command: id
notify: restart test
handlers:
- name: restart test
import_tasks: tasks/restart_test.yaml

tasks/restart_test.yaml:

---
- block:
- name: message 1
debug: msg=operation1
- name: message 2
debug: msg=operation2
- name: message 3
debug: msg=operation3

A command:

ansible-playbook -i localhost, play.yaml  -c local

A result:

PLAY [all] *********************************************************************TASK [command] *****************************************************************
changed: [localhost]
TASK [command] *****************************************************************
changed: [localhost]
TASK [command] *****************************************************************
changed: [localhost]
TASK [command] *****************************************************************
changed: [localhost]
RUNNING HANDLER [message 1] ****************************************************
ok: [localhost] => {
"msg": "operation1"
}
RUNNING HANDLER [message 2] ****************************************************
ok: [localhost] => {
"msg": "operation2"
}
RUNNING HANDLER [message 3] ****************************************************
ok: [localhost] => {
"msg": "operation3"
}
PLAY RECAP *********************************************************************
localhost : ok=7 changed=4 unreachable=0 failed=0

Cool! We have few tasks calling a handler. The handler is been executed only once, and it consists of few tasks both in a block and outside of the block statement. It’s exactly what we need.

Real example

My real goal is more complicated. Nevertheless, the example from above is a good starting point. I add some more complicated tricks here, see explanation below.

roles/foo/tasks/main.yaml:

---
# ... install, etc..
- name: Configure foo
template: src=foo.conf.j2 dest=/etc/foo.conf
notify: restart foo
- name: Check if need to restart
stat: path=/var/run/roo-restart.pending
register: restart_pending
changed_when: restart_pending.stat.exists
notify: restart foo

roles/foo/handlers/main.yaml:

---
- name: restart foo
include_tasks: tasks/restart.yaml

roles/foo/tasks/restart.yaml:

---
- name: Create restart flag
become: yes
file: path=/var/run/foo-restart.pending state=touch
- name: Wait for lock release
wait_for:
path: '{{foo_lock_file}}'
state: absent
timeout: '{{foo_lock_max_timeout}}'
- name: Restart foo
become: yes
service: name=foo state=restarted
- name: Remove restart flag
become: yes
file: path=/var/run/foo-restart.pending state=absent
- name: Waiting for foo to come online
uri:
method: GET
url: http://{{api_external_ip}}/api/heartbeat
force_basic_auth: yes
user: "{{api_user}}"
password: "{{api_password}}"
status_code: [200, 500]
register: status
until: status.status == 200
retries: 31
delay: 2

Comments:

  1. My first example had worked with import. A real-life case in the role had required me to use include. It’s yet another import VS include mystery. I suspect it somehow related to the ‘name’ for the imported task. (I think imports have no name to use).
  2. There are few tasks related to /var/run/roo-restart.pending. We need to restart the service after changing the configuration even if previous attempt to restart service had failed (because of timeout, for example). To make handler ‘persistent’ we create a special flag, and then check if that flag is ‘on’. If it exists, we knew we need to restart. And we cleanup flag only if restart was successful. (Moreover, flag is placed in /var/run, that means it is cleaned on a server restart).
  3. The handler creates ‘restart flag’, then check/wait for the lock, then restart the service, then cleanup ‘restart flag’, then wait for the service to come online. All in one handler.

Exactly what we wants!

--

--

George Shuklin
OpsOps

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.