mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-19 17:06:37 +00:00
Update notication service
This commit is contained in:
@@ -416,25 +416,34 @@ export function NotificationSettings() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<p className="text-[11px] font-medium text-muted-foreground">
|
<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>
|
</p>
|
||||||
<pre className="text-[11px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
|
<pre className="text-[11px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
|
||||||
{`# Create webhook endpoint on PBS
|
{`webhook: proxmenux-webhook
|
||||||
proxmox-backup-manager notification endpoint webhook create proxmenux-webhook \\
|
\turl http://<PVE_IP>:8008/api/notifications/webhook
|
||||||
--url http://<PVE_HOST_IP>:8008/api/notifications/webhook \\
|
\tmethod post
|
||||||
--header "X-Webhook-Secret=<YOUR_SECRET>"
|
\theader Content-Type:application/json
|
||||||
|
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}
|
||||||
|
|
||||||
# Create matcher to route PBS events
|
matcher: proxmenux-pbs
|
||||||
proxmox-backup-manager notification matcher create proxmenux-pbs \\
|
\ttarget proxmenux-webhook
|
||||||
--target proxmenux-webhook \\
|
\tmatch-severity warning,error`}
|
||||||
--match-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>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start gap-2 p-2 rounded-md bg-blue-500/10 border border-blue-500/20">
|
<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" />
|
<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">
|
<div className="text-[10px] text-blue-400/90 leading-relaxed space-y-1">
|
||||||
<p>
|
<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>
|
||||||
<p>
|
<p>
|
||||||
{"Replace <YOUR_SECRET> with the webhook secret shown in your notification settings."}
|
{"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
|
Backups launched from PVE are covered by the PVE webhook. PBS internal jobs
|
||||||
(Verify, Prune, GC, Sync) require separate configuration on the PBS server.
|
(Verify, Prune, GC, Sync) require separate configuration on the PBS server.
|
||||||
</p>
|
</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">
|
<pre className="text-[10px] bg-background p-2 rounded border border-border overflow-x-auto font-mono">
|
||||||
{`# On the PBS host:
|
{`webhook: proxmenux-webhook
|
||||||
proxmox-backup-manager notification endpoint webhook \\
|
\turl http://<PVE_IP>:8008/api/notifications/webhook
|
||||||
create proxmenux-webhook \\
|
\tmethod post
|
||||||
--url http://<PVE_IP>:8008/api/notifications/webhook \\
|
\theader Content-Type:application/json
|
||||||
--header "X-Webhook-Secret=<SECRET>"
|
\theader X-Webhook-Secret:{{ secrets.proxmenux_secret }}
|
||||||
|
|
||||||
proxmox-backup-manager notification matcher \\
|
matcher: proxmenux-pbs
|
||||||
create proxmenux-pbs \\
|
\ttarget proxmenux-webhook
|
||||||
--target proxmenux-webhook \\
|
\tmatch-severity warning,error`}
|
||||||
--match-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>
|
</pre>
|
||||||
<p className="text-[10px] text-muted-foreground">
|
<p className="text-[10px] text-muted-foreground">
|
||||||
{"Replace <PVE_IP> with this node's IP and <SECRET> with the webhook secret above."}
|
{"Replace <PVE_IP> with this node's IP and <SECRET> with the webhook secret above."}
|
||||||
|
|||||||
@@ -161,16 +161,21 @@ def send_notification():
|
|||||||
def setup_proxmox_webhook():
|
def setup_proxmox_webhook():
|
||||||
"""Automatically configure PVE notifications to call our webhook.
|
"""Automatically configure PVE notifications to call our webhook.
|
||||||
|
|
||||||
Idempotent: safe to call multiple times. Only creates/updates
|
Writes directly to /etc/pve/notifications.cfg (cluster filesystem)
|
||||||
ProxMenux-owned objects (proxmenux-webhook endpoint, proxmenux-default matcher).
|
instead of using pvesh, which has complex property-string formats
|
||||||
Never deletes or overrides user notification targets.
|
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
|
import secrets as secrets_mod
|
||||||
|
|
||||||
ENDPOINT_ID = 'proxmenux-webhook'
|
ENDPOINT_ID = 'proxmenux-webhook'
|
||||||
MATCHER_ID = 'proxmenux-default'
|
MATCHER_ID = 'proxmenux-default'
|
||||||
WEBHOOK_URL = 'http://127.0.0.1:8008/api/notifications/webhook'
|
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 = {
|
result = {
|
||||||
'configured': False,
|
'configured': False,
|
||||||
@@ -181,111 +186,152 @@ def setup_proxmox_webhook():
|
|||||||
'error': None,
|
'error': None,
|
||||||
}
|
}
|
||||||
|
|
||||||
def _run_pvesh(args: list, check: bool = True) -> tuple:
|
def _build_fallback(secret_val):
|
||||||
"""Run pvesh command. Returns (success, stdout, stderr)."""
|
"""Build manual instructions as fallback."""
|
||||||
try:
|
return [
|
||||||
proc = subprocess.run(
|
"# Add to /etc/pve/notifications.cfg:",
|
||||||
['pvesh'] + args,
|
f"webhook: {ENDPOINT_ID}",
|
||||||
capture_output=True, text=True, timeout=15
|
f"\turl {WEBHOOK_URL}",
|
||||||
)
|
"\tmethod post",
|
||||||
return proc.returncode == 0, proc.stdout.strip(), proc.stderr.strip()
|
f"\theader Content-Type:application/json",
|
||||||
except FileNotFoundError:
|
f"\theader X-Webhook-Secret:{{{{ secrets.proxmenux_secret }}}}",
|
||||||
return False, '', 'pvesh not found'
|
"",
|
||||||
except subprocess.TimeoutExpired:
|
"# Add to /etc/pve/priv/notifications.cfg:",
|
||||||
return False, '', 'pvesh timed out'
|
f"webhook: {ENDPOINT_ID}",
|
||||||
except Exception as e:
|
f"\tsecret proxmenux_secret {secret_val}",
|
||||||
return False, '', str(e)
|
"",
|
||||||
|
"# Also add a matcher block to /etc/pve/notifications.cfg:",
|
||||||
|
f"matcher: {MATCHER_ID}",
|
||||||
|
f"\ttarget {ENDPOINT_ID}",
|
||||||
|
"\tmatch-severity warning,error",
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Step 1: Ensure webhook secret exists
|
# ── Step 1: Ensure webhook secret exists ──
|
||||||
secret = notification_manager.get_webhook_secret()
|
secret = notification_manager.get_webhook_secret()
|
||||||
if not secret:
|
if not secret:
|
||||||
secret = secrets_mod.token_urlsafe(32)
|
secret = secrets_mod.token_urlsafe(32)
|
||||||
notification_manager._save_setting('webhook_secret', secret)
|
notification_manager._save_setting('webhook_secret', secret)
|
||||||
|
|
||||||
secret_header = f'X-Webhook-Secret={secret}'
|
# ── Step 2: Read current config ──
|
||||||
|
try:
|
||||||
# Step 2: Check if endpoint already exists
|
with open(NOTIFICATIONS_CFG, 'r') as f:
|
||||||
exists_ok, _, _ = _run_pvesh([
|
cfg_text = f.read()
|
||||||
'get', f'/cluster/notifications/endpoints/webhook/{ENDPOINT_ID}',
|
except FileNotFoundError:
|
||||||
'--output-format', 'json'
|
cfg_text = ''
|
||||||
])
|
except PermissionError:
|
||||||
|
result['error'] = f'Permission denied reading {NOTIFICATIONS_CFG}'
|
||||||
if exists_ok:
|
result['fallback_commands'] = _build_fallback(secret)
|
||||||
# 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}'
|
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
# Step 3: Create or update matcher
|
# Read private config (secrets)
|
||||||
matcher_exists, _, _ = _run_pvesh([
|
try:
|
||||||
'get', f'/cluster/notifications/matchers/{MATCHER_ID}',
|
with open(PRIV_CFG, 'r') as f:
|
||||||
'--output-format', 'json'
|
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:
|
# ── Step 3: Build / replace our endpoint block ──
|
||||||
ok_m, _, err_m = _run_pvesh([
|
endpoint_block = (
|
||||||
'set', f'/cluster/notifications/matchers/{MATCHER_ID}',
|
f"webhook: {ENDPOINT_ID}\n"
|
||||||
'--target', ENDPOINT_ID,
|
f"\turl {WEBHOOK_URL}\n"
|
||||||
'--match-severity', 'warning,error',
|
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:
|
else:
|
||||||
ok_m, _, err_m = _run_pvesh([
|
# Append with blank line separator
|
||||||
'create', '/cluster/notifications/matchers',
|
if cfg_text and not cfg_text.endswith('\n\n'):
|
||||||
'--name', MATCHER_ID,
|
cfg_text = cfg_text.rstrip('\n') + '\n\n'
|
||||||
'--target', ENDPOINT_ID,
|
cfg_text += endpoint_block
|
||||||
'--match-severity', 'warning,error',
|
|
||||||
])
|
|
||||||
|
|
||||||
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'] = [
|
result['fallback_commands'] = [
|
||||||
f'pvesh create /cluster/notifications/matchers '
|
f"# Add to {PRIV_CFG}:",
|
||||||
f'--name {MATCHER_ID} --target {ENDPOINT_ID} '
|
f"webhook: {ENDPOINT_ID}",
|
||||||
f'--match-severity warning,error',
|
f"\tsecret proxmenux_secret {secret}",
|
||||||
]
|
]
|
||||||
result['error'] = f'Endpoint OK, but matcher failed: {err_m}'
|
|
||||||
result['configured'] = False
|
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
result['configured'] = True
|
result['configured'] = True
|
||||||
result['secret'] = secret # Return so UI can display it
|
result['secret'] = secret
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
result['error'] = str(e)
|
result['error'] = str(e)
|
||||||
result['fallback_commands'] = [
|
try:
|
||||||
f'pvesh create /cluster/notifications/endpoints/webhook '
|
result['fallback_commands'] = _build_fallback(
|
||||||
f'--name {ENDPOINT_ID} --url {WEBHOOK_URL} --method post '
|
notification_manager.get_webhook_secret() or 'YOUR_SECRET'
|
||||||
f'--header "X-Webhook-Secret=YOUR_SECRET"',
|
)
|
||||||
f'pvesh create /cluster/notifications/matchers '
|
except Exception:
|
||||||
f'--name {MATCHER_ID} --target {ENDPOINT_ID} '
|
result['fallback_commands'] = _build_fallback('YOUR_SECRET')
|
||||||
f'--match-severity warning,error',
|
|
||||||
]
|
|
||||||
return jsonify(result), 200
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -769,10 +769,9 @@ class PollingCollector:
|
|||||||
class ProxmoxHookWatcher:
|
class ProxmoxHookWatcher:
|
||||||
"""Receives native Proxmox VE notifications via local webhook endpoint.
|
"""Receives native Proxmox VE notifications via local webhook endpoint.
|
||||||
|
|
||||||
Proxmox can be configured to send notifications to a webhook target:
|
Configured automatically via /etc/pve/notifications.cfg (endpoint +
|
||||||
pvesh create /cluster/notifications/endpoints/webhook/proxmenux \\
|
matcher blocks). The setup-webhook API writes these blocks on first
|
||||||
--url http://127.0.0.1:8008/api/notifications/webhook \\
|
enable. See flask_notification_routes.py for details.
|
||||||
--method POST
|
|
||||||
|
|
||||||
Payload varies by source (storage, replication, cluster, PBS, apt).
|
Payload varies by source (storage, replication, cluster, PBS, apt).
|
||||||
This class normalizes them into NotificationEvent objects.
|
This class normalizes them into NotificationEvent objects.
|
||||||
|
|||||||
Reference in New Issue
Block a user