How can I skip redundant Ansible handlers? - ansible

Imagine the following playbook, which manages a systemd service unit and a configuration file for a "thing" service:
---
- hosts: all
tasks:
- copy:
src: thing.service
dest: /etc/systemd/system/thing.service
notify: restart thing
- copy:
src: thing.conf
dest: /etc/thing.conf
notify: reload thing
handlers:
- name: restart thing
systemd:
name: thing
state: restarted
- name: reload thing
systemd:
name: thing
state: reloaded # Unnecessary if the restart handler has triggered.
If I modify the thing.service file AND the thing.conf file the handlers will trigger a restart AND a reload.
The reload is not necessary because the service will have been restarted.
Is there any way to inform Ansible of this so that it doesn't trigger the unnecessary reload after the restart?
I don't want to register variables and check those in the handlers with "when" clauses. I'm asking if this is something that Ansible accommodates in its playbook and task syntax.

Related

Ansible: Reload container if previous tasks resulted in changed==true?

How do I configure a docker_container task to restart a container if specific prior tasks resulted in changed==true?
Say I have three copy tasks(A,B,C) and then a docker_container task with state=started.
How do I have a docker_container task perform a restart if copy task A or copy task C resulted in changed==true?
If task A and task C both result in changed==false and the docker_container would otherwise not restart the container, then ansible should NOT restart the container.
You can simply use a handler for this.
---
- name: Testplaybook
hosts: ...
handlers:
- name: restart docker
...
tasks:
- name: Copy A
copy:
...
notify: restart docker
- name: Copy B
copy:
...
- name: Copy C
copy:
...
notify: restart docker
This will restart your docker container if copy A and/or copy C changes.
The handlers will be run at the end of the playbook (or when a task see below asks for it) and multiple notify events of the same category will only restart the container once.
If you want your restart right after the copy tasks you can add a task:
- name: Flush handlers
meta: flush_handlers
There's an easier way which doesn't require you to duplicate the container parameters in the handler - use the restart parameter:
- name: Create config file
ansible.builtin.template:
src: config.yml.j2
dest: /etc/config.yml
register: foobar_config
- community.general.docker_container:
name: foobar
image: "{{ foobar_image }}"
restart_policy: always
restart: "{{ foobar_config.changed | default(false) }}"
volumes:
- "/etc/config.yml:/etc/conf.yml"

Restart service when service file changes when using Ansible

I am creating a systemd service using template module
---
- name: Systemd service
template:
src: sonar.unit.j2
dest: /etc/systemd/system/sonarqube.service
when: "ansible_service_mgr == 'systemd'"
The contents of the sonarqube.service can change of course. On change I want to restart the service. How can I do this?
There are two solutions.
Register + When changed
You can register template module output (with its status change),
register: service_conf
and then use when clause.
when: service_conf.changed
For example:
---
- name: Systemd service
template:
src: sonar.unit.j2
dest: /etc/systemd/system/sonarqube.service
when: "ansible_service_mgr == 'systemd'"
register: service_conf
- name: restart service
service:
name: sonarqube
state: restarted
when: service_conf.changed
Handler + Notify
You define your restart service task as handler. And then in your template task you notify the handler.
tasks:
- name: Add Sonarqube to Systemd service
template:
src: sonar.unit.j2
dest: /etc/systemd/system/sonarqube.service
when: "ansible_service_mgr == 'systemd'"
notify: Restart Sonarqube
- …
handlers:
- name: Restart Sonarqube
service:
name: sonarqube
state: restarted
More info can be found in Ansible Doc.
Difference between those 2?
In the first case, the service will restart directly. In the case of the handler the restart will happen at the end of the play.
Another difference will be, if you have several tasks changes that need to restart of your service, you simply add the notify to all of them.
The handler will run if any of those task get a changed status. With the first solution, you will have to register several return. And it will generate a longer when clause_1 or clause_2 or …
The handler will run only once even if notified several times.
This calls for a handler
---
- name: Testplaybook
hosts: all
handlers:
- name: restart_service
service:
name: <servicename>
state: restarted
tasks:
- template:
src: ...
dest: ...
notify:
- restart_service
The handler will automatically get notified by the module when something changed. See the documentatation for further information on handlers.
Since you are using systemd, you will also need to execute daemon-reload because you updated the service file.
The task just templates the service file and notifies a handler:
- name: Systemd service
template:
src: sonar.unit.j2
dest: /etc/systemd/system/sonarqube.service
when: "ansible_service_mgr == 'systemd'"
notify: restart sonarqube systemd
Based on the presence of your specific when clause above, I'm assuming you might want to specify separate handlers in the case that systemd is not in use. The handler for the systemd case would look like the following:
- name: restart sonarqube systemd
systemd:
name: sonarqube
state: restarted
daemon_reload: yes

Cleaner way of choosing between start and restart

I have ansible configuration for deploying locally built daemons to a series of target machines, these daemons have associated systemd service files to control them.
What I want to happen is:
If daemon or unit file is changed then restart service
If daemon is unchanged then just start service (which may count as 'unchanged', because it's probably already running)
I'm doing this in a few places so I have a commonly repeating pattern that looks like:
- name: Populate the daemon
copy:
src: "local_build/mydaemon"
dest: "/usr/bin/mydaemon"
mode: 0775
register: daemon_bin
- name: Populate the service
template:
src: "Daemon.service"
dest: "/etc/systemd/system/mydaemon.service"
register: daemon_service
- name: Enable and restart
systemd:
state: restarted
daemon_reload: yes
enabled: yes
name: "mydaemon.service"
when: (daemon_bin.changed or daemon_service.changed)
- name: Enable and start
systemd:
state: started
enabled: yes
name: "mydaemon.service"
when: not (daemon_bin.changed or daemon_service.changed)
Is there a cleaner way to achieve this? It feels like it might be a common problem. Or is my approach somehow wrong?
Yes, you can use notify and handlers.

Run an Ansible handler only once for the entire playbook

I would like to run a handler only once in an entire playbook.
I attempted using an include statement in the following in the playbook file, but this resulted in the handler being run multiple times, once for each play:
- name: Configure common config
hosts: all
become: true
vars:
OE: "{{ ansible_hostname[5] }}"
roles:
- { role: common }
handlers:
- include: handlers/main.yml
- name: Configure metadata config
hosts: metadata
become: true
vars:
OE: "{{ ansible_hostname[5] }}"
roles:
- { role: metadata }
handlers:
- include: handlers/main.yml
Here is the content of handlers/main.yml:
- name: restart autofs
service:
name: autofs.service
state: restarted
Here is an example of one of the tasks that notifies the handler:
- name: Configure automount - /opt/local/xxx in /etc/auto.direct
lineinfile:
dest: /etc/auto.direct
regexp: "^/opt/local/xxx"
line: "/opt/local/xxx -acdirmin=0,acdirmax=0,rdirplus,rw,hard,intr,bg,retry=2 nfs_server:/vol/xxx"
notify: restart autofs
How can I get the playbook to only execute the handler once for the entire playbook?
The answer
The literal answer to the question in the title is: no.
Playbook is a list of plays. Playbook has no namespace, no variables, no state. All the configuration, logic, and tasks are defined in plays.
Handler is a task with a different calling schedule (not sequential, but conditional, once at the end of a play, or triggered by the meta: flush_handlers task).
A handler belongs to a play, not a playbook, and there is no way to trigger it outside of the play (i.e. at the end of the playbook).
Solution
The solution to the problem is possible without referring to handlers.
You can use group_by module to create an ad-hoc group based on the result of the tasks at the bottom of each play.
Then you can define a separate play at the end of the playbook restarting the service on targets belonging to the above ad-hoc group.
Refer to the below stub for the idea:
- hosts: all
roles:
# roles declaration
tasks:
- # an example task modifying Nginx configuration
register: nginx_configuration
# ... other tasks ...
- name: the last task in the play
group_by:
key: hosts_to_restart_{{ 'nginx' if nginx_configuration is changed else '' }}
# ... other plays ...
- hosts: hosts_to_restart_nginx
gather_facts: no
tasks:
- service:
name: nginx
state: restarted
Possible solution
Use handlers to add hosts to in-memory inventory. Then add play to run restart service only for these hosts.
See this example:
If task is changed, it notify mark to restart to set fact, that host needs service restart.
Second handler add host is quite special, because add_host task only run once for whole play even in handler, see also documentation. But if notified, it will run after marking is done implied from handlers order.
Handler loops over hosts on which tasks were run and check if host service needs restart, if yes, add to special hosts_to_restart group.
Because facts are persistent across plays, notify third handler clear mark for affected hosts.
A lot of lines you hide with moving handlers to separate file and include them.
inventory file
10.1.1.[1:10]
[primary]
10.1.1.1
10.1.1.5
test.yml
---
- hosts: all
gather_facts: no
tasks:
- name: Random change to notify trigger
debug: msg="test"
changed_when: "1|random == 1"
notify:
- mark to restart
- add host
- clear mark
handlers:
- name: mark to restart
set_fact: restart_service=true
- name: add host
add_host:
name: "{{item}}"
groups: "hosts_to_restart"
when: hostvars[item].restart_service is defined and hostvars[item].restart_service
with_items: "{{ansible_play_batch}}"
- name: clear mark
set_fact: restart_service=false
- hosts: primary
gather_facts: no
tasks:
- name: Change to notify trigger
debug: msg="test"
changed_when: true
notify:
- mark to restart
- add host
- clear mark
handlers:
- name: mark to restart
set_fact: restart_service=true
- name: add host
add_host:
name: "{{item}}"
groups: "hosts_to_restart"
when: hostvars[item].restart_service is defined and hostvars[item].restart_service
with_items: "{{ansible_play_batch}}"
- name: clear mark
set_fact: restart_service=false
- hosts: hosts_to_restart
gather_facts: no
tasks:
- name: Restart service
debug: msg="Service restarted"
changed_when: true
A handler triggered in post_tasks will run after everything else. And the handler can be set to run_once: true.
It's not clear to me what your handler should do. Anyway, as for official documentation, handlers
are triggered at the end of each block of tasks in a play,
and will only be triggered once even if notified by multiple different
tasks [...] As of Ansible 2.2, handlers can also “listen” to generic topics, and tasks can notify those topics as follows:
So handlers are notified / executed once for each block of tasks.
May be you get your goal just keeping handlers after "all" target hosts, but it doesn't seem a clean use of handlers.
.

How to write an Ansible role task that only runs when any of the previous other tasks in the task file have been changed?

I am working on a role where I want one task to be run at the end of the tasks file if and only if any of the previous tasks in that task file have changed.
For example, I have:
- name: install package
apt: name=mypackage state=latest
- name: modify a file
lineinfile: do stuff
- name: modify a second file
lineinfile: other stuff
- name: restart if anything changed
service: name=mypackage state=restarted
... and I want to only restart the service if an update has been installed or any of the config files have been changed.
How can I do this?
Best practice here is to use handlers.
In your role create a file handlers/main.yml with the content:
- name: restart mypackage
service: name=mypackage state=restarted
Then notify this handler from all tasks. The handler will be notified only if a task reports a changed state (=yellow output)
- name: install package
apt: name=mypackage state=latest
notify: restart mypackage
- name: modify a file
lineinfile: do stuff
notify: restart mypackage
- name: modify a second file
lineinfile: other stuff
notify: restart mypackage
Handlers will be executed at the very end of your play. If you have other roles involved which depend on the restarted mypackage service, you might want to flush all handlers at the end of the role:
- meta: flush_handlers
Additionally have a look at the force_handlers setting. In case an error happens in any other role processed after your mypackge role, the handler would not be triggered. Set force_handlers=True in your ansible.cfg to still force your handlers to be executed after errors. This is a very important topic since when you run your playbook the next time the files will not be changed and therefore the handler not get notified, hence your service never restarted.
You can also do this without handlers but this is very ugly. You need to register the output of every single task so you can later check the state in the condition applied to the restart task.
- name: install package
apt: name=mypackage state=latest
register: mypackage_1
- name: modify a file
lineinfile: do stuff
register: mypackage_2
- name: modify a second file
lineinfile: other stuff
register: mypackage_3
- name: restart if anything changed
service: name=mypackage state=restarted
when: mypackage_1 is changed or mypackage_2 is changed or mypackage_3 is changed
It was possible to use mypackage_1 | changed till ansible 2.9
See also the answer to Ansible Handler notify vs register.

Resources