Update notification service

This commit is contained in:
MacRimi
2026-02-21 20:33:22 +01:00
parent ec21050fad
commit 0d854ae42b
2 changed files with 86 additions and 24 deletions

View File

@@ -231,23 +231,21 @@ def _pve_remove_our_blocks(text, headers_to_remove):
def _build_webhook_fallback(): def _build_webhook_fallback():
"""Build fallback manual commands for webhook setup.""" """Build fallback manual commands for webhook setup."""
return [ return [
"# Append to END of /etc/pve/notifications.cfg", "# Append to END of /etc/pve/notifications.cfg",
"# (do NOT delete existing content):", "# (do NOT delete existing content):",
"", "",
f"webhook: {_PVE_ENDPOINT_ID}", f"webhook: {_PVE_ENDPOINT_ID}",
f"\tmethod post", f"\tmethod post",
f"\turl {_PVE_WEBHOOK_URL}", f"\turl {_PVE_WEBHOOK_URL}",
'\theader Content-Type=application/json', "",
'\tbody {"title":"{{ escape title }}","message":"{{ escape message }}","severity":"{{ severity }}","timestamp":{{ timestamp }},"fields":{{ json fields }}}', f"matcher: {_PVE_MATCHER_ID}",
"", f"\ttarget {_PVE_ENDPOINT_ID}",
f"matcher: {_PVE_MATCHER_ID}", "\tmode all",
f"\ttarget {_PVE_ENDPOINT_ID}", "",
"\tmode all", "# ALSO append to /etc/pve/priv/notifications.cfg :",
"", f"webhook: {_PVE_ENDPOINT_ID}",
"# ALSO append to /etc/pve/priv/notifications.cfg :", ]
f"webhook: {_PVE_ENDPOINT_ID}",
]
def setup_pve_webhook_core() -> dict: def setup_pve_webhook_core() -> dict:
@@ -314,12 +312,13 @@ def setup_pve_webhook_core() -> dict:
# - A matching entry in priv/notifications.cfg (even if empty) # - A matching entry in priv/notifications.cfg (even if empty)
# The body template uses PVE's Handlebars syntax to pass notification # The body template uses PVE's Handlebars syntax to pass notification
# metadata to our webhook handler as structured JSON. # metadata to our webhook handler as structured JSON.
# PVE sends JSON by default (title, message, severity, timestamp, fields).
# Do NOT set header or body -- PVE's config parser rejects custom header
# formats and a missing/malformed header line corrupts the ENTIRE config.
endpoint_block = ( endpoint_block = (
f"webhook: {_PVE_ENDPOINT_ID}\n" f"webhook: {_PVE_ENDPOINT_ID}\n"
f"\tmethod post\n" f"\tmethod post\n"
f"\turl {_PVE_WEBHOOK_URL}\n" f"\turl {_PVE_WEBHOOK_URL}\n"
f'\theader Content-Type=application/json\n'
f'\tbody {{"title":"{{{{ escape title }}}}","message":"{{{{ escape message }}}}","severity":"{{{{ severity }}}}","timestamp":{{{{ timestamp }}}},"fields":{{{{ json fields }}}}}}\n'
) )
matcher_block = ( matcher_block = (
@@ -382,6 +381,35 @@ def setup_pve_webhook_core() -> dict:
except Exception: except Exception:
pass pass
# ── Step 10: Configure body and header via pvesh API ──
# Writing header/body directly to the config file uses a different
# internal format that PVE's parser rejects. Using pvesh set handles
# escaping and the priv-config wire format correctly.
import subprocess
# Body template: PVE Handlebars that sends JSON to our webhook
body_template = '{"title":"{{ escape title }}","message":"{{ escape message }}","severity":"{{ severity }}","timestamp":"{{ timestamp }}"}'
try:
# Set body template
subprocess.run(
['pvesh', 'set',
f'/cluster/notifications/endpoints/webhook/{_PVE_ENDPOINT_ID}',
'--body', body_template],
capture_output=True, text=True, timeout=10
)
# Set Content-Type header
subprocess.run(
['pvesh', 'set',
f'/cluster/notifications/endpoints/webhook/{_PVE_ENDPOINT_ID}',
'--header', 'Content-Type:application/json'],
capture_output=True, text=True, timeout=10
)
except Exception as e:
# Non-fatal: webhook still works, just sends raw format
result['body_config_warning'] = str(e)
result['configured'] = True result['configured'] = True
result['secret'] = secret result['secret'] = secret
return result return result
@@ -635,14 +663,40 @@ def proxmox_webhook():
# ── Parse and process payload ── # ── Parse and process payload ──
try: try:
content_type = request.content_type or ''
raw_data = request.get_data(as_text=True) or ''
# Try JSON first
payload = request.get_json(silent=True) or {} payload = request.get_json(silent=True) or {}
# If not JSON, try form data
if not payload: if not payload:
payload = dict(request.form) payload = dict(request.form)
# If still empty, try parsing raw data as JSON (PVE may not set Content-Type)
if not payload and raw_data:
try:
import json
payload = json.loads(raw_data)
except (json.JSONDecodeError, ValueError):
pass
# If still empty, create a minimal test event from raw data
if not payload: if not payload:
return _reject(400, 'invalid_payload', 400) if raw_data:
payload = {
'type': 'webhook_test',
'title': 'PVE Webhook Test',
'body': raw_data[:500],
'severity': 'info',
}
else:
return _reject(400, 'empty_payload', 400)
result = notification_manager.process_webhook(payload) result = notification_manager.process_webhook(payload)
status_code = 200 if result.get('accepted') else 400 # Always return 200 to PVE -- a non-200 makes PVE report the webhook as broken.
return jsonify(result), status_code # The 'accepted' field in the JSON body indicates actual processing status.
except Exception: return jsonify(result), 200
return jsonify({'accepted': False, 'error': 'internal_error'}), 500 except Exception as e:
# Still return 200 to avoid PVE flagging the webhook as broken
return jsonify({'accepted': False, 'error': 'internal_error', 'detail': str(e)}), 200

View File

@@ -363,6 +363,14 @@ TEMPLATES = {
'default_enabled': True, 'default_enabled': True,
}, },
# ── PVE webhook test ──
'webhook_test': {
'title': '{hostname}: Webhook test received',
'body': 'PVE webhook connectivity test successful.\n{reason}',
'group': 'system',
'default_enabled': True,
},
# ── Burst aggregation summaries ── # ── Burst aggregation summaries ──
'burst_auth_fail': { 'burst_auth_fail': {
'title': '{hostname}: {count} auth failures in {window}', 'title': '{hostname}: {count} auth failures in {window}',