fix: use display name in runtime notifications

This commit is contained in:
VAIO73
2026-09-17 12:49:41 +02:00
parent ad23a1167f
commit 25b71efab0
3 changed files with 93 additions and 1 deletions
+6 -1
View File
@@ -93,7 +93,7 @@ from flask_security_routes import security_bp # noqa: E402
from flask_notification_routes import notification_bp # noqa: E402
from flask_oci_routes import oci_bp # noqa: E402
from flask_audit_routes import audit_bp # noqa: E402
from notification_manager import notification_manager # noqa: E402
from notification_manager import notification_manager, resolve_notification_hostname # noqa: E402
import post_install_versions # noqa: E402 — Sprint 12A: detect post-install function updates
from jwt_middleware import require_auth, require_auth_or_ticket, require_admin_scope # noqa: E402
import auth_manager # noqa: E402
@@ -1464,6 +1464,11 @@ def _health_collector_loop():
if not hostname:
import socket as _sock
hostname = _sock.gethostname()
# The health collector builds its title before the event
# reaches NotificationManager, so normalize here as well.
hostname = resolve_notification_hostname(
hostname, notification_manager._config,
)
# Capture journal context for AI enrichment
# Extract category keys and reasons for keyword matching
+44
View File
@@ -211,6 +211,40 @@ def _resolve_display_hostname(config: Optional[Dict[str, str]] = None) -> str:
return socket.gethostname()
def resolve_notification_hostname(value: Any = None,
config: Optional[Dict[str, str]] = None) -> str:
"""Return the configured display name for a hostname of this local node.
Some event producers pass ``socket.gethostname()`` explicitly while
others let the notification layer resolve it. A configured display name
must have the same result in both cases. Keep a hostname that is not an
alias of this machine intact: it can identify a remote node forwarded to
this monitor.
"""
configured_name = (config or {}).get('hostname', '')
configured_name = str(configured_name or '').strip()
candidate = str(value or '').strip()
if not configured_name:
return candidate or _resolve_display_hostname(config)
if not candidate:
return configured_name
local_aliases = set()
for resolver in (socket.gethostname, socket.getfqdn):
try:
hostname = str(resolver() or '').strip()
except Exception:
hostname = ''
if hostname:
local_aliases.add(hostname.casefold())
local_aliases.add(hostname.split('.', 1)[0].casefold())
if candidate.casefold() in local_aliases:
return configured_name
return candidate
# ─── Encryption for Sensitive Data ───────────────────────────────
#
# Audit Tier 4 #24 flagged the previous implementation as trivially reversible:
@@ -1223,6 +1257,13 @@ class NotificationManager:
def _dispatch_event(self, event: NotificationEvent):
"""Shared dispatch pipeline: cooldown -> rate limit -> render -> send."""
# Event sources may supply the local kernel hostname themselves.
# Normalize it here so every delivery path honours the configured
# notification display name, including newly added event producers.
event.data['hostname'] = resolve_notification_hostname(
event.data.get('hostname'), self._config,
)
# Suppress VM/CT start/stop during active backups (second layer of defense).
# The primary filter is in TaskWatcher, but timing gaps can let events
# slip through. This catch-all filter checks at dispatch time.
@@ -2376,6 +2417,9 @@ class NotificationManager:
}
runtime_data = dict(data or {})
runtime_data['hostname'] = resolve_notification_hostname(
runtime_data.get('hostname'), self._config,
)
runtime_data.setdefault('_notification_language', self._notification_language())
# Render template if available
@@ -253,6 +253,49 @@ class RuntimeCatalogTests(unittest.TestCase):
self.assertIn("účtovníctvo", channel.payload[1])
self.assertNotIn("is now running", channel.payload[1])
def test_display_name_replaces_only_the_local_runtime_hostname(self):
config = {"hostname": "HomeLAB_2"}
with mock.patch.object(notification_manager.socket, "gethostname", return_value="homelab-2"), \
mock.patch.object(notification_manager.socket, "getfqdn", return_value="homelab-2.home.lab"):
self.assertEqual(
notification_manager.resolve_notification_hostname("homelab-2", config),
"HomeLAB_2",
)
self.assertEqual(
notification_manager.resolve_notification_hostname("remote-pve", config),
"remote-pve",
)
class RecordingChannel:
def __init__(self):
self.payload = None
def send(self, title, body, severity, data=None):
self.payload = (title, body, severity, data)
return {"success": True}
manager = notification_manager.NotificationManager()
channel = RecordingChannel()
manager._channels = {"telegram": channel}
manager._config = {
"notification_language": "sk",
"hostname": "HomeLAB_2",
"ai_enabled": "false",
"telegram.rich_format": "false",
}
with mock.patch.object(notification_manager.socket, "gethostname", return_value="homelab-2"), \
mock.patch.object(notification_manager.socket, "getfqdn", return_value="homelab-2.home.lab"), \
mock.patch.object(manager, "_record_history"):
result = manager.send_notification(
"docker_stack_update_available", "INFO", "", "",
data={"hostname": "homelab-2", "vmid": "210", "ct_name": "repopulse", "count": "1", "details": "Docker Engine"},
skip_toggle_check=True,
)
self.assertTrue(result["success"])
self.assertIn("HomeLAB_2: Na CT 210 sú dostupné aktualizácie Docker", channel.payload[0])
self.assertNotIn("homelab-2:", channel.payload[0])
def test_email_channel_chrome_uses_the_runtime_catalog(self):
channel = object.__new__(notification_channels.EmailChannel)
channel.subject_prefix = "[ProxMenux]"