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():
"""Build fallback manual commands for webhook setup."""
return [
"# Append to END of /etc/pve/notifications.cfg",
"# (do NOT delete existing content):",
"",
f"webhook: {_PVE_ENDPOINT_ID}",
f"\tmethod post",
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}",
"\tmode all",
"",
"# ALSO append to /etc/pve/priv/notifications.cfg :",
f"webhook: {_PVE_ENDPOINT_ID}",
]
return [
"# Append to END of /etc/pve/notifications.cfg",
"# (do NOT delete existing content):",
"",
f"webhook: {_PVE_ENDPOINT_ID}",
f"\tmethod post",
f"\turl {_PVE_WEBHOOK_URL}",
"",
f"matcher: {_PVE_MATCHER_ID}",
f"\ttarget {_PVE_ENDPOINT_ID}",
"\tmode all",
"",
"# ALSO append to /etc/pve/priv/notifications.cfg :",
f"webhook: {_PVE_ENDPOINT_ID}",
]
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)
# The body template uses PVE's Handlebars syntax to pass notification
# 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 = (
f"webhook: {_PVE_ENDPOINT_ID}\n"
f"\tmethod post\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 = (
@@ -382,6 +381,35 @@ def setup_pve_webhook_core() -> dict:
except Exception:
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['secret'] = secret
return result
@@ -635,14 +663,40 @@ def proxmox_webhook():
# ── Parse and process payload ──
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 {}
# If not JSON, try form data
if not payload:
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:
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)
status_code = 200 if result.get('accepted') else 400
return jsonify(result), status_code
except Exception:
return jsonify({'accepted': False, 'error': 'internal_error'}), 500
# Always return 200 to PVE -- a non-200 makes PVE report the webhook as broken.
# The 'accepted' field in the JSON body indicates actual processing status.
return jsonify(result), 200
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