mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-27 00:56:40 +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_notification_routes import notification_bp # noqa: E402
|
||||||
from flask_oci_routes import oci_bp # noqa: E402
|
from flask_oci_routes import oci_bp # noqa: E402
|
||||||
from flask_audit_routes import audit_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
|
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
|
from jwt_middleware import require_auth, require_auth_or_ticket, require_admin_scope # noqa: E402
|
||||||
import auth_manager # noqa: E402
|
import auth_manager # noqa: E402
|
||||||
@@ -1464,6 +1464,11 @@ def _health_collector_loop():
|
|||||||
if not hostname:
|
if not hostname:
|
||||||
import socket as _sock
|
import socket as _sock
|
||||||
hostname = _sock.gethostname()
|
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
|
# Capture journal context for AI enrichment
|
||||||
# Extract category keys and reasons for keyword matching
|
# 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()
|
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 ───────────────────────────────
|
# ─── Encryption for Sensitive Data ───────────────────────────────
|
||||||
#
|
#
|
||||||
# Audit Tier 4 #24 flagged the previous implementation as trivially reversible:
|
# Audit Tier 4 #24 flagged the previous implementation as trivially reversible:
|
||||||
@@ -1223,6 +1257,13 @@ class NotificationManager:
|
|||||||
|
|
||||||
def _dispatch_event(self, event: NotificationEvent):
|
def _dispatch_event(self, event: NotificationEvent):
|
||||||
"""Shared dispatch pipeline: cooldown -> rate limit -> render -> send."""
|
"""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).
|
# Suppress VM/CT start/stop during active backups (second layer of defense).
|
||||||
# The primary filter is in TaskWatcher, but timing gaps can let events
|
# The primary filter is in TaskWatcher, but timing gaps can let events
|
||||||
# slip through. This catch-all filter checks at dispatch time.
|
# slip through. This catch-all filter checks at dispatch time.
|
||||||
@@ -2376,6 +2417,9 @@ class NotificationManager:
|
|||||||
}
|
}
|
||||||
|
|
||||||
runtime_data = dict(data or {})
|
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())
|
runtime_data.setdefault('_notification_language', self._notification_language())
|
||||||
|
|
||||||
# Render template if available
|
# Render template if available
|
||||||
|
|||||||
@@ -253,6 +253,49 @@ class RuntimeCatalogTests(unittest.TestCase):
|
|||||||
self.assertIn("účtovníctvo", channel.payload[1])
|
self.assertIn("účtovníctvo", channel.payload[1])
|
||||||
self.assertNotIn("is now running", 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):
|
def test_email_channel_chrome_uses_the_runtime_catalog(self):
|
||||||
channel = object.__new__(notification_channels.EmailChannel)
|
channel = object.__new__(notification_channels.EmailChannel)
|
||||||
channel.subject_prefix = "[ProxMenux]"
|
channel.subject_prefix = "[ProxMenux]"
|
||||||
|
|||||||
Reference in New Issue
Block a user