mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-22 10:26:26 +00:00
Update notification service
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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}',
|
||||||
|
|||||||
Reference in New Issue
Block a user