diff --git a/README.md b/README.md index 633d2c2..2139a4a 100644 --- a/README.md +++ b/README.md @@ -687,6 +687,44 @@ zeigten korrekt die neue Fehlermeldung, da die realen Switch-IPs (192.168.200.x) vom Test-Host aus nicht erreichbar sind — vorher hätte das fälschlich als 43× „PoE deaktiviert“/„PoE aktiviert“ geloggt. +## SSH-Login-Banner (dynamisches MOTD) + +Ein SSH-Login auf den TESM-Host selbst zeigt statt der Standard-Ubuntu- +Willkommensmeldung einen kompakten Live-Status der Flotte — Bernstein/Amber +als Akzentfarbe (passend zum Logo), fett hervorgehobene Zahlen: + +``` +TESM · TimEShepManager +überwachen · booten · anbinden (DHCP) · Betriebssystem geben (PXE) + +3 Clients online · 45 down · DHCP: installiert, gestoppt +``` + +Umgesetzt als zwei Teile: +- **`srv/tesm/motd_banner.py`** — liest den Live-Status aus genau denselben + Quellen wie das Web-Dashboard, ohne selbst zu prüfen: `get_device_status()` + parst dieselbe Check-Log-Datei (`live.log`) wie das Dashboard, + `_detect_dhcp_status()`/`_dhcp_read_leases()` lesen Dienststatus bzw. die + Kea-Lease-CSV direkt. Läuft über `venv/bin/python3` (importiert `app.py`, + das Flask u.a. braucht, die nur dort installiert sind — dasselbe Muster + wie `generate_ips.py`). Ein Fehler beim Datenzugriff lässt die jeweilige + Zeile nur einen Platzhaltertext zeigen, statt den Login zu stören. +- **`etc/update-motd.d/50-tesm`** — der eigentliche, ausführbare + Ubuntu-„dynamic MOTD"-Hook (`run-parts /etc/update-motd.d`, von + `pam_motd.so` bei jedem SSH-Login neu ausgeführt und nach + `/run/motd.dynamic` geschrieben). `timeout 3` verhindert, dass ein + hängender DB-/Dateizugriff den Login blockiert. + +**Bewusst kein PXE-Status** in der Live-Zeile: anders als Überwachen/ +Booten/DHCP ist PXE bislang nur Teil der Tagline, keine echte, mit Daten +hinterlegte Funktion der App — ein erfundener Wert wäre keine Live-Daten. + +Die Standard-Ubuntu-MOTD-Skripte (`00-header`, `10-help-text`, +`50-motd-news`, `60-unminimize`, `91-release-upgrade`, +`92-unattended-upgrades`) werden vom Installer per `chmod -x` deaktiviert +(nicht gelöscht — jederzeit reversibel mit `chmod +x`), damit nur noch der +TESM-Banner erscheint. + ## Netzwerkeinstellungen des Hosts (Systemeinstellungen) Eigene, klar getrennte Funktion von obigem DHCP-*Server*: hier geht es um diff --git a/etc/update-motd.d/50-tesm b/etc/update-motd.d/50-tesm new file mode 100644 index 0000000..2b52651 --- /dev/null +++ b/etc/update-motd.d/50-tesm @@ -0,0 +1,10 @@ +#!/bin/sh +# TESM -- Live-Flotten-Status im SSH-Login-Banner (dynamisches MOTD, siehe +# /etc/pam.d/sshd: "session optional pam_motd.so motd=/run/motd.dynamic"). +# Ruft motd_banner.py im TESM-Quellverzeichnis über dessen venv auf, da +# app.py (die eigentliche Datenquelle) Flask u.a. importiert, die nur dort +# installiert sind -- gleiches Muster wie generate_ips.py/poe.sh. +# "timeout" verhindert, dass ein hängender DB-/Dateizugriff den Login +# blockiert; "|| true" sorgt dafür, dass ein Fehler hier nie den SSH-Login +# selbst stört -- im schlimmsten Fall bleibt der Banner einfach leer. +timeout 3 /srv/tesm/venv/bin/python3 /srv/tesm/motd_banner.py 2>/dev/null || true diff --git a/install.sh b/install.sh index b9d2da7..cec4d07 100644 --- a/install.sh +++ b/install.sh @@ -63,6 +63,20 @@ sudo cp "$REPO_DIR"/etc/systemd/system/*.service /etc/systemd/system/ sudo cp "$REPO_DIR"/etc/systemd/system/*.timer /etc/systemd/system/ print_status "Scripts and units copied" +# ---- SSH-Login-Banner (MOTD) ---- +# Live-Flotten-Status (Online/Offline, DHCP) statt der Standard-Ubuntu- +# Willkommensmeldung -- siehe srv/tesm/motd_banner.py für die Datenquellen. +# Die Standard-Skripte bleiben erhalten, werden aber deaktiviert (chmod -x, +# nicht gelöscht), damit ein Rückbau jederzeit per chmod +x möglich ist. +step "Setting up TESM SSH login banner" +sudo cp "$REPO_DIR/etc/update-motd.d/50-tesm" /etc/update-motd.d/50-tesm +sudo chmod 755 /etc/update-motd.d/50-tesm +sudo chmod -x /etc/update-motd.d/00-header /etc/update-motd.d/10-help-text \ + /etc/update-motd.d/50-motd-news /etc/update-motd.d/60-unminimize \ + /etc/update-motd.d/91-release-upgrade /etc/update-motd.d/92-unattended-upgrades \ + 2>/dev/null || true +print_status "SSH login banner configured" + # ---- nginx ---- step "Configuring nginx reverse proxy" sudo cp "$REPO_DIR/etc/nginx/sites-available/tesm" /etc/nginx/sites-available/tesm diff --git a/srv/tesm/motd_banner.py b/srv/tesm/motd_banner.py new file mode 100644 index 0000000..6c4939b --- /dev/null +++ b/srv/tesm/motd_banner.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Live-Flotten-Banner für die SSH-Login-Message (MOTD). + +Liest denselben Live-Status wie das Web-Dashboard, direkt aus den +gleichen Quellen (siehe app.py): + - Online/Offline: get_device_status() parst dieselbe Check-Log-Datei + (live.log) wie das Dashboard, statt selbst zu pingen -- ein Login + soll niemals eine eigene Prüfung anstoßen. + - DHCP: _detect_dhcp_status()/_dhcp_read_leases() lesen Dienststatus + bzw. Kea-Lease-CSV direkt, kein eigener Zustand. + +Eingebunden über /etc/update-motd.d/50-tesm, das dieses Skript per +venv/bin/python3 aufruft -- app.py importiert Flask u.a., die nur in +der venv installiert sind (dasselbe Muster wie generate_ips.py, das +poe.sh auf demselben Weg konsumiert). + +Bewusst NICHT enthalten: PXE-Status. Anders als überwachen/booten/ +DHCP ist PXE (noch) keine echte, mit Daten hinterlegte Funktion der +App (bislang nur Teil der Tagline) -- ein erfundener "PXE: idle"-Wert +wäre keine Live-Daten, sondern eine Behauptung. +""" +from app import _detect_dhcp_status, _dhcp_read_leases, get_db_connection, get_device_status + +AMBER = "\033[38;2;226;166;60m" +DIM = "\033[2m" +BOLD = "\033[1m" +RESET = "\033[0m" + + +def _fleet_line(amber, dim, bold, reset): + try: + conn = get_db_connection() + devices = conn.execute( + "SELECT mac, name, is_active FROM devices WHERE deleted_at IS NULL" + ).fetchall() + conn.close() + status, _, _, _ = get_device_status(devices) + active = [d for d in devices if d["is_active"]] + online = sum(1 for d in active if status.get(d["mac"]) == "online") + down = len(active) - online + return f"{bold}{online}{reset} Clients online · {bold}{down}{reset} down" + except Exception: + return f"{dim}Geräte-Status nicht verfügbar{reset}" + + +def _dhcp_line(): + try: + dhcp = _detect_dhcp_status() + if not dhcp["installed"]: + return "nicht installiert" + if not dhcp["active"]: + return "installiert, gestoppt" + leases = _dhcp_read_leases() + return f"{len(leases)} Leases aktiv" if leases is not None else "läuft, keine Lease-Datei" + except Exception: + return "Status nicht verfügbar" + + +def main(): + # KEIN sys.stdout.isatty()-Check: dieses Skript schreibt beim Login + # nicht direkt auf ein Terminal, sondern über run-parts in + # /run/motd.dynamic (eine normale Datei -- isatty() wäre hier immer + # False). Der eigentliche Betrachter ist trotzdem ein echtes Terminal + # (die SSH-Session, der PAM den Inhalt danach ausgibt), daher immer + # Farbe ausgeben. + amber, dim, bold, reset = AMBER, DIM, BOLD, RESET + + print(f"{bold}{amber}TESM{reset} · TimEShepManager") + print(f"{dim}überwachen · booten · anbinden (DHCP) · Betriebssystem geben (PXE){reset}") + print() + print(f"{_fleet_line(amber, dim, bold, reset)} · DHCP: {_dhcp_line()}") + print() + + +if __name__ == "__main__": + try: + main() + except Exception: + # Ein MOTD-Banner darf einen SSH-Login niemals stören -- lieber + # gar keine Ausgabe als ein Traceback vor dem Shell-Prompt. + pass diff --git a/srv/tesm/static/css/style.css b/srv/tesm/static/css/style.css index 015ece6..0f07a04 100644 --- a/srv/tesm/static/css/style.css +++ b/srv/tesm/static/css/style.css @@ -999,28 +999,41 @@ select { .login-page { min-height: 100vh; + position: relative; + overflow: hidden; display: flex; align-items: center; justify-content: center; - gap: 5vw; + padding: 40px 20px; background: radial-gradient(circle at 15% 15%, rgba(255, 113, 0, 0.14), transparent 45%), radial-gradient(circle at 85% 85%, rgba(255, 113, 0, 0.08), transparent 40%), var(--bg); - padding: 20px; } -/* TESM-Wortmarke (mit Funktionszeile) als eigenes Panel neben der Login-Karte - -- nebeneinander statt überlagert, damit beides gleichzeitig sichtbar - bleibt. Auf schmalen Bildschirmen ausgeblendet (Platz reicht sonst nicht). */ -.login-branding { display: none; pointer-events: none; } -.login-branding img { width: min(34vw, 520px); height: auto; } +/* TESM-Wortmarke mit Funktionszeile als tatsächlicher Seitenhintergrund -- + absolut positioniert und hinter allem (z-index 0), kein eigenes Panel und + kein bloßes "Logo über der Box"-Stacking mehr. Die Login-Box (z-index 1) + liegt bewusst weit oben auf der Seite, dadurch sichtbar VOR dem oberen + Teil des Hintergrundmotivs statt klassisch vertikal zentriert. */ +.login-bg { + position: absolute; + top: 4vh; + left: 50%; + transform: translateX(-50%); + width: min(72vw, 880px); + pointer-events: none; + z-index: 0; +} +.login-bg img { width: 100%; height: auto; display: block; } -@media (min-width: 860px) { - .login-branding { display: block; } +@media (max-width: 640px) { + .login-bg { width: min(90vw, 460px); } } .login-card { + position: relative; + z-index: 1; width: 100%; max-width: 380px; background: var(--bg-card); @@ -1030,11 +1043,7 @@ select { padding: 34px 32px 28px; } -.login-logo { display: flex; justify-content: center; margin-bottom: 22px; } -.login-logo img { height: 34px; } - .login-card h1 { font-size: 18px; text-align: center; margin-bottom: 4px; } -.login-card .login-sub { text-align: center; color: var(--text-faint); font-size: 12.5px; margin-bottom: 24px; } /* ========================================================================== Logs page diff --git a/srv/tesm/templates/login.html b/srv/tesm/templates/login.html index 4e096e0..7907f1d 100644 --- a/srv/tesm/templates/login.html +++ b/srv/tesm/templates/login.html @@ -5,23 +5,15 @@
- Melde dich an, um fortzufahren
- {% with messages = get_flashed_messages() %} {% if messages %}