Update notification service

This commit is contained in:
MacRimi
2026-02-24 18:20:43 +01:00
parent 05cd21d44e
commit f43feb825f
3 changed files with 145 additions and 15 deletions

View File

@@ -686,16 +686,6 @@ def proxmox_webhook():
else: else:
return _reject(400, 'empty_payload', 400) return _reject(400, 'empty_payload', 400)
# DEBUG: Log full webhook payload to file for analysis
import json as _json
try:
with open('/tmp/proxmenux_webhook_payload.log', 'a') as _f:
_f.write(f"\n{'='*60}\n{time.strftime('%Y-%m-%d %H:%M:%S')}\n")
_f.write(_json.dumps(payload, indent=2, default=str, ensure_ascii=False))
_f.write('\n')
except Exception:
pass
result = notification_manager.process_webhook(payload) result = notification_manager.process_webhook(payload)
# Always return 200 to PVE -- a non-200 makes PVE report the webhook as broken. # 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. # The 'accepted' field in the JSON body indicates actual processing status.

View File

@@ -996,6 +996,7 @@ class ProxmoxHookWatcher:
'hostname': pve_hostname, 'hostname': pve_hostname,
'pve_type': pve_type, 'pve_type': pve_type,
'pve_message': message, 'pve_message': message,
'pve_title': title,
'title': title, 'title': title,
'job_id': pve_job_id, 'job_id': pve_job_id,
} }

View File

@@ -12,11 +12,129 @@ Author: MacRimi
""" """
import json import json
import re
import socket import socket
import time import time
import urllib.request import urllib.request
import urllib.error import urllib.error
from typing import Dict, Any, Optional from typing import Dict, Any, Optional, List
# ─── vzdump message parser ───────────────────────────────────────
def _parse_vzdump_message(message: str) -> Optional[Dict[str, Any]]:
"""Parse a PVE vzdump notification message into structured data.
PVE vzdump messages contain:
- A table: VMID Name Status Time Size Filename
- Totals: Total running time: Xs / Total size: X GiB
- Full logs per VM
Returns dict with 'vms' list, 'total_time', 'total_size', or None.
"""
if not message:
return None
vms: List[Dict[str, str]] = []
total_time = ''
total_size = ''
lines = message.split('\n')
# Find the table header line
header_idx = -1
for i, line in enumerate(lines):
if re.match(r'\s*VMID\s+Name\s+Status', line, re.IGNORECASE):
header_idx = i
break
if header_idx >= 0:
# Parse column positions from header
header = lines[header_idx]
# Parse table rows after header
for line in lines[header_idx + 1:]:
stripped = line.strip()
if not stripped or stripped.startswith('Total') or stripped.startswith('Logs') or stripped.startswith('='):
break
# Table row: VMID Name Status Time Size Filename
# Use regex to parse flexible whitespace columns
m = re.match(
r'\s*(\d+)\s+' # VMID
r'(\S+)\s+' # Name
r'(\S+)\s+' # Status (ok/error)
r'(\S+)\s+' # Time
r'([\d.]+\s+\S+)\s+' # Size (e.g. "1.423 GiB")
r'(\S+)', # Filename
line
)
if m:
vms.append({
'vmid': m.group(1),
'name': m.group(2),
'status': m.group(3),
'time': m.group(4),
'size': m.group(5),
'filename': m.group(6).split('/')[-1], # just filename
})
# Extract totals
for line in lines:
m_time = re.search(r'Total running time:\s*(.+)', line)
if m_time:
total_time = m_time.group(1).strip()
m_size = re.search(r'Total size:\s*(.+)', line)
if m_size:
total_size = m_size.group(1).strip()
if not vms and not total_size:
return None
return {
'vms': vms,
'total_time': total_time,
'total_size': total_size,
'vm_count': len(vms),
}
def _format_vzdump_body(parsed: Dict[str, Any], is_success: bool) -> str:
"""Format parsed vzdump data into a clean Telegram-friendly message."""
parts = []
for vm in parsed.get('vms', []):
status = vm.get('status', '').lower()
if status == 'ok':
icon = '\u2705' # green check
else:
icon = '\u274C' # red X
vm_line = f"{icon} ID {vm['vmid']} ({vm['name']})"
parts.append(vm_line)
if vm.get('size'):
parts.append(f" Size: {vm['size']}")
if vm.get('time'):
parts.append(f" Duration: {vm['time']}")
if vm.get('filename'):
parts.append(f" File: {vm['filename']}")
parts.append('') # blank line between VMs
# Summary
vm_count = parsed.get('vm_count', 0)
if vm_count > 0 or parsed.get('total_size'):
parts.append('Summary:')
if vm_count:
ok_count = sum(1 for v in parsed.get('vms', []) if v.get('status', '').lower() == 'ok')
fail_count = vm_count - ok_count
parts.append(f" Total: {vm_count} backup(s)")
if fail_count:
parts.append(f" Failed: {fail_count}")
if parsed.get('total_size'):
parts.append(f" Total size: {parsed['total_size']}")
if parsed.get('total_time'):
parts.append(f" Total time: {parsed['total_time']}")
return '\n'.join(parts)
# ─── Severity Icons ────────────────────────────────────────────── # ─── Severity Icons ──────────────────────────────────────────────
@@ -475,10 +593,31 @@ def render_template(event_type: str, data: Dict[str, Any]) -> Dict[str, Any]:
except (KeyError, ValueError): except (KeyError, ValueError):
title = template['title'] title = template['title']
try: # ── PVE vzdump special formatting ──
body_text = template['body'].format(**variables) # When the event came from PVE webhook with a full vzdump message,
except (KeyError, ValueError): # parse the table/logs and format a rich body instead of the sparse template.
body_text = template['body'] pve_message = data.get('pve_message', '')
pve_title = data.get('pve_title', '')
if event_type in ('backup_complete', 'backup_fail') and pve_message:
parsed = _parse_vzdump_message(pve_message)
if parsed:
is_success = (event_type == 'backup_complete')
body_text = _format_vzdump_body(parsed, is_success)
# Use PVE's own title if available (contains hostname and status)
if pve_title:
title = pve_title
else:
# Couldn't parse -- use PVE raw message as body
body_text = pve_message.strip()
elif event_type == 'system_mail' and pve_message:
# System mail -- use PVE message directly (mail bounce, cron, smartd)
body_text = pve_message.strip()[:1000]
else:
try:
body_text = template['body'].format(**variables)
except (KeyError, ValueError):
body_text = template['body']
# Clean up: remove empty lines and consecutive duplicate lines # Clean up: remove empty lines and consecutive duplicate lines
cleaned_lines = [] cleaned_lines = []