How to alert via email in Ansible - automation

I have setup a mail task in ansible to send emails if yum update is marked as 'changed'.
Here is my current working code:
- name: Send mail alert if updated
community.general.mail:
to:
- 'recipient1'
cc:
- 'recipient2'
subject: Update Alert
body: 'Ansible Tower Updates have been applied on the following system: {{ ansible_hostname }}'
sender: "ansible.updates#domain.com"
delegate_to: localhost
when: yum_update.changed
This works great, however, every system that gets updated per host group sends a separate email. Last night for instance I had a group of 20 servers update and received 20 separate emails. I'm aware of why this happens, but my question is how would I script this to add all the systems to one email? Is that even possible or should I just alert that the group was updated and inform teams of what servers are in each group? (I'd prefer not to take the second option)
Edit 1:
I have added the code suggested and am now unable to receive any emails. Here's the error message:
"msg": "The conditional check '_changed|length > 0' failed. The error was: error while evaluating conditional (_changed|length > 0): {{ hostvars|dict2items| selectattr('value.yum_update.changed')| map(attribute='key')|list }}: 'ansible.vars.hostvars.HostVarsVars object' has no attribute 'yum_update'\n\nThe error appears to be in '/tmp/bwrap_1073_o8ibkgrl/awx_1073_0eojw5px/project/yum-update-ent_template_servers.yml': line 22, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - name: Send mail alert if updated\n ^ here\n",
I am also attaching my entire playbook for reference:
---
- name: Update enterprise template servers
hosts: ent_template_servers
tasks:
- name: Update all packages
yum:
name: '*'
state: latest
register: yum_update
- name: Reboot if needed
import_tasks: /usr/share/ansible/tasks/reboot-if-needed-centos.yml
- name: Kernel Cleanup
import_tasks: /usr/share/ansible/tasks/kernel-cleanup.yml
- debug:
var: yum_update.changed
- name: Send mail alert if updated
community.general.mail:
to:
- 'email#domain.com'
subject: Update Alert
body: |-
Updates have been applied on the following system(s):
{{ _changed }}
sender: "ansible.updates#domain.com"
delegate_to: localhost
run_once: true
when: _changed|length > 0
vars:
_changed: "{{ hostvars|dict2items|
selectattr('yum_update.changed')|
map(attribute='key')|list }}"
...
Ansible version is: 2.9.27
Ansible Tower version is: 3.8.3
Thanks in advance!

For example, the mail task below
- debug:
var: yum_update.changed
- community.general.mail:
sender: ansible
to: root
subject: Update Alert
body: |-
Updates have been applied to the following system:
{{ _changed }}
delegate_to: localhost
run_once: true
when: _changed|length > 0
vars:
_changed: "{{ hostvars|dict2items|
selectattr('value.yum_update.changed')|
map(attribute='key')|list }}"
TASK [debug] ***************************************************************
ok: [host01] =>
yum_update.changed: true
ok: [host02] =>
yum_update.changed: false
ok: [host03] =>
yum_update.changed: true
TASK [community.general.mail] **********************************************
ok: [host01 -> localhost]
will send
From: ansible#domain.com
To: root#domain.com
Cc:
Subject: Update Alert
Date: Wed, 09 Feb 2022 16:55:47 +0100
X-Mailer: Ansible mail module
Updates have been applied to the following system:
['host01', 'host03']
Remove the condition below if you want to receive also empty lists
when: _changed|length > 0
Debug
'ansible.vars.hostvars.HostVarsVars object' has no attribute 'yum_update'
Q: "What I could try?"
A: Some of the hosts are missing the variables yum_update. You can test it
- debug:
msg: "{{ hostvars|dict2items|
selectattr('value.yum_update.changed')|
map(attribute='key')|list }}"
run_once: true
Either make sure that the variable is defined on all hosts or use json_query. This filter tolerates missing attributes, e.g.
- debug:
msg: "{{ hostvars|dict2items|
json_query('[?value.yum_update.changed].key') }}"
run_once: true
Q: "The 'debug' task prior to the 'mail' task gives me the same output. But it fails when the 'mail' task is executed."
A: Minimize the code and isolate the problem. For example, in the code below you can see
Variable yum_update.changed is missing on host03
The filter json_query ignores this
The filter selectattr fails
- debug:
var: yum_update.changed
- debug:
msg: "{{ hostvars|dict2items|
json_query('[?value.yum_update.changed].key') }}"
run_once: true
- debug:
msg: "{{ hostvars|dict2items|
selectattr('value.yum_update.changed')|
map(attribute='key')|list }}"
run_once: true
gives
TASK [debug] **************************************************
ok: [host01] =>
yum_update.changed: true
ok: [host02] =>
yum_update.changed: false
ok: [host03] =>
yum_update.changed: VARIABLE IS NOT DEFINED!
TASK [debug] **************************************************
ok: [host01] =>
msg:
- host01
TASK [debug] **************************************************
fatal: [host01]: FAILED! =>
msg: |-
The task includes an option with an undefined variable.
The error was: 'ansible.vars.hostvars.HostVarsVars object'
has no attribute 'yum_update'
Both filters give the same results if all variables are present
TASK [debug] **************************************************
ok: [host01] =>
yum_update.changed: true
ok: [host02] =>
yum_update.changed: false
ok: [host03] =>
yum_update.changed: true
TASK [debug] **************************************************
ok: [host01] =>
msg:
- host01
- host03
TASK [debug] **************************************************
ok: [host01] =>
msg:
- host01
- host03

