callbacks: Add ntfy callback plugin
This plugin sends a notification using _ntfy_ whenever a playbook fails. This will be useful especially for automated deployments when the playbook was not launched manually.dynamic-inventory
parent
f705e98fab
commit
a3a2dde6ab
|
@ -5,3 +5,5 @@
|
||||||
/victoria-metrics-*.tar.gz
|
/victoria-metrics-*.tar.gz
|
||||||
/victoria-metrics-*/
|
/victoria-metrics-*/
|
||||||
/tmp/
|
/tmp/
|
||||||
|
__pycache__/
|
||||||
|
*.py[co]
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
[defaults]
|
[defaults]
|
||||||
inventory = hosts
|
inventory = hosts
|
||||||
|
|
||||||
|
callback_plugins = plugins/callback
|
||||||
|
|
||||||
gathering = smart
|
gathering = smart
|
||||||
fact_caching = jsonfile
|
fact_caching = jsonfile
|
||||||
fact_caching_connection = .fact-cache
|
fact_caching_connection = .fact-cache
|
||||||
|
@ -10,3 +12,6 @@ fact_caching_connection = .fact-cache
|
||||||
force_valid_group_names = ignore
|
force_valid_group_names = ignore
|
||||||
|
|
||||||
remote_tmp = /var/tmp
|
remote_tmp = /var/tmp
|
||||||
|
|
||||||
|
[callback_ntfy]
|
||||||
|
server = https://ntfy.pyrocufflink.blue
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
from pathlib import Path
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from ansible.errors import AnsibleError
|
||||||
|
from ansible.executor.stats import AggregateStats
|
||||||
|
from ansible.playbook import Playbook
|
||||||
|
from ansible.plugins.callback import CallbackBase
|
||||||
|
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
author: Dustin C. Hatch
|
||||||
|
name: ntfy
|
||||||
|
short_description: Send notifications to ntfy.sh
|
||||||
|
description:
|
||||||
|
- This plugin sends playbook failure notifications via ntfy.sh.
|
||||||
|
options:
|
||||||
|
server:
|
||||||
|
description: ntfy.sh server
|
||||||
|
type: str
|
||||||
|
default: https://ntfy.sh
|
||||||
|
env:
|
||||||
|
- name: NTFY_SERVER
|
||||||
|
ini:
|
||||||
|
- section: callback_ntfy
|
||||||
|
key: server
|
||||||
|
topic:
|
||||||
|
description: ntfy.sh topic name
|
||||||
|
type: str
|
||||||
|
default: ansible
|
||||||
|
env:
|
||||||
|
- name: NTFY_TOPIC
|
||||||
|
ini:
|
||||||
|
- section: callback_ntfy
|
||||||
|
key: topic
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
class CallbackModule(CallbackBase):
|
||||||
|
CALLBACK_VERSION = 1.0
|
||||||
|
CALLBACK_TYPE = 'notification'
|
||||||
|
CALLBACK_NAME = 'ntfy'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.playbook = None
|
||||||
|
|
||||||
|
def set_options(self, task_keys=None, var_options=None, direct=None):
|
||||||
|
super().set_options(task_keys, var_options, direct)
|
||||||
|
ntfy_server = self.get_option('server').rstrip('/')
|
||||||
|
if '://' not in ntfy_server:
|
||||||
|
ntfy_server = f'http://{ntfy_server}'
|
||||||
|
ntfy_topic = self.get_option('topic')
|
||||||
|
self.ntfy_url = f'{ntfy_server}/{ntfy_topic}'
|
||||||
|
|
||||||
|
def v2_playbook_on_start(self, playbook: Playbook):
|
||||||
|
self.playbook = playbook
|
||||||
|
|
||||||
|
def v2_playbook_on_stats(self, stats: AggregateStats):
|
||||||
|
if not self.playbook:
|
||||||
|
return
|
||||||
|
if not stats.failures and not stats.dark:
|
||||||
|
return
|
||||||
|
assert self.playbook._file_name
|
||||||
|
playbook = Path(self.playbook._file_name)
|
||||||
|
results = {}
|
||||||
|
hosts = set(stats.failures.keys()).union(stats.dark.keys())
|
||||||
|
title = f'Playbook {playbook.name} failed for {len(hosts)} hosts'
|
||||||
|
for host in hosts:
|
||||||
|
results[host] = {
|
||||||
|
'ok': stats.ok.get(host, 0),
|
||||||
|
'changed': stats.changed.get(host, 0),
|
||||||
|
'unreachable': stats.dark.get(host, 0),
|
||||||
|
'failed': stats.failures.get(host, 0),
|
||||||
|
'skipped': stats.skipped.get(host, 0),
|
||||||
|
'rescued': stats.rescued.get(host, 0),
|
||||||
|
'ignored': stats.ignored.get(host, 0),
|
||||||
|
}
|
||||||
|
lines = []
|
||||||
|
for host, result in results.items():
|
||||||
|
result_txt = ' '.join(f'{k}={v}' for k, v in result.items())
|
||||||
|
lines.append(f'{host} : {result_txt}')
|
||||||
|
message = '\n'.join(lines)
|
||||||
|
req = urllib.request.Request(
|
||||||
|
self.ntfy_url,
|
||||||
|
method='POST',
|
||||||
|
data=message.encode('utf-8'),
|
||||||
|
)
|
||||||
|
req.add_header('Title', title)
|
||||||
|
req.add_header('Tag', 'red_circle')
|
||||||
|
with urllib.request.urlopen(req) as response:
|
||||||
|
status_code = response.getcode()
|
||||||
|
if status_code < 200 or status_code >= 300:
|
||||||
|
response_data = response.read()
|
||||||
|
raise AnsibleError(f'Failed to send notification: {response_data.decode()}')
|
Loading…
Reference in New Issue