Skip to content

Commit 05ffba1

Browse files
committed
Merge pull request #82 from wiseflat/dev/build/hook-notify
hook notify
2 parents d069dea + a78fdf3 commit 05ffba1

11 files changed

Lines changed: 359 additions & 25 deletions

File tree

ansible/ansible.cfg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
[defaults]
2-
callbacks_enabled = minimal
2+
callbacks_enabled = minimal, webhook_notifier
33
collections_path = ./collections
44
deprecation_warnings = False
55
host_key_checking = False
66
interpreter_python = auto_silent
77
inventory = inventory.py
88
library = ./library
99
action_plugins = ./plugins/action
10+
callback_plugins = ./plugins/callback
1011
lookup_plugins = ./plugins/lookup
1112
roles_path = ./roles
1213
stdout_callback = unixy

ansible/playbooks/paas/firewall.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,10 @@
44
hosts: "{{ hosts_limit | default('infrastructure') }}"
55
gather_facts: true
66
become: true
7+
pre_tasks:
8+
- name: End the play for hosts because ufw_enable is disabled
9+
ansible.builtin.meta: end_host
10+
when: ufw_enable is defined and not ufw_enable
11+
712
roles:
813
- ansible-ufw

ansible/playbooks/paas/nvidia.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
- name: End the play for hosts that don't have nvidia gpu
2222
ansible.builtin.meta: end_host
23-
when: not nvidia_enable
23+
when: not (nvidia_enable is defined and nvidia_enable)
2424

2525
- name: Créer le répertoire du keyring s'il n'existe pas
2626
ansible.builtin.file:

ansible/playbooks/saas/roles/simplestack_ansible/templates/nomad.hcl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ job "{{ domain }}" {
4444
SIMPLE_STACK_UI_URL = "{{ lookup('simple-stack-ui', type='secret', key=domain, subkey='url', missing='error') }}"
4545
GITHUB_API_TOKEN = "{{ lookup('simple-stack-ui', type='secret', key=domain, subkey='github_api_token', missing='error') }}"
4646
ANSIBLE_LOOKUP_PLUGINS = "/ansible/plugins/lookup"
47+
ANSIBLE_CALLBACK_PLUGINS = "/ansible/plugins/callback"
48+
ANSIBLE_CALLBACKS_ENABLED = "minimal,webhook_notifier"
4749
}
4850