Related

Ansible "stat" module doesn't work with the when clause

I have a playbooks that checks to see if the endpoint is registered to Spacewalk using the stat module
- name: "Check spacewalk registraton"
stat:
path: /usr/sbin/rhn_check
register: sw_registered
- debug:
msg: "{{ sw_registered }}"
Output is:
TASK [Check spacewalk registraton] *********************************************
ok: [hostname]
TASK [debug] *******************************************************************
ok: [hostname] =>
msg:
changed: false
failed: false
stat:
atime: 1670244246.6493175
attr_flags: e
attributes:
- extents
block_size: 4096
blocks: 32
charset: us-ascii
checksum: 7b22e2e756706ef1b81e50cda7c41005e15441d7
ctime: 1623819058.4283004
dev: 64768
device_type: 0
executable: true
**exists: true**
gid: 0
gr_name: root
inode: 143991
isblk: false
ischr: false
isdir: false
isfifo: false
isgid: false
islnk: false
isreg: true
issock: false
isuid: false
mimetype: text/x-python
mode: '0755'
mtime: 1536233638.0
nlink: 1
path: /usr/sbin/rhn_check
pw_name: root
readable: true
rgrp: true
roth: true
rusr: true
size: 15291
uid: 0
version: '290956743'
wgrp: false
woth: false
writeable: true
wusr: true
xgrp: true
xoth: true
xusr: true
So the sw_registered.stat.exists is a value of true
Further in my role are tasks based on this variable
- name: "Yum update for RHEL6 and above using RedHat Satellite"
yum:
name: '*'
state: latest
exclude: rhn-client-tools
when: (ansible_distribution_major_version >= "6") and (sw_registered.stat.exists is not defined and sw_registered.stat.exists is false)
Output from that task is
TASK [QL-patching : Yum update for RHEL6 and above using RedHat Satellite] *****
skipping: [hostname]
I would expect that task to be skipped but the next task is:
- name: "Yum update for RHEL6 and above using spacewalk"
yum:
name: '*'
state: latest
disable_gpg_check: yes
when: (ansible_distribution_major_version >= "6") and (sw_registered.stat.exists is defined and sw_registered.stat.exists is true )
Output from that task is:
TASK [QL-patching : Yum update for RHEL6 and above using spacewalk] ************
skipping: [hostname]
I expect this task be executed and not skipped. What am I missing here?
Based on the comment of Zeitounator you may have a look into the following minimal example
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Test file
stat:
path: "/home/{{ ansible_user }}/test.file"
register: result
- name: Show result
debug:
msg: "{{ result.stat.exists }}"
resulting into an output of
TASK [Show result] ******
ok: [localhost] =>
msg: false
TASK [Show result] ******
ok: [localhost] =>
msg: true
and depending on if the file to test exists or not.
The key result.stat.exists will be defined in both cases if the stat test task was executed successful. This is because of Return Values of the stat module. Therefore the Conditional task based on registered variables could be simplified to something like
- name: Show result
debug:
msg: "File exists."
when: result.stat.exists
resulting into an output of
TASK [Show result] ******
ok: [localhost] =>
msg: File exists.
if the file is available or skipped if not.
You may also consinder to Provide default values as also mentioned and to catch corner cases like a former failing task because of insufficient access rights or if the task wasn't running because of Check mode. In such cases the result set could look like
TASK [Test file] ***************************
fatal: [localhost]: FAILED! => changed=false
msg: Permission denied
...ignoring
TASK [Show result] *************************
ok: [localhost] =>
msg:
changed: false
failed: true
msg: Permission denied

