Update notication service

This commit is contained in:
MacRimi
2026-02-19 17:26:36 +01:00
parent 7c5cdb9161
commit 4ce2699a48
3 changed files with 174 additions and 111 deletions

View File

@@ -416,25 +416,34 @@ export function NotificationSettings() {
</div>
<div className="space-y-1.5">
<p className="text-[11px] font-medium text-muted-foreground">
Run on the PBS host:
Add to /etc/proxmox-backup/notifications.cfg on the PBS host:
</p>
<pre className="text-[11px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
{`# Create webhook endpoint on PBS
proxmox-backup-manager notification endpoint webhook create proxmenux-webhook \\
--url http://<PVE_HOST_IP>:8008/api/notifications/webhook \\
--header "X-Webhook-Secret=<YOUR_SECRET>"
{`webhook: proxmenux-webhook
\turl http://<PVE_IP>:8008/api/notifications/webhook
\tmethod post
\theader Content-Type:application/json
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}
# Create matcher to route PBS events
proxmox-backup-manager notification matcher create proxmenux-pbs \\
--target proxmenux-webhook \\
--match-severity warning,error`}
matcher: proxmenux-pbs
\ttarget proxmenux-webhook
\tmatch-severity warning,error`}
</pre>
</div>
<div className="space-y-1.5">
<p className="text-[11px] font-medium text-muted-foreground">
Add to /etc/proxmox-backup/notifications-priv.cfg:
</p>
<pre className="text-[11px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
{`webhook: proxmenux-webhook
\tsecret proxmenux_secret <YOUR_SECRET>`}
</pre>
</div>
<div className="flex items-start gap-2 p-2 rounded-md bg-blue-500/10 border border-blue-500/20">
<Info className="h-3.5 w-3.5 text-blue-400 shrink-0 mt-0.5" />
<div className="text-[10px] text-blue-400/90 leading-relaxed space-y-1">
<p>
{"Replace <PVE_HOST_IP> with the IP address of this PVE node (not 127.0.0.1, unless PBS runs on the same host)."}
{"Replace <PVE_IP> with the IP of this PVE node (not 127.0.0.1, unless PBS runs on the same host)."}
</p>
<p>
{"Replace <YOUR_SECRET> with the webhook secret shown in your notification settings."}
@@ -1098,17 +1107,26 @@ proxmox-backup-manager notification matcher create proxmenux-pbs \\
Backups launched from PVE are covered by the PVE webhook. PBS internal jobs
(Verify, Prune, GC, Sync) require separate configuration on the PBS server.
</p>
<p className="text-[10px] font-medium text-muted-foreground">
Add to /etc/proxmox-backup/notifications.cfg:
</p>
<pre className="text-[10px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
{`# On the PBS host:
proxmox-backup-manager notification endpoint webhook \\
create proxmenux-webhook \\
--url http://<PVE_IP>:8008/api/notifications/webhook \\
--header "X-Webhook-Secret=<SECRET>"
{`webhook: proxmenux-webhook
\turl http://<PVE_IP>:8008/api/notifications/webhook
\tmethod post
\theader Content-Type:application/json
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}
proxmox-backup-manager notification matcher \\
create proxmenux-pbs \\
--target proxmenux-webhook \\
--match-severity warning,error`}
matcher: proxmenux-pbs
\ttarget proxmenux-webhook
\tmatch-severity warning,error`}
</pre>
<p className="text-[10px] font-medium text-muted-foreground">
Add to /etc/proxmox-backup/notifications-priv.cfg:
</p>
<pre className="text-[10px] bg-background p-1.5 rounded border border-border overflow-x-auto font-mono">
{`webhook: proxmenux-webhook
\tsecret proxmenux_secret <SECRET>`}
</pre>
<p className="text-[10px] text-muted-foreground">
{"Replace <PVE_IP> with this node's IP and <SECRET> with the webhook secret above."}

View File

