How to run an ansible task only once regardless of how many targets there are - ansible

Consider the following Ansible task:
- name: stop tomcat
gather_facts: false
hosts: pod1
pre_tasks:
- include_vars:
dir: "vars/{{ environment }}"
vars:
hipchat_message: "stop tomcat pod1 done."
hipchat_notify: "yes"
tasks:
- include: tasks/stopTomcat8AndClearCache.yml
- include: tasks/stopHttpd.yml
- include: tasks/hipchatNotification.yml
This stops tomcat on n number of servers. What I want it to do is send a hipchat notification when it's done doing this. However, this code sends a separate hipchat message for each server the task happens on. This floods the hipchat window with redundant messages. Is there a way to make the hipchat task happen once after the stop tomcat/stop httpd tasks have been done on all the targets? I want the task to shut down tomcat on all the servers, then send one hip chat message saying "tomcat stopped on pod 1".

You can conditionally run the hipchat notification task on only one of the pod1 hosts.
- include: tasks/hipChatNotification.yml
when: inventory_hostname == groups.pod1[0]
Alternately you could only run it on localhost if you don't need any of the variables from the previous play.
- name: Run notification
gather_facts: false
hosts: localhost
tasks:
- include: tasks/hipchatNotification.yml
You also could use the run_once flag on the task itself.
- name: Do a thing on the first host in a group.
debug:
msg: "Yay only prints once"
run_once: true
- name: Run this block only once per host group
block:
- name: Do a thing on the first host in a group.
debug:
msg: "Yay only prints once"
run_once: true

Ansible handlers are made for this type of problem where you want to run a task once at the end of an operation even though it may have been triggered multiple times in the play.
You can define a handler section in your playbook and notify it in the tasks, the handlers will not run unless notified by a task, and will only run once regardless of how many times they are notified.
handlers:
- name: hipchat notify
hipchat:
room: someroom
msg: tomcat stopped on pod 1
In your play tasks just include a "notify" on the tasks that should trigger the handler and if they change it will run the handler after all tasks have executed.
- name: Stop service httpd, if started
service:
name: httpd
state: stopped
notify:
- hipchat notify

Related

Error handling with "failed_when" doesn't work

I wish to create an ansible playbook to run on multiple servers in order to check for conformity to the compliance rules. Therefore the playbook is supposed to check whether several services are disabled and whether they are stopped.
I do know that not all the services will be running or enabled on all machines. Therefore I wish to handle certain return codes within the playbook.
I tried the failed_when Statement for this Task. It seems the way to go as it allows to set which RCs to handle.
- hosts: server_group1
remote_user: ansible
tasks:
- name: Stop services
command: /bin/systemctl stop "{{ item }}"
with_items:
- cups
- avahi
- slapd
- isc-dhcp-server
- isc-dhcp-server6
- nfs-server
- rpcbind
- bind9
- vsftpd
- dovecot
- smbd
- snmpd
- squid
register: output
failed_when: "output.rc != 0 and output.rc != 1"
However the loop only works if I use ignore_errors: True which is not shown in the example code.
I do know that I wish to catch RCs of 0 and 1 from the command executed by the playbook. But no matter what failed_when always generates a fatal error and my playbook fails.
I also tried the failed_when line without the "" but that doesn't change a thing.
Something is missing, but I don't see what.
Any advice?
Therefore the playbook is supposed to check whether several services are disabled and whether they are stopped.
Your playbook isn't doing either of these things. If a service exists, whether or not it's running, then systemctl stop <service> will return successfully in most cases. The only time you'll get a non-zero exit code is if either (a) the service does not exist or (b) systemd is unable to stop the service for some reason.
Note that calling systemctl stop has no effect on whether or not a service is disabled; with your current playbook, all those services would start back up the next time the host boots.
If your goal is not simply to check that services are stopped and disabled, but rather to ensure that services are stopped and disabled, you could do something like this:
- name: "Stop services"
service:
name: "{{ item }}"
enabled: false
state: stopped
ignore_errors: true
register: results
loop:
- cups
- avahi
- slapd
- isc-dhcp-server
- isc-dhcp-server6
- nfs-server
- rpcbind
- bind9
- vsftpd
- dovecot
- smbd
- snmpd
- squid
You probably want ignore_errors: true here, because you want run the task for every item in your list.
A different way of handling errors might be with something like:
failed_when: >-
results is failed and
"Could not find the requested service" not in results.msg|default('')
This would only fail the task if stopping the service failed for a reason other than the the fact that there was not matching service on the target host.

validating log file before continuing the playbook execution

I want to look for particular sentence '*****--Finished Initialization**' in log before starting the application on next host. The tail command will never stop printing the data since the application will be used by some process immediately after application start and data will be logged.
how can I validate that before restarting the application on the second host?
As of now, I have skipped the tail command and given a timeout of 3 minutes.
- name: playbook to restart the application on hosts
hosts: host1, host2
tags: ddapp
connection: ssh
gather_facts: no
tasks:
- name: start app and validate before proceeding
shell: |
sudo systemctl start tomcat#application
#tail –f application_log.txt
wait_for: timeout=180
#other shell commands
args:
chdir: /path/to/files/directory
Use wait_for module:
- name: playbook to restart the application on hosts
hosts: host1, host2
tags: ddapp
connection: ssh
gather_facts: no
tasks:
- name: start app
become: yes
service:
name: tomcat#application
state: started
- name: validate before proceeding
wait_for:
path: /path/to/files/directory/application_log.txt
search_regex: Finished Initialization
Notice if the log is not cleared between app restarts and contains multiple Finished Initialization strings inside, refer to this question.
You have to use the wait_for module to look for a particular string with regex
- name: start app
service:
state: started
name: tomcat#application
become: true
- name: Wait for application to be ready
wait_for:
search_regex: '\*\*\*\*\*--Finished Initialization\*\*'
path: /you/path/to/application_log.txt
wait_for can also be used to detect for file apparition (like pid) or network port being opened (or not).
also, always prefer using native module instead of using a shell script to handle deployment or action. That why I replace the shell with service module.

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.
.