How to deal with multiple when condition for registered variable in ansible

I have a playbook 3 raw task (or more) with sample commands like below:
Playbook mytest.yml
- hosts: remotehost
gather_facts: no
tasks:
- name: Execute command1
raw: "ls -ltr"
register: cmdoutput
when: remcmd == "list"
- name: Execute command2
raw: "hostname"
register: cmdoutput
when: remcmd == "host"
- name: Execute command3
raw: "uptime"
register: cmdoutput
when: remcmd == "up"
- hosts: localhost
gather_facts: no
tasks:
- debug:
msg: "Printing {{ hostvars['remotehost']['cmdoutput'] }}"
This is my nventory myhost.yml
[remotehost]
myserver1
Here is how I run the playbook:
ansible-playbook -i myhost.yml mytest.yml -e remcmd="host"
PLAY [remotehost] ***************************************************************************************************************
TASK [Execute command1] *********************************************************************************************************
Thursday 06 October 2022 07:06:06 -0500 (0:00:00.013) 0:00:00.013 ******
skipping: [myserver1]
TASK [Execute command2] *********************************************************************************************************
Thursday 06 October 2022 07:06:06 -0500 (0:00:00.023) 0:00:00.036 ******
changed: [myserver1]
TASK [Execute command3] *********************************************************************************************************
Thursday 06 October 2022 07:06:06 -0500 (0:00:00.521) 0:00:00.557 ******
skipping: [myserver1]
PLAY [localhost] ****************************************************************************************************************
TASK [debug] ********************************************************************************************************************
Thursday 06 October 2022 07:06:06 -0500 (0:00:00.032) 0:00:00.590 ******
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: \"hostvars['remotehost']\" is undefined\n\nThe error appears to be in '/home/wladmin/mytest.yml': line 22, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n tasks:\n - debug:\n ^ here\n"}
PLAY RECAP **********************************************************************************************************************
localhost : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
myserver1 : ok=1 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
My requirement is no matter what value is passed for remcmd my localhost play should print stdoutlines of cmdoutput
Preliminary notes:
Using raw is evil.
Don't use raw unless to install prereqs (i.e. python) on the target host. Then switch to modules or at the very least command/shell
If you still intend to use raw, go back to point 1 above
In case your forgot to go back to point 1: using raw is evil
Don't register several tasks with the same var name (the last one always win, even if skipped). Don't create tasks you can avoid up-start.
As an illustration of the above principles
- hosts: remotehost
gather_facts: no
vars:
cmd_map:
list: ls -ltr
host: hostname
up: uptime
tasks:
- name: Make sure remcmd is known
assert:
that: remcmd in cmp_map.keys()
fail_msg: "remcmd must be one of: {{ cmd_map.keys() | join(', ') }}"
- name: Execute command
command: "{{ cmd_map[remcmd] }}"
register: cmdoutput
- name: Show entire result from above task
debug:
var: cmdoutput
my localhost play should print stdout_lines of cmdoutput
As far as I understand "How the debug module works", it can only print on the Control Node.
Therefore you could just remove three (3) lines in your example
- hosts: localhost
gather_facts: no
tasks:
and give it a try with
- hosts: remotehost
gather_facts: no
tasks:
- name: Execute command1
raw: "ls -ltr"
register: cmdoutput
when: remcmd == "list"
- name: Execute command2
raw: "hostname"
register: cmdoutput
when: remcmd == "host"
- name: Execute command3
raw: "uptime"
register: cmdoutput
when: remcmd == "up"
- debug:
msg: "Printing {{ cmdoutput }}"
and independently of which task became executed the result would be provided.
Apart from the answer about "How the debug module works" here, I like to recommended to proceed further with the answer of Zeitounator, since it will address your possible use case more complete.

