mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-19 21:26:47 +00:00
fix: use display name in runtime notifications
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]"
|
||||
|
||||
Reference in New Issue
Block a user