@@ -161,16 +161,21 @@ def send_notification():
def setup_proxmox_webhook():
"""Automatically configure PVE notifications to call our webhook.
Idempotent: safe to call multiple times. Only creates/updates
ProxMenux-owned objects (proxmenux-webhook endpoint, proxmenux-default matcher).
Never deletes or overrides user notification targets.
Writes directly to /etc/pve/notifications.cfg (cluster filesystem)
instead of using pvesh, which has complex property-string formats
for headers and secrets that vary between PVE versions.
Idempotent: safe to call multiple times.
Only creates/updates ProxMenux-owned objects.
"""
import subprocess
import re
import secrets as secrets_mod
ENDPOINT_ID = 'proxmenux-webhook'
MATCHER_ID = 'proxmenux-default'
WEBHOOK_URL = 'http://127.0.0.1:8008/api/notifications/webhook'
NOTIFICATIONS_CFG = '/etc/pve/notifications.cfg'
PRIV_CFG = '/etc/pve/priv/notifications.cfg'
result = {
'configured': False,
@@ -181,111 +186,152 @@ def setup_proxmox_webhook():
'error': None,
}
def _run_pvesh(args: list, check: bool = True) -> tuple:
"""Run pvesh command. Returns (success, stdout, stderr)."""
try:
proc = subprocess.run(
['pvesh'] + args,
capture_output=True, text=True, timeout=15
)
return proc.returncode == 0, proc.stdout.strip(), proc.stderr.strip()
except FileNotFoundError:
return False, '', 'pvesh not found'
except subprocess.TimeoutExpired:
return False, '', 'pvesh timed out'
except Exception as e:
return False, '', str(e)
def _build_fallback(secret_val):
"""Build manual instructions as fallback."""
return [
"# Add to /etc/pve/notifications.cfg:",
f"webhook: {ENDPOINT_ID}",
f"\turl {WEBHOOK_URL}",
"\tmethod post",
f"\theader Content-Type:application/json",
f"\theader X-Webhook-Secret:{{{{ secrets.proxmenux_secret }}}}",
"",
"# Add to /etc/pve/priv/notifications.cfg:",
f"webhook: {ENDPOINT_ID}",
f"\tsecret proxmenux_secret {secret_val}",
"",
"# Also add a matcher block to /etc/pve/notifications.cfg:",
f"matcher: {MATCHER_ID}",
f"\ttarget {ENDPOINT_ID}",
"\tmatch-severity warning,error",
]
try:
# Step 1: Ensure webhook secret exists
# ── Step 1: Ensure webhook secret exists ──
secret = notification_manager.get_webhook_secret()
if not secret:
secret = secrets_mod.token_urlsafe(32)
notification_manager._save_setting('webhook_secret', secret)
secret_header = f'X-Webhook-Secret={secret}'
# Step 2: Check if endpoint already exists
exists_ok, _, _ = _run_pvesh([
'get', f'/cluster/notifications/endpoints/webhook/{ENDPOINT_ID}',
'--output-format', 'json'
])
if exists_ok:
# Update existing endpoint
ok, _, err = _run_pvesh([
'set', f'/cluster/notifications/endpoints/webhook/{ENDPOINT_ID}',
'--url', WEBHOOK_URL,
'--method', 'post',
'--header', secret_header,
])
else:
# Create new endpoint
ok, _, err = _run_pvesh([
'create', '/cluster/notifications/endpoints/webhook',
'--name', ENDPOINT_ID,
'--url', WEBHOOK_URL,
'--method', 'post',
'--header', secret_header,
])
if not ok:
# Build fallback commands for manual execution
result['fallback_commands'] = [
f'pvesh create /cluster/notifications/endpoints/webhook '
f'--name {ENDPOINT_ID} --url {WEBHOOK_URL} --method post '
f'--header "{secret_header}"',
f'pvesh create /cluster/notifications/matchers '
f'--name {MATCHER_ID} --target {ENDPOINT_ID} '
f'--match-severity warning,error',
]
result['error'] = f'Failed to configure endpoint: {err}'
# ── Step 2: Read current config ──
try:
with open(NOTIFICATIONS_CFG, 'r') as f:
cfg_text = f.read()
except FileNotFoundError:
cfg_text = ''
except PermissionError:
result['error'] = f'Permission denied reading {NOTIFICATIONS_CFG}'
result['fallback_commands'] = _build_fallback(secret)
return jsonify(result), 200
# Step 3: Create or update matcher
matcher_exists, _, _ = _run_pvesh([
'get', f'/cluster/notifications/matchers/{MATCHER_ID}',
'--output-format', 'json'
])
# Read private config (secrets)
try:
with open(PRIV_CFG, 'r') as f:
priv_text = f.read()
except FileNotFoundError:
priv_text = ''
except PermissionError:
result['error'] = f'Permission denied reading {PRIV_CFG}'
result['fallback_commands'] = _build_fallback(secret)
return jsonify(result), 200
if matcher_exists:
ok_m, _, err_m = _run_pvesh([
'set', f'/cluster/notifications/matchers/{MATCHER_ID}',
'--target', ENDPOINT_ID,
'--match-severity', 'warning,error',
])
# ── Step 3: Build / replace our endpoint block ──
endpoint_block = (
f"webhook: {ENDPOINT_ID}\n"
f"\turl {WEBHOOK_URL}\n"
f"\tmethod post\n"
f"\theader Content-Type:application/json\n"
f"\theader X-Webhook-Secret:{{{{ secrets.proxmenux_secret }}}}\n"
)
# Regex to find existing block: "webhook: proxmenux-webhook\n..." until next block or EOF
block_re = re.compile(
rf'^webhook:\s+{re.escape(ENDPOINT_ID)}\n(?:\t[^\n]*\n)*',
re.MULTILINE
)
if block_re.search(cfg_text):
cfg_text = block_re.sub(endpoint_block, cfg_text)
else:
ok_m, _, err_m = _run_pvesh([
'create', '/cluster/notifications/matchers',
'--name', MATCHER_ID,
'--target', ENDPOINT_ID,
'--match-severity', 'warning,error',
])
# Append with blank line separator
if cfg_text and not cfg_text.endswith('\n\n'):
cfg_text = cfg_text.rstrip('\n') + '\n\n'
cfg_text += endpoint_block
if not ok_m:
# ── Step 4: Build / replace matcher block ──
matcher_block = (
f"matcher: {MATCHER_ID}\n"
f"\ttarget {ENDPOINT_ID}\n"
f"\tmatch-severity warning,error\n"
)
matcher_re = re.compile(
rf'^matcher:\s+{re.escape(MATCHER_ID)}\n(?:\t[^\n]*\n)*',
re.MULTILINE
)
if matcher_re.search(cfg_text):
cfg_text = matcher_re.sub(matcher_block, cfg_text)
else:
if not cfg_text.endswith('\n\n'):
cfg_text = cfg_text.rstrip('\n') + '\n\n'
cfg_text += matcher_block
# ── Step 5: Build / replace private config (secret) ──
priv_block = (
f"webhook: {ENDPOINT_ID}\n"
f"\tsecret proxmenux_secret {secret}\n"
)
priv_re = re.compile(
rf'^webhook:\s+{re.escape(ENDPOINT_ID)}\n(?:\t[^\n]*\n)*',
re.MULTILINE
)
if priv_re.search(priv_text):
priv_text = priv_re.sub(priv_block, priv_text)
else:
if priv_text and not priv_text.endswith('\n\n'):
priv_text = priv_text.rstrip('\n') + '\n\n'
priv_text += priv_block
# ── Step 6: Write back ──
try:
with open(NOTIFICATIONS_CFG, 'w') as f:
f.write(cfg_text)
except PermissionError:
result['error'] = f'Permission denied writing {NOTIFICATIONS_CFG}'
result['fallback_commands'] = _build_fallback(secret)
return jsonify(result), 200
try:
with open(PRIV_CFG, 'w') as f:
f.write(priv_text)
except PermissionError:
# Rollback is complex; just warn
result['error'] = (
f'Endpoint configured but secret could not be written to {PRIV_CFG}. '
f'Add manually: secret proxmenux_secret {secret}'
)
result['fallback_commands'] = [
f'pvesh create /cluster/notifications/matchers '
f'--name {MATCHER_ID} --target {ENDPOINT_ID} '
f'--match-severity warning,error',
f"# Add to {PRIV_CFG}:",
f"webhook: {ENDPOINT_ID}",
f"\tsecret proxmenux_secret {secret}",
]
result['error'] = f'Endpoint OK, but matcher failed: {err_m}'
result['configured'] = False
return jsonify(result), 200
result['configured'] = True
result['secret'] = secret # Return so UI can display it
result['secret'] = secret
return jsonify(result), 200
except Exception as e:
result['error'] = str(e)
result['fallback_commands'] = [
f'pvesh create /cluster/notifications/endpoints/webhook '
f'--name {ENDPOINT_ID} --url {WEBHOOK_URL} --method post '
f'--header "X-Webhook-Secret=YOUR_SECRET"',
f'pvesh create /cluster/notifications/matchers '
f'--name {MATCHER_ID} --target {ENDPOINT_ID} '
f'--match-severity warning,error',
]
try:
result['fallback_commands'] = _build_fallback(
notification_manager.get_webhook_secret() or 'YOUR_SECRET'
)
except Exception:
result['fallback_commands'] = _build_fallback('YOUR_SECRET')
return jsonify(result), 200

View File

@@ -769,10 +769,9 @@ class PollingCollector:
class ProxmoxHookWatcher:
"""Receives native Proxmox VE notifications via local webhook endpoint.
Proxmox can be configured to send notifications to a webhook target:
pvesh create /cluster/notifications/endpoints/webhook/proxmenux \\
--url http://127.0.0.1:8008/api/notifications/webhook \\
--method POST
Configured automatically via /etc/pve/notifications.cfg (endpoint +
matcher blocks). The setup-webhook API writes these blocks on first
enable. See flask_notification_routes.py for details.
Payload varies by source (storage, replication, cluster, PBS, apt).
This class normalizes them into NotificationEvent objects.