Is there a way in Ansible to retrieve a variable located on a different host, to get it on the localhost

I have the following playbook:
- name: Some action on the workers
hosts: workers
gather_facts: false
run_once: true
tasks:
- name: Set var for server information
set_fact:
server_info:
"name": "winserver"
"os": "Microsoft Windows Server 2019 Datacenter"
- name: Some action on the localhost
hosts: localhost
gather_facts: false
run_once: true
tasks:
- name: Show script stdout
debug:
msg:
- "{{ server_info }}"
The hosts is actually a group of servers put in a group named workers (for example server1, server2 and server3), where just one is chosen (arbitrary) to run this task. Now I need to retrieve the information from this variable on the localhost, but as I don't know on which server the first task runs, I cannot explicitly reference it by using:
"{{ hostvars['server2']['server_info'] }}"
Does someone know if there is a way to retrieve this variable on the localhost?
Q: "I don't know on which server the first task runs."
A: It's irrelevant which server the first task runs on. The variable server_info will be declared on all of them. For example, given the inventory
shell> cat hosts
[workers]
server1
server2
server3
The playbook
- hosts: workers
gather_facts: false
run_once: true
tasks:
- set_fact:
server_info: winserver
- debug:
var: hostvars[item]['server_info']
loop: "{{ ansible_play_hosts_all }}"
gives
TASK [debug] *********************************************************
ok: [server1] => (item=server1) =>
ansible_loop_var: item
hostvars[item]['server_info']: winserver
item: server1
ok: [server1] => (item=server2) =>
ansible_loop_var: item
hostvars[item]['server_info']: winserver
item: server2
ok: [server1] => (item=server3) =>
ansible_loop_var: item
hostvars[item]['server_info']: winserver
item: server
You can pick any host you like. For example,
- hosts: localhost
gather_facts: false
run_once: true
tasks:
- debug:
var: hostvars.server2.server_info
gives
TASK [debug] ************************************************************
ok: [localhost] =>
hostvars.server2.server_info: winserver
I just found the answer to this question myself: through the use of
delegate_to: localhost
delegate_facts: true
This way the variable gets stored on the localhost.
- name: Some action on the workers
hosts: workers
gather_facts: false
run_once: true
tasks:
- name: Set var for server information
set_fact:
server_info:
"name": "winserver"
"os": "Microsoft Windows Server 2019 Datacenter"
delegate_to: localhost
delegate_facts: true
- name: Some action on the localhost
hosts: localhost
gather_facts: false
run_once: true
tasks:
- name: Show script stdout
debug:
msg:
- "{{ server_info }}"

Ansible loop with multiple register value

