mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-05-01 11:56:21 +00:00
Update notification service
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
Reference in New Issue
Block a user