mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-08-09 17:26:20 +00:00
Merge pull request #283 from Vaso73/fix/vzdump-webhook-truncation
fix(notifications): preserve full vzdump webhook messages
This commit is contained in:
@@ -1548,11 +1548,15 @@ def proxmox_webhook():
|
|||||||
return _reject(400, 'missing_title', 400)
|
return _reject(400, 'missing_title', 400)
|
||||||
if not isinstance(message, str):
|
if not isinstance(message, str):
|
||||||
message = str(message) if message is not None else ''
|
message = str(message) if message is not None else ''
|
||||||
# Bound runaway sizes — webhooks shouldn't exceed a few KB of text.
|
# Keep the full webhook body for downstream parsers. PVE vzdump
|
||||||
|
# reports can legitimately exceed Telegram's 4096-character delivery
|
||||||
|
# limit when a job covers many VM/CT guests. Truncating here can cut a
|
||||||
|
# table row in half before notification_templates._parse_vzdump_message
|
||||||
|
# sees it, which makes a successful backup look like a failed one.
|
||||||
|
# Channel-specific senders (for example TelegramChannel._split_message)
|
||||||
|
# are responsible for splitting the final formatted notification.
|
||||||
if len(title) > 256:
|
if len(title) > 256:
|
||||||
payload['title'] = title[:256]
|
payload['title'] = title[:256]
|
||||||
if len(message) > 4096:
|
|
||||||
payload['message'] = message[:4096]
|
|
||||||
# Severity normalisation: accept the canonical set, default to 'info'.
|
# Severity normalisation: accept the canonical set, default to 'info'.
|
||||||
sev = (payload.get('severity') or '').lower()
|
sev = (payload.get('severity') or '').lower()
|
||||||
if sev not in {'info', 'warning', 'critical', 'error', 'notice'}:
|
if sev not in {'info', 'warning', 'critical', 'error', 'notice'}:
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPTS_DIR = Path(__file__).resolve().parents[1]
|
||||||
|
if str(SCRIPTS_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||||
|
|
||||||
|
from notification_templates import _format_vzdump_body, _parse_vzdump_message
|
||||||
|
|
||||||
|
|
||||||
|
def _vzdump_table_row(vmid, name, status="OK", time_value="00:00:12", size="1.23 GiB"):
|
||||||
|
filename = (
|
||||||
|
f"/mnt/pve/fast-zfs-backup/dump/"
|
||||||
|
f"vzdump-lxc-{vmid}-2026_08_09-04_00_00.tar.zst"
|
||||||
|
)
|
||||||
|
return "{:<8}{:<22}{:<10}{:<10}{:<14}{}".format(
|
||||||
|
str(vmid),
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
time_value,
|
||||||
|
size,
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_long_vzdump_report():
|
||||||
|
header = "{:<8}{:<22}{:<10}{:<10}{:<14}{}".format(
|
||||||
|
"VMID",
|
||||||
|
"Name",
|
||||||
|
"Status",
|
||||||
|
"Time",
|
||||||
|
"Size",
|
||||||
|
"Filename",
|
||||||
|
)
|
||||||
|
rows = [_vzdump_table_row(100 + i, f"ct-{100 + i:03d}") for i in range(28)]
|
||||||
|
dockflare_row = _vzdump_table_row(129, "dockflare")
|
||||||
|
|
||||||
|
message_prefix = (
|
||||||
|
"Proxmox vzdump report\n\n"
|
||||||
|
+ header
|
||||||
|
+ "\n"
|
||||||
|
+ "\n".join(rows)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Place the 4096-character cut right after the dockflare VMID + name
|
||||||
|
# columns, before the Status/Time/Size/Filename columns. This reproduces
|
||||||
|
# the production symptom: CT 129 appears as a failed backup with no detail.
|
||||||
|
cut_at = len(message_prefix) + 8 + 22
|
||||||
|
filler = "X" * max(0, 4096 - cut_at) + "\n"
|
||||||
|
|
||||||
|
return (
|
||||||
|
filler
|
||||||
|
+ message_prefix
|
||||||
|
+ dockflare_row
|
||||||
|
+ "\nTotal running time: 00:12:34\nTotal size: 42.0 GiB\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class VzdumpWebhookTruncationTests(unittest.TestCase):
|
||||||
|
def test_truncating_vzdump_report_at_4096_can_create_false_failed_backup(self):
|
||||||
|
full_message = _make_long_vzdump_report()
|
||||||
|
truncated_message = full_message[:4096]
|
||||||
|
|
||||||
|
full_parsed = _parse_vzdump_message(full_message)
|
||||||
|
full_body = _format_vzdump_body(full_parsed, True)
|
||||||
|
truncated_parsed = _parse_vzdump_message(truncated_message)
|
||||||
|
truncated_body = _format_vzdump_body(truncated_parsed, True)
|
||||||
|
|
||||||
|
full_dockflare = [
|
||||||
|
vm for vm in full_parsed["vms"] if vm.get("vmid") == "129"
|
||||||
|
][0]
|
||||||
|
truncated_dockflare = [
|
||||||
|
vm for vm in truncated_parsed["vms"] if vm.get("vmid") == "129"
|
||||||
|
][0]
|
||||||
|
|
||||||
|
self.assertEqual(full_dockflare["status"], "OK")
|
||||||
|
self.assertIn("✅ CT dockflare (129)", full_body)
|
||||||
|
self.assertNotIn("failed", full_body)
|
||||||
|
|
||||||
|
self.assertEqual(truncated_dockflare["name"], "dockflare")
|
||||||
|
self.assertEqual(truncated_dockflare["status"], "")
|
||||||
|
self.assertIn("❌ dockflare (129)", truncated_body)
|
||||||
|
self.assertIn("❌ 1 failed", truncated_body)
|
||||||
|
|
||||||
|
def test_webhook_handler_does_not_truncate_message_before_parsing(self):
|
||||||
|
source = (SCRIPTS_DIR / "flask_notification_routes.py").read_text()
|
||||||
|
|
||||||
|
self.assertNotIn("payload['message'] = message[:4096]", source)
|
||||||
|
self.assertNotIn('payload["message"] = message[:4096]', source)
|
||||||
|
self.assertIn("Keep the full webhook body for downstream parsers", source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user