4951
config {
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import (absolute_import, annotations, division, print_function)
3+
__metaclass__ = type
4+
5+
DOCUMENTATION = '''
6+
name: webhook_notifier
7+
type: notification
8+
short_description: Envoie des notifications webhook au démarrage et à la fin des playbooks
9+
description:
10+
- Ce plugin de callback envoie des requêtes HTTP POST à un webhook configuré
11+
- Il notifie au démarrage du playbook, en cas de succès et en cas d'échec
12+
- Utilise l'API Simple Stack UI pour envoyer les notifications
13+
options:
14+
webhook_enabled:
15+
description: Active ou désactive les notifications
16+
env:
17+
- name: ANSIBLE_WEBHOOK_ENABLED
18+
ini:
19+
- section: webhook_notifier
20+
key: enabled
21+
default: true
22+
type: bool
23+
'''
24+
25+
import base64
26+
import json
27+
import os
28+
import socket
29+
import datetime
30+
import subprocess
31+
32+
from ansible.plugins.callback import CallbackBase
33+
from ansible.utils.display import Display
34+
35+
display = Display()
36+
37+
38+
class CallbackModule(CallbackBase):
39+
"""
40+
Plugin de callback pour envoyer des notifications webhook
41+
lors des différentes étapes d'exécution d'un playbook Ansible.
42+
43+
Configuration via variables d'environnement ou variables Ansible:
44+
- SIMPLE_STACK_UI_URL / webhook_api_url
45+
- SIMPLE_STACK_UI_USER / webhook_user
46+
- SIMPLE_STACK_UI_PASSWORD / webhook_password
47+
"""
48+
49+
CALLBACK_VERSION = 2.0
50+
CALLBACK_TYPE = 'notification'
51+
CALLBACK_NAME = 'webhook_notifier'
52+
CALLBACK_NEEDS_WHITELIST = True
53+
54+
def __init__(self):
55+
super(CallbackModule, self).__init__()
56+
self.playbook_name = None
57+
self.playbook_path = None
58+
self.start_time = None
59+
self.hosts = []
60+
self.disabled = False
61+
self.play_vars = {}
62+
63+
def set_options(self, task_keys=None, var_options=None, direct=None):
64+
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
65+
66+
self.webhook_enabled = self.get_option('webhook_enabled')
67+
68+
if not self.webhook_enabled:
69+
display.vvv("webhook_notifier: Plugin désactivé via configuration")
70+
self.disabled = True
71+
72+
def _get_connection_config(self, variables: dict = None):
73+
"""
74+
Récupère les informations de connexion à l'API.
75+
Priorité: variables d'environnement > variables Ansible > valeur par défaut
76+
"""
77+
variables = variables or self.play_vars or {}
78+
79+
api_url = (
80+
os.environ.get("SIMPLE_STACK_UI_URL")
81+
or variables.get("webhook_api_url")
82+
or "http://127.0.0.1:8000"
83+
)
84+
85+
username = (
86+
os.environ.get("SIMPLE_STACK_UI_USER")
87+
or variables.get("webhook_user")
88+
)
89+
90+
password = (
91+
os.environ.get("SIMPLE_STACK_UI_PASSWORD")
92+
or variables.get("webhook_password")
93+
)
94+
95+
return api_url, username, password
96+
97+
def _send_webhook(self, event_type: str, status: str, message: str, extra_data: dict = None):
98+
"""
99+
Envoie une notification au webhook configuré via curl (subprocess).
100+
Utilise curl pour éviter les problèmes de fork sur macOS.
101+
"""
102+
if self.disabled:
103+
return
104+
105+
try:
106+
api_url, username, password = self._get_connection_config()
107+
108+
if not api_url:
109+
display.warning("webhook_notifier: URL de l'API non configurée, notification ignorée")
110+
return
111+
112+
payload = {
113+
"schema": "events_create",
114+
"data": {
115+
"event_type": event_type,
116+
"status": status,
117+
"message": message,
118+
"timestamp": datetime.datetime.utcnow().isoformat() + "Z",
119+
"playbook": {
120+
"name": self.playbook_name or "unknown",
121+
"path": self.playbook_path or "unknown",
122+
},
123+
"execution": {
124+
"hostname": socket.gethostname(),
125+
"user": os.environ.get("USER", "unknown"),
126+
}
127+
}
128+
}
129+
130+
if self.start_time:
131+
payload["data"]["execution"]["start_time"] = self.start_time.isoformat() + "Z"
132+
payload["data"]["duration_seconds"] = (
133+
datetime.datetime.utcnow() - self.start_time
134+
).total_seconds()
135+
136+
if self.hosts:
137+
payload["data"]["hosts"] = [str(h) for h in self.hosts]
138+
139+
if extra_data:
140+
payload["data"].update(extra_data)
141+
142+
webhook_endpoint = f"{api_url}/api"
143+
json_payload = json.dumps(payload)
144+
145+
display.vvv(f"webhook_notifier: Envoi vers {webhook_endpoint}")
146+
display.vvvv(f"webhook_notifier: Payload: {json.dumps(payload, indent=2)}")
147+
148+
# Construire la commande curl
149+
curl_cmd = [
150+
"curl", "-s", "-S",
151+
"-X", "POST",
152+
"-H", "Content-Type: application/json",
153+
"-H", "User-Agent: Ansible-Webhook-Notifier/1.0",
154+
"--connect-timeout", "5",
155+
"--max-time", "10",
156+
"-d", json_payload,
157+
]
158+
159+
# Ajouter l'authentification si configurée
160+
if username and password:
161+
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
162+
curl_cmd.extend(["-H", f"Authorization: Bearer {token}"])
163+
164+
curl_cmd.append(webhook_endpoint)
165+
166+
# Exécuter curl en arrière-plan (non-bloquant)
167+
# start_new_session=True détache le processus du groupe de processus parent
168+
subprocess.Popen(
169+
curl_cmd,
170+
stdout=subprocess.DEVNULL,
171+
stderr=subprocess.DEVNULL,
172+
start_new_session=True
173+
)
174+
175+
display.vvv(f"webhook_notifier: Requête envoyée")
176+
177+
except Exception as e:
178+
display.warning(f"webhook_notifier: Erreur lors de l'envoi du webhook: {str(e)}")
179+
180+
# -------------------------------------------------------------------------
181+
# Callback: Démarrage du playbook
182+
# -------------------------------------------------------------------------
183+
def v2_playbook_on_start(self, playbook):
184+
"""Appelée au démarrage du playbook."""
185+
try:
186+
self.playbook_path = str(playbook._file_name) if playbook._file_name else "unknown"
187+
self.playbook_name = os.path.basename(self.playbook_path)
188+
self.start_time = datetime.datetime.utcnow()
189+
190+
display.vvv(f"webhook_notifier: Playbook démarré - {self.playbook_name}")
191+
192+
self._send_webhook(
193+
event_type="playbook_start",
194+
status="started",
195+
message=f"Playbook '{self.playbook_name}' démarré"
196+
)
197+
except Exception as e:
198+
display.warning(f"webhook_notifier: Erreur dans v2_playbook_on_start: {str(e)}")
199+
200+
# -------------------------------------------------------------------------
201+
# Callback: Statistiques finales (succès ou échec)
202+
# -------------------------------------------------------------------------
203+
def v2_playbook_on_stats(self, stats):
204+
"""
205+
Appelée à la fin du playbook avec les statistiques d'exécution.
206+
Permet de déterminer si le playbook a réussi ou échoué.
207+
"""
208+
try:
209+
hosts_stats = {}
210+
total_failures = 0
211+
total_unreachable = 0
212+
total_ok = 0
213+
total_changed = 0
214+
total_skipped = 0
215+
216+
for host in stats.processed.keys():
217+
summary = stats.summarize(host)
218+
hosts_stats[str(host)] = dict(summary)
219+
total_failures += summary.get('failures', 0)
220+
total_unreachable += summary.get('unreachable', 0)
221+
total_ok += summary.get('ok', 0)
222+
total_changed += summary.get('changed', 0)
223+
total_skipped += summary.get('skipped', 0)
224+
225+
has_failures = total_failures > 0 or total_unreachable > 0
226+
227+
stats_summary = {
228+
"stats": {
229+
"total_hosts": len(stats.processed),
230+
"ok": total_ok,
231+
"changed": total_changed,
232+
"failures": total_failures,
233+
"unreachable": total_unreachable,
234+
"skipped": total_skipped,
235+
},
236+
"hosts_details": hosts_stats
237+
}
238+
239+
if has_failures:
240+
failed_hosts = [
241+
str(host) for host, summary in hosts_stats.items()
242+
if summary.get('failures', 0) > 0 or summary.get('unreachable', 0) > 0
243+
]
244+
245+
display.vvv(f"webhook_notifier: Playbook échoué - {self.playbook_name}")
246+
247+
self._send_webhook(
248+
event_type="playbook_failure",
249+
status="failure",
250+
message=f"Playbook '{self.playbook_name}' échoué sur {len(failed_hosts)} hôte(s): {', '.join(failed_hosts)}",
251+
extra_data=stats_summary
252+
)
253+
else:
254+
display.vvv(f"webhook_notifier: Playbook réussi - {self.playbook_name}")
255+
256+
self._send_webhook(
257+
event_type="playbook_success",
258+
status="success",
259+
message=f"Playbook '{self.playbook_name}' terminé avec succès sur {len(stats.processed)} hôte(s)",
260+
extra_data=stats_summary
261+
)
262+
except Exception as e:
263+
display.warning(f"webhook_notifier: Erreur dans v2_playbook_on_stats: {str(e)}")
264+
265+
# -------------------------------------------------------------------------
266+
# Callbacks optionnelles pour collecter plus d'informations
267+
# -------------------------------------------------------------------------
268+
def v2_playbook_on_play_start(self, play):
269+
"""Appelée au démarrage de chaque play - collecte les hôtes ciblés et les variables."""
270+
try:
271+
variable_manager = play.get_variable_manager()
272+
raw_vars = variable_manager.get_vars() or {}
273+
274+
self.play_vars = {}
275+
for key in ['webhook_api_url', 'webhook_user', 'webhook_password']:
276+
if key in raw_vars:
277+
self.play_vars[key] = str(raw_vars[key])
278+
279+
extra_vars = variable_manager.extra_vars or {}
280+
for key in ['webhook_api_url', 'webhook_user', 'webhook_password']:
281+
if key in extra_vars:
282+
self.play_vars[key] = str(extra_vars[key])
283+
284+
display.vvvv(f"webhook_notifier: Variables récupérées: {list(self.play_vars.keys())}")
285+
286+
hosts = raw_vars.get('ansible_play_hosts_all', [])
287+
if hosts:
288+
self.hosts = [str(h) for h in hosts]
289+
else:
290+
self.hosts = [str(play.hosts)]
291+
292+
except Exception as e:
293+
display.vvvv(f"webhook_notifier: Impossible de récupérer les variables: {e}")
294+
self.play_vars = {}
295+
try:
296+
self.hosts = [str(play.hosts)]
297+
except Exception:
298+
self.hosts = []

ansible/rulebook.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,14 @@
4444
extra_vars:
4545
hosts_limit: "{{ event.payload.meta.hosts }}"
4646

47+
- name: paas-scan_exporter
48+
condition: event.payload.type == "paas-scan_exporter"
49+
actions:
50+
- run_playbook:
51+
name: playbooks/paas/scan_exporter.yml
52+
extra_vars:
53+
hosts_limit: "{{ event.payload.meta.hosts }}"
54+
4755
- name: saas-deploy
4856
condition: event.payload.type == "saas-deploy"
4957
actions:

0 commit comments

Comments
 (0)