would you please help me with this problem:
I have a playbook with multiple tasks and each tasks contains loop and register for the output of the task. Last task is going to use lineinfile to create a csv report based on the previous registers. something like below:
- name: information
module:
xxxx: xxxx
xxxx: xxxxx
loop:
- xxxx
- xxxx
register: task1_info
- name: information
module:
xxxx: xxxx
xxxx: xxxxx
loop:
- xxxx
- xxxx
register: task2_info
- name: information
lineinfile:
path: xxxx
line: "{{ item.tags.Name }}, {{ item.lastName }}"
loop:
- task1_info.results
- task2_info.results
if i use only one register at the end it is working, but not loop through all registers. the other option is to write a task after each register which I don't think reasonable!!
I understand your use case that you like to append one list to an other or to merge two lists.
To do so you could use an approach like
---
- hosts: localhost
become: false
gather_facts: false
vars:
LIST_1:
- 1
- 2
- 3
LIST_2:
- A
- B
- C
tasks:
- name: Info
debug:
msg: "{{ item }}"
loop: "{{ LIST_1 + LIST_2 }}"
loop_control:
extended: true
label: "{{ansible_loop.index0 }}"
resulting into an output of
TASK [Info] ******************
ok: [localhost] => (item=0) =>
msg: 1
ok: [localhost] => (item=1) =>
msg: 2
ok: [localhost] => (item=2) =>
msg: 3
ok: [localhost] => (item=3) =>
msg: A
ok: [localhost] => (item=4) =>
msg: B
ok: [localhost] => (item=5) =>
msg: C
Credits to
Append list variable to another list in Ansible
Further Q&A
Combine two lists in Ansible when one list could be empty
Ansible: Merge two lists based on an attribute

How to detect unreachable target hosts in ansible

I wish to grab in a variable sshreachable if a target hosts all_hosts are reachable or not.
I wrote the below playbook for the same.
- name: Play 3- check telnet nodes
hosts: localhost
ignore_unreachable: yes
- name: Check all port numbers are accessible from current host
include_tasks: innertelnet.yml
with_items: "{{ groups['all_hosts'] }}"
cat innertelnet.yml
---
- name: Check ssh connectivity
block:
- raw: "ssh -o BatchMode=yes root#{{ item }} echo success"
ignore_errors: yes
register: sshcheck
- debug:
msg: "SSHCHECK variable:{{ sshcheck }}"
- set_fact:
sshreachable: 'SSH SUCCESS'
when: sshcheck.unreachable == 'false'
- set_fact:
sshreachable: 'SSH FAILED'
when: sshcheck.unreachable == 'true'
- debug:
msg: "INNERSSH1: {{ sshreachable }}"
Unfortunately, i get error like below:
Output:
TASK [raw] *********************************************************************
fatal: [localhost]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Shared connection to 10.9.9.126 closed.", "skip_reason": "Host localhost is unreachable", "unreachable": true}
TASK [debug] ***********************************************************************************************************************************************************
task path:
ok: [localhost] => {
"msg": "SSHCHECK variable:{'msg': u'Failed to connect to the host via ssh: Shared connection to 10.9.9.126 closed.', 'unreachable': True, 'changed': False}"
}
TASK [set_fact] ****************************************************************
skipping: [localhost]
TASK [set_fact] ****************************************************************
skipping: [localhost]
TASK [debug] *******************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'sshreachable' is undefined\n\nThe error appears to be in '/app/playbook/checkssh/innertelnet.yml': line 45, column 10, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n - debug:\n ^ here\n"}
PLAY RECAP *********************************************************************
10.0.116.194 : ok=101 changed=1 unreachable=9 failed=0 skipped=12 rescued=0 ignored=95
localhost : ok=5 changed=0 unreachable=1 failed=1 skipped=4 rescued=0 ignored=0
Can you please suggest changes to my code to get this to work?
The error seems to indicate that sshreachable variable is not getting set as the when: condition does not match. I.e. sshcheck.unreachable might not be something returned by raw.
For this purpose, command module should be enough, and we can evaluate the return code of the command to set_fact.
You could do something like:
- block:
- command: ssh -o BatchMode=yes user#host1 echo success
ignore_errors: yes
register: sshcheck
- set_fact:
sshreachable: "{{ sshcheck is success }}"
- debug:
msg: "Host1 reachable: {{ sshreachable | string }}"
Update:
raw module seems to work the same way. Example (including #mdaniel's valuable input):
- block:
- raw: ssh -o BatchMode=yes user#host1 echo success
ignore_errors: yes
register: sshcheck
- set_fact:
sshreachable: SSH SUCCESS
when: sshcheck is success
- set_fact:
sshreachable: SSH FAILED
when: sshcheck is failed
- debug:
msg: "Host1 reachable: {{ sshreachable }}"