Ansible handlers and shell module

I have a playbook with the following task and handler section (just a snippet):
tasks:
- name: 'Run legacy script and power off'
debug: msg="Preparing for reboot"
notify: Legacy sysprep
handlers:
- name: Enable Service1
service: name=service1 enabled=yes state=restarted
- name: Legacy sysprep
shell: /var/scripts/prep-reboot.sh
When I run the playbook, I see the debug message for the task that calls the Legacy sysprep handler, and I see the Enable Service1 handler executed, but the Legacy sysprep handler isn't called (it doesn't show up in the playbook output, nor does it run on the system) and the servers are not rebooted (part of the script).
Yes, I plan to migrate the prep-reboot.sh script to an Ansible playbook, but I was surprised that apparently the shell module doesn't seem to work? Or is there an error I've overlooked? Running with -vvv doesn't report anything unexpected.
Ansible and Ansible-playbook version 2.1.1.0 running on RHEL 6.8.
Thank you, #techraf you got me pointed in the right direction. I ended up adding the changed_when: true to the debug line and that forces that play to register a change which then triggers the appropriate handler.
Here is my actual test playbook for reference:
---
- name: Testing forced handler
hosts: testsys_only
gather_facts: True
tasks:
- name: 'Run legacy script and power off'
debug: msg="Preparing for reboot"
changed_when: true
notify: Legacy sysprep
handlers:
- name: Enable Service1
service: name=service1 enabled=yes state=restarted
- name: Legacy sysprep
shell: /var/scripts/prep-reboot.sh

Why are my Ansible handlers not firing?

I have a playbook that installs tomcat and then deploys some web applications. The web application deploy task(s) notifies a handler to restart tomcat. But the handler never fires. I am using a handler to manage the tomcat service because I understand from the docs that handlers should only fire once even if called multiple times. Am I missing something obvious?
This is the playbook:
---
- hosts: all
become: true
become_user: root
roles:
- role: common
- role: nginx
- role: tomcat
- role: launchpad
- role: manager
- role: reporting
handlers:
- include: /tomcat/handlers/etitomcat_service_ctrl.yml
This is one of the roles that deploys the web app:
---
- name: Remove the current installation of LaunchPad
file: path={{etitomcat_path}}/webapps/{{launchpad_module}} state=absent
- name: Remove the current war file for {{launchpad_module}}
file: path={{etitomcat_path}}/webapps/{{launchpad_module}}.war state=absent
- name: Download the latest snapshot of LaunchPad and deploy it to {{etitomcat_path}}
get_url: url={{launchpad_source_url}} dest={{etitomcat_path}}/webapps/{{launchpad_module}}.war mode=0744 owner={{etitomcat_user}} group={{etitomcat_group}} force=yes
notify: "restart_eti_tomcat"
This is the handler:
- name: "Restart ETI Tomcat"
service: name=etitomcat state=restarted
become: true
become_user: root
listen: "restart_eti_tomcat"
- name: "Start ETI Tomcat"
service: name=etitomcat state=started
become: true
become_user: root
listen: "start_eti_tomcat"
- name: "Stop ETI Tomcat"
service: name=etitomcat state=stopped
become: true
become_user: root
listen: "stop_eti_tomcat"
Adding static: yes should resolve this issue when using Ansible >= 2.1.
handlers:
- include: /tomcat/handlers/etitomcat_service_ctrl.yml
static: yes
Take a look at this Github issue, the linked google groups thread might contain valuable information as well.
edit
As pointed out by #rhythmicdevil the documentation notes:
You cannot notify a handler that is defined inside of an include. As of Ansible 2.1, this does work, however the include must be static.
This may be beside the point but I'll add this regardless as the question headline is rather wide and this is the question I found when googling and below is the solution for the particular issue I had.
Do take into consideration that the handlers only triggers when there is a change registered in the corresponding task. Even if you run the play with the highest verbosity level there will be NO entry like this which spells this out.
RUNNING HANDLER [base : somehandler ] ***********************
Unchanged: Skipping
And when they are triggered after a change it will be after all the tasks has been performed.
This really did my head in since the tasks notified you regardless of if they really did something or not whereas handlers stays quiet.
For example, if you have a task A that you have been running a few times until it works like you expect.
Then after that connect a handler B to restart the service, nothing will happen unless you wipe the operation the task A are doing or change it.
As long as task A registers no change it will not trigger handler B.
This is the behavior for ansible 2.2.1 anyways.
You can add the tasks and post_tasks section instead of handlers, hope it will work for you:
---
- hosts: all
become: true
become_user: root
tasks:
- role: common
- role: nginx
- role: tomcat
- role: launchpad
- role: manager
- role: reporting
post_tasks:
- include: /tomcat/handlers/etitomcat_service_ctrl.yml

Resources