Ergänzt install.sh (das ein bestehendes Checkout voraussetzt) um einen Weg, eine bisher ad-hoc gepflegte Installation (z.B. per manuellem Datei-Upload während der Entwicklung) durch eine frische, aus einem Git-Tag geklonte Installation zu ersetzen: - Stoppt/deaktiviert tesm.service, tesm-check.service, tesm-check-restart.timer. - Verschiebt das bestehende /srv/tesm NICHT gelöscht, sondern nach /srv/tesm-backup-<Zeitstempel> -- bleibt als Rückfalloption und Datenquelle für einen späteren Export/Import vollständig erhalten. - Klont den angegebenen Release-Tag frisch und führt darin install.sh aus. - Weist sowohl vor dem Start als auch am Ende deutlich darauf hin, dass das Ergebnis eine FRISCHE Installation ohne die Daten der alten Instanz ist -- vorher exportieren (Einstellungen -> Im-/Export) oder danach alles neu einrichten. README um den entsprechenden Abschnitt ergänzt.
1191 lines
71 KiB
Markdown
1191 lines
71 KiB
Markdown
# TESM (TimEShepManager)
|
||
|
||
Webbasierte Verwaltung und Monitoring von PoE-Devices und Aruba-Switchen —
|
||
funktionales Rebuild von [Aruba-PoE](https://gitea.int.eertmoed.net/WiS/Aruba-PoE)
|
||
mit komplett neuem, modernem Frontend und einem granularen Rechtesystem.
|
||
|
||
**Das Projekt ist ausschließlich für Linux ausgelegt** (Zielsystem: eine
|
||
Linux-VM). Entwickelt wird unter WSL2 (Ubuntu) unter Windows — siehe
|
||
[Entwicklung unter WSL](#entwicklung-unter-wsl) weiter unten.
|
||
|
||
Die App ermöglicht:
|
||
|
||
- **Öffentliches Kachel-Dashboard ohne Login** — zeigt nur aktive (nicht
|
||
deaktivierte) Geräte, nur Online/Offline/Gesamt-Statistik; volle Verwaltung,
|
||
Aktivieren/Deaktivieren und PoE-Neustart bleiben hinter dem Login. Eingeloggt
|
||
sind zusätzlich deaktivierte Geräte sichtbar (eigene Kachel-Sektion). Alle
|
||
Kacheln sind durchsuchbar und in drei Abschnitten sortiert: Offline, Online,
|
||
Deaktiviert — jeweils alphabetisch. Die vier Stat-Kacheln oben (Online/
|
||
Offline/Deaktiviert/Gesamt) sind klickbare Filter — ein Klick blendet alle
|
||
anderen Abschnitte aus, „Gesamt“ oder ein erneuter Klick auf die aktive
|
||
Kachel hebt den Filter wieder auf. Jeder Abschnitt lässt sich unabhängig
|
||
davon per Klick auf seinen Titel ein-/ausklappen (z.B. um eine lange
|
||
Offline-Liste vorübergehend zu verstecken, ohne den Filter zu nutzen) —
|
||
beide Zustände bleiben über die automatischen Live-Updates hinweg erhalten.
|
||
Abmelden führt zurück auf dieses (auch ohne Login einsehbare) Dashboard
|
||
statt auf die Login-Seite
|
||
- Der Hauptinhaltsbereich nutzt die **volle verfügbare Breite** (kein
|
||
Bootstrap-typisches, festes `max-width` mehr) — nur Modals/Dialoge behalten
|
||
bewusst ihre kompakte, feste Breite
|
||
- Verwaltung von Devices und Switches
|
||
- **Zugangsdaten als eigene, wiederverwendbare Entität**: SSH-Logins werden
|
||
einmal angelegt und können mehreren Switchen zugeordnet werden; beim
|
||
Anlegen eines Switches lassen sich bestehende Zugangsdaten auswählen oder
|
||
direkt neue hinterlegen, ohne vorher zu „Zugangsdaten“ wechseln zu müssen
|
||
- **Browser-SSH-Terminal beim Anlegen/Bearbeiten eines Switches**, um den
|
||
Host-Key zu bestätigen und die Zugangsdaten direkt zu prüfen, bevor der
|
||
Switch gespeichert wird — funktioniert auf jedem Betriebssystem, die
|
||
SSH-Verbindung wird dabei immer vom PoE-Server aus aufgebaut
|
||
- Manueller PoE-Neustart einzelner Geräte per Klick — ausschließlich über
|
||
den zugeordneten Switch (`poe.sh`); der Button erscheint im Detail-Popup
|
||
nur, wenn dem Gerät tatsächlich Switch **und** Port zugeordnet sind (sonst
|
||
wäre kein PoE-Reset möglich), und ist immer der am weitesten links stehende
|
||
Aktions-Button — „Schließen“ steht unabhängig davon immer ganz rechts. Bei
|
||
deaktivierten Geräten bietet das Detail-Popup stattdessen direkt eine
|
||
Aktivieren-Option
|
||
- **SSH-basierte Wartung für Linux-Clients** (eigene Seite „Wartung“,
|
||
getrennt vom PoE-Neustart oben): Bulk-Update (`apt update && apt upgrade`)
|
||
und Neustart per SSH direkt auf dem Gerät, unabhängig von jeder
|
||
Switch-Zuordnung (siehe [Wartung](#wartung-ssh-bulk-update--neustart))
|
||
- **Optionale LDAP/Active-Directory-Anmeldung** mit dem Domänen-Passwort,
|
||
zusätzlich zu lokalen Konten — automatische Konto-Anlage beim ersten
|
||
Login (siehe [LDAP / Active Directory](#ldap--active-directory-same-sign-on))
|
||
- Einstellbares Prüfintervall (steuert den Hintergrund-Check-Dienst) —
|
||
der Countdown „Nächste Prüfung“ wird aus dem tatsächlichen letzten
|
||
Log-Eintrag berechnet, nicht bei jedem Seitenaufruf neu gestartet, und
|
||
läuft **global in der Topbar auf jeder Seite** (nicht nur im Dashboard)
|
||
- Live-Log-Ansicht mit farblicher Hervorhebung (online/offline/Neustart);
|
||
Dashboard und Live-Log aktualisieren sich automatisch im Hintergrund
|
||
(per AJAX, ohne vollen Seiten-Reload). Ein „Jetzt prüfen"-Icon neben dem
|
||
Timer (nur Admins) startet manuell einen sofortigen Prüf-Durchlauf —
|
||
der Countdown setzt sich dabei auf den manuell ausgelösten Zeitpunkt zurück
|
||
- **Einheitliches Seiten-Layout**: die Topbar zeigt rechts ausschließlich
|
||
den „Nächste Prüfung“-Timer; Anlegen-Buttons und eine kurze Beschreibung
|
||
sitzen stattdessen direkt über der jeweiligen Tabelle. Die Listen
|
||
(Geräte, Switche, Zugangsdaten, Benutzer, Gruppen) sind per Klick auf die
|
||
Spaltenüberschrift sortierbar
|
||
- **Rechtesystem mit Gruppen**: Admins dürfen immer alles (als Gruppe „Admin“
|
||
überall mit aufgeführt); normalen Benutzern lassen sich über Gruppen
|
||
einzelne Verwaltungsrechte gezielt zuweisen, inkl. reiner Ansichtsrechte
|
||
(siehe [Rechtesystem](#rechtesystem-gruppen--berechtigungen))
|
||
- Einklappbare Sidebar (Desktop: dauerhaft, über Neuladen hinweg gemerkt;
|
||
mobil: temporäres Überlagern)
|
||
- **Änderungslog**: jede Anlage/Bearbeitung/Löschung sowie Aktivieren/
|
||
Deaktivieren von Geräten, Switchen, Zugangsdaten, Benutzern und Gruppen
|
||
wird protokolliert (wer, wann, was) — einsehbar unter „Logs → Änderungslog“;
|
||
Geräte und Switche merken sich zusätzlich direkt am Datensatz, wer sie
|
||
zuletzt geändert hat. PoE-Neustarts werden bewusst **nicht** geloggt
|
||
(siehe [Änderungslog](#änderungslog))
|
||
- **Verschlüsselter Import/Export**: Geräte, Switche und Zugangsdaten lassen
|
||
sich unter „Einstellungen → Im-/Export" als ein mit selbstgewählter
|
||
Passphrase verschlüsseltes Bundle exportieren und auf einer neuen Umgebung
|
||
wieder importieren (siehe [Import/Export](#importexport))
|
||
- **Eigenes Konto**: das Zahnrad-Symbol neben dem eigenen Namen unten in der
|
||
Sidebar führt zu „Mein Konto“ — Vor-/Nachname, eigenes Passwort und
|
||
Profilbild pflegen (siehe [Eigenes Konto](#eigenes-konto--profilbild))
|
||
- **Anpassbare Navigation**: Admins legen dort zusätzlich die Reihenfolge der
|
||
Sidebar-Hauptpunkte per Auf-/Ab-Buttons fest; jeder Benutzer sieht davon
|
||
ohnehin nur, wofür er berechtigt ist — auch pro Unterpunkt innerhalb einer
|
||
Gruppe (siehe [Navigation anpassen](#navigation-anpassen))
|
||
|
||
## Was ist neu gegenüber dem Original?
|
||
|
||
- **Komplett neues Frontend**: eigenes Design-System (kein Bootstrap mehr —
|
||
der Hauptinhaltsbereich nutzt bewusst die volle verfügbare Breite statt
|
||
eines festen, Bootstrap-typischen `max-width`; nur Modals/Dialoge behalten
|
||
ihre kompakte, feste Breite), Logo mittig in der Topbar (auf jeder Seite,
|
||
skaliert auf die Balkenhöhe), Sidebar-Navigation (nur für eingeloggte
|
||
Nutzer sichtbar), Dark/Light-Umschalter, Karten-Dashboard mit Live-Zählern,
|
||
moderne Modals/Toasts statt Browser-`confirm()` und Bootstrap-Alerts. Der
|
||
Akzent-Ton (`--accent` in `style.css`) ist exakt das Orange aus dem
|
||
WiS-Logo (`#ff7100`, per Pixel-Sampling aus `logo.png` ermittelt), nicht
|
||
nur eine ähnliche, frei gewählte Nuance.
|
||
- **Dashboard jetzt teilweise öffentlich**: `/` ist ohne Login erreichbar und
|
||
zeigt eine reduzierte Lese-Ansicht (Hostname, IP, Status, letzte Prüfung)
|
||
mit Login-Button; alle Verwaltungsseiten und der PoE-Neustart bleiben
|
||
`@login_required` und zusätzlich rechtebasiert (s.u.).
|
||
- **Browser-SSH-Terminal für Switche**, implementiert mit **paramiko** (reines
|
||
Python) statt eines lokalen pty + System-`ssh`-Client — läuft dadurch
|
||
plattformunabhängig. Die SSH-Verbindung zum Switch wird immer serverseitig
|
||
(vom PoE-Server) aufgebaut, nie vom Browser/Client aus.
|
||
- **Granulares Rechtesystem mit Gruppen** (neu, siehe unten) statt der
|
||
ursprünglichen reinen Admin/User-Unterscheidung.
|
||
- **Gleiche Kernlogik**: Datenbankschema (erweitert um Gruppen/Rechte) und
|
||
Login sind ansonsten unverändert. Der PoE-Restart-Mechanismus selbst
|
||
(`poe.sh` per SSH/`expect` auf die Aruba-Switche) wurde dagegen
|
||
überarbeitet — parallele statt sequentielle Geräte-Prüfung und echte
|
||
Erfolgskontrolle statt blind angenommenem Erfolg, siehe
|
||
[Geräte-Prüfung](#geräte-prüfung-poesh).
|
||
- **Aufgeräumtes Backend**: konsistente, konfigurierbare Pfade (`TESM_DB_PATH`,
|
||
`POE_SCRIPT`, `TESM_LOG_DIR`, …) statt hart codierter/inkonsistenter Pfade;
|
||
Secret-Key und Fernet-Key werden beim ersten Start automatisch generiert
|
||
statt im Repo zu liegen.
|
||
|
||
> ⚠️ **Sicherheitshinweis zum Original-Repo:** Im Original sind `sqlite.db`
|
||
> (Nutzer-Passwort-Hashes, verschlüsselte Switch-Passwörter) und `fernet.key`
|
||
> (der zugehörige Schlüssel) mit im Git-Repo eingecheckt — die `.gitignore`
|
||
> dort schließt sie über die `!`-Regeln versehentlich wieder ein. In diesem
|
||
> Rebuild sind beide Dateien in `.gitignore` ausgeschlossen und werden beim
|
||
> ersten Start frisch erzeugt. Empfehlung fürs Original-Repo: beide Dateien
|
||
> aus der Git-Historie entfernen und Fernet-Key sowie alle Switch-/User-
|
||
> Passwörter rotieren.
|
||
|
||
## Rechtesystem (Gruppen & Berechtigungen)
|
||
|
||
Admins dürfen immer alles. Zusätzlich gibt es **Gruppen** mit einem
|
||
**vollständig granularen, an der Navbar gespiegelten Rechtebaum** — jeder
|
||
Bereich der App (nicht nur Devices/Switches) lässt sich einzeln freischalten.
|
||
Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich
|
||
(Vereinigung, nicht Schnittmenge).
|
||
|
||
### Zweistufiger Rechtebaum
|
||
|
||
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei
|
||
Rechte-Bereichen der Gruppen-Seite (Geräte/Logs/Einstellungen) — das ist
|
||
**unabhängig** von der Sidebar-Gliederung: „Benutzer“ und „Gruppen“ stehen
|
||
in der Sidebar zwar als eigene, flache Top-Level-Punkte (siehe
|
||
[Navigation anpassen](#navigation-anpassen)), gehören rechtlich aber
|
||
weiterhin zum Bereich „Einstellungen“. Auf der Gruppen-Seite stehen die drei
|
||
Bereiche als drei kompakte Spalten nebeneinander, mit einer kurzen Legende
|
||
direkt darüber. Jede Spalte ist eine kleine Tabelle: die Kopfzeile enthält
|
||
in EINER Zeile sowohl den Bereichsnamen mit seinem Kästchen (dem
|
||
Kill-Switch, dicht nebeneinander statt an den Rand gestellt) als auch die
|
||
Buchstaben-Spalten **R**(ead/Lesen), **W**(rite/Schreiben, Anlegen),
|
||
**E**(dit/Ändern) — nur die Rechte, die dieser Bereich tatsächlich nutzt.
|
||
Darunter je eine eingerückte Zeile pro Unterpunkt. **„Ändern“ deckt Löschen
|
||
überall mit ab** — es gibt bewusst kein eigenes Löschen-Recht (stattdessen
|
||
landet ein gelöschter Eintrag im [Papierkorb](#papierkorb)). Zwei Bereiche
|
||
haben eine zusätzliche, bereichsspezifische vierte Spalte: **D** nur bei
|
||
Geräte → Clients bedeutet **nicht** Löschen, sondern *PoE-Neustart über das
|
||
Dashboard-Popup auslösen* (automatische Neustarts bei Ausfall über `poe.sh`
|
||
sind davon komplett unabhängig und laufen immer); **X** nur bei
|
||
Einstellungen → Im-/Export bedeutet *Export ausführen* — bewusst getrennt
|
||
von E (Import), da die Export-Datei Passwörter im Klartext enthält (siehe
|
||
[Import/Export](#importexport)). Checken einer W/E/D/X-Zelle einer Zeile
|
||
hakt automatisch auch deren R an, und Abwählen von R wählt automatisch auch
|
||
die übrigen Spalten derselben Zeile ab (R ist logisch Voraussetzung für
|
||
alles andere). Für Zeilen ohne passendes Recht (z.B. „D“ bei Switche/
|
||
Zugangsdaten/Benutzer/Gruppen, oder „X“ bei allem außer Im-/Export) wird
|
||
eine ausgegraute, nicht anklickbare Checkbox angezeigt statt die Zelle
|
||
wegzulassen:
|
||
|
||
```
|
||
☑ Geräte R W E D ☑ Logs R W E ☑ Einstellungen R W E X
|
||
Clients ☑ ☑ ☑ ☑ Live ☑ ☑ ☑ Benutzer ☑ ☑ ☑ ▢
|
||
Switche ☑ ☑ ☑ ▢ Änderungen☑ ☑ ☑ Gruppen ☑ ☑ ☑ ▢
|
||
Zugangsd. ☑ ☑ ☑ ▢ Kea-DHCP ☑ ☑ ☑ Systemeinst. ☑ ▢ ☑ ▢
|
||
Wartung ☑ ▢ ☑ ▢ LDAP ☑ ▢ ☑ ▢
|
||
DHCP ☑ ▢ ☑ ▢
|
||
Im-/Export ☑ ▢ ☑ ☑
|
||
```
|
||
|
||
Das jeweilige „Bereich sichtbar“-Recht (`devices_group.view` /
|
||
`logs_group.view` / `settings_group.view`, das Kästchen in der Kopfzeile
|
||
einer Spalte) wirkt als **Kill-Switch**: ist es für eine Gruppe nicht
|
||
gesetzt, greift kein einziges Recht darunter mehr — selbst wenn z.B.
|
||
`devices.view` einzeln noch angehakt ist. So lässt sich ein ganzer Bereich
|
||
mit einem Klick sperren, ohne jedes Unterrecht einzeln zurücknehmen zu
|
||
müssen (`User.has_permission()` in `app.py`). Auf der Gruppen-Seite sind
|
||
die Unterpunkt-Zeilen einer Spalte per JavaScript so lange gesperrt UND
|
||
sichtbar ausgegraut (nicht nur das dezente native disabled-Styling), bis
|
||
das Kopfzeilen-Kästchen angehakt ist.
|
||
|
||
### Systemgruppen
|
||
|
||
Zwei Gruppen sind fest und **weder umbenennbar noch in ihren Rechten
|
||
änderbar** (auch nicht durch Admins über die UI) — Mitgliedschaft bleibt bei
|
||
beiden frei verwaltbar:
|
||
- **Admin** — virtuell (kein echter `groups`-Datensatz), intern über den
|
||
`is_admin`-Schalter der Benutzer gesteuert, immer alle Rechte. Mindestens
|
||
ein Admin muss bestehen bleiben (serverseitig erzwungen).
|
||
- **Benutzer** — die Standardgruppe (`is_system`-Flag), der jeder neu
|
||
angelegte Nicht-Admin automatisch zugeordnet wird. Fester Rechtesatz:
|
||
Geräte-Bereich + Clients/Switche lesen, Logs-Bereich + Live-Log lesen.
|
||
|
||
Alle anderen Gruppen sind vom jeweiligen Rechteinhaber (`groups.edit`, das
|
||
auch Anlegen und Löschen von Gruppen umfasst) frei konfigurierbar.
|
||
|
||
### Eskalationsschutz
|
||
|
||
Da jetzt auch **Benutzer-** und **Gruppenverwaltung** delegierbar sind (z.B.
|
||
eine Gruppe mit nur `users.edit`, ohne Admin zu sein), gelten zusätzlich zu
|
||
den granularen Rechten diese fest einprogrammierten Schranken, unabhängig
|
||
davon, was eine Gruppe an Rechten hat:
|
||
- Ein Benutzer als Admin anlegen/dazu machen (`group_id=admin` bei
|
||
Anlegen/Zuweisen) bleibt echten Admins vorbehalten.
|
||
- Ein bestehendes Admin-Konto bearbeiten, löschen oder ihm die Gruppe ändern
|
||
bleibt echten Admins vorbehalten.
|
||
- Wer Admin ist, wird ausschließlich über „Admin-Mitglieder verwalten“ auf
|
||
der Gruppen-Seite gesteuert — bleibt admin-exklusiv, unabhängig von
|
||
`groups.edit`.
|
||
|
||
Sowohl das Anzeigen jeder Seite als auch jede einzelne Aktion (Buttons,
|
||
Formulare) ist an das jeweilige Recht gekoppelt — im Frontend ausgeblendet
|
||
**und** im Backend serverseitig durchgesetzt, unabhängig vom Frontend.
|
||
|
||
Datenmodell: `groups` (inkl. `is_default`- und `is_system`-Flag),
|
||
`group_permissions` (Gruppe → Recht), `user_groups` (Benutzer → Gruppe).
|
||
Bestehende Datenbanken werden beim App-Start automatisch migriert
|
||
(`_ensure_schema()` in `app.py`): `devices.toggle` → `devices.edit`,
|
||
`switches.*` → gespiegelte `credentials.*`, nachträgliches Setzen der neuen
|
||
Bereichs-Rechte für bereits vergebene Unterrechte, sowie (spätere Revision)
|
||
sämtliche `*.delete`-Rechte → `*.edit` (Löschen wurde nachträglich überall in
|
||
Ändern gefaltet). Jede dieser Migrationen läuft **nur einmalig** über einen
|
||
eigenen Guard in der `settings`-Tabelle, damit z.B. ein bewusst deaktiviertes
|
||
Bereichs-Recht nicht bei jedem Neustart automatisch wieder gesetzt wird.
|
||
|
||
## LDAP / Active Directory (Same Sign-On)
|
||
|
||
Optionale Anmeldung mit dem Domänen-Passwort, zusätzlich zu lokalen Konten
|
||
— auf der eigenen Seite **Einstellungen → LDAP** (Recht `settings_ldap.view`
|
||
zum Ansehen, Speichern erfordert Admin).
|
||
|
||
**Wichtige Begriffsklärung, direkt aus einer Nutzerfrage entstanden:** das
|
||
ist **"Same Sign-On"**, kein automatisches, promptloses **Single Sign-On**.
|
||
Der Benutzer tippt sein AD-Passwort weiterhin auf der normalen Login-Seite
|
||
ein — es wird nur bei jedem Login **live** gegen Active Directory geprüft
|
||
statt gegen ein lokal gespeichertes Passwort. Eine Passwortänderung in AD
|
||
greift dadurch sofort, ohne dass irgendetwas synchronisiert werden müsste.
|
||
Echtes, promptloses Single Sign-On (der Browser meldet automatisch den
|
||
bereits an Windows angemeldeten Benutzer an) bräuchte zusätzlich:
|
||
- **Kerberos/SPNEGO** ("Integrated Windows Authentication"): ein SPN +
|
||
Keytab vom AD-Admin, eine per DNS auflösbare Hostname für die App, und
|
||
die Browser-Konfiguration der Clients (i.d.R. bereits Standard für die
|
||
Intranet-Zone auf domänenbeigetretenen PCs) — **oder**
|
||
- **SAML/OIDC über Entra ID (Azure AD)**, falls Microsoft 365/Entra
|
||
vorhanden ist — oft der pragmatischere Weg zu echtem SSO als klassisches
|
||
Kerberos.
|
||
|
||
Beides sind eigenständige, deutlich aufwändigere Ausbaustufen, die sich
|
||
später auf die hier beschriebene LDAP-Anmeldung aufsetzen ließen, aber
|
||
AD-seitige Vorarbeit (Keytab bzw. App-Registrierung) durch einen
|
||
Domain-Admin voraussetzen.
|
||
|
||
### Funktionsweise (Search+Bind)
|
||
|
||
Beim Login wird zunächst lokal gesucht — per exaktem Benutzernamen, per
|
||
E-Mail-Adresse, oder (case-insensitiv, nur für `auth_source='ldap'`-Konten)
|
||
per bereits bekanntem AD-Benutzernamen. Ist das gefundene (oder noch nicht
|
||
existierende) Konto gesperrt (`is_locked`, siehe unten) oder im Papierkorb,
|
||
wird der Login abgelehnt, ohne AD überhaupt zu kontaktieren. Existiert kein
|
||
lokales Konto und ist LDAP aktiviert, prüft die App die eingegebene Kennung
|
||
(sAMAccountName **oder** UPN, z.B. sowohl `jdoe` als auch
|
||
`jdoe@ad.eertmoed.net` funktionieren) und das Passwort per **Search+Bind**
|
||
gegen den konfigurierten Server:
|
||
|
||
1. Verbindung mit dem konfigurierten **Service-Konto** aufbauen (ein
|
||
normales, unprivilegiertes Domänenkonto reicht — es braucht nur
|
||
Lesezugriff auf das Verzeichnis, was jedes Domänenkonto in AD
|
||
standardmäßig hat).
|
||
2. Den Benutzer per konfigurierbarem Attribut suchen (Standard
|
||
`sAMAccountName` für Active Directory; für generisches LDAP z.B. `uid`).
|
||
3. Eine **zweite, separate** Verbindung mit der gefundenen DN des Benutzers
|
||
und dem eingegebenen Passwort aufbauen, um es zu verifizieren — robuster
|
||
als ein direktes UPN-Bind (`user@domain`), da es nicht voraussetzt, dass
|
||
`sAMAccountName` und der UPN-Präfix übereinstimmen.
|
||
4. Zusätzlich wird das `userAccountControl`-Bit `ACCOUNTDISABLE` geprüft
|
||
(auch wenn ein echter AD-DC den Bind eines deaktivierten Kontos i.d.R.
|
||
ohnehin schon selbst ablehnt) — zusätzliche Absicherung.
|
||
|
||
Die **Base-DN** wird standardmäßig automatisch per RootDSE
|
||
(`defaultNamingContext`) ermittelt, kann aber manuell überschrieben werden
|
||
(z.B. bei mehreren Domänen in einer Gesamtstruktur).
|
||
|
||
Bei erfolgreichem erstem Login wird automatisch ein lokales Konto angelegt
|
||
(`auth_source='ldap'`, kein Admin). Das gespeicherte Passwort dieses Kontos
|
||
ist nur ein **nie geprüfter Platzhalter-Hash** — bei jedem weiteren Login
|
||
wird live gegen AD verifiziert, nie gegen die lokale DB. Benutzernamen
|
||
werden dabei case-insensitiv abgeglichen (nur für `auth_source='ldap'`-
|
||
Konten, wirkt sich nie auf lokale Konten aus), damit z.B. "JDoe" und "jdoe"
|
||
nicht zwei verschiedene lokale Konten erzeugen.
|
||
|
||
**Gruppen-Zuordnung (AD-Gruppe → App-Gruppe).** Auf der LDAP-Einstellungsseite
|
||
lässt sich jeder AD-Gruppe (per DN) genau eine App-Gruppe zuordnen. Ist der
|
||
Benutzer beim Login Mitglied mehrerer zugeordneter AD-Gruppen, erhält er
|
||
**additiv** die Rechte aller passenden App-Gruppen (dieselbe
|
||
Vereinigungslogik wie bei mehrfacher lokaler Gruppenmitgliedschaft). Passt
|
||
keine der konfigurierten Zuordnungen, greift ersatzweise eine konfigurierbare
|
||
**Standardgruppe** (sonst die System-Standardgruppe „Benutzer“) — es sei denn,
|
||
eine der AD-Gruppen ist explizit der internen Admin-Zuordnung zugewiesen,
|
||
dann wird das Konto automatisch zum Admin. Die Zuordnung wird bei **jedem**
|
||
Login neu ausgewertet, nicht nur beim ersten — Änderungen an
|
||
AD-Gruppenmitgliedschaften wirken sich also ohne manuellen Eingriff auf die
|
||
nächste Anmeldung aus.
|
||
|
||
**Konten sperren.** Ein Admin kann jedes Konto (lokal wie AD) in „Benutzer“
|
||
per Schalter sperren (`is_locked`) — ein gesperrtes Konto kann sich nicht
|
||
mehr anmelden, unabhängig davon, ob das AD-Passwort weiterhin gültig ist.
|
||
Nützlich, um jemandem ohne Eingriff in AD selbst kurzfristig den Zugriff auf
|
||
die App zu entziehen.
|
||
|
||
**Vorab-Anlage aus Active Directory** („Benutzer" → „Aus Active Directory
|
||
hinzufügen"): eine Live-Suche (Substring, ab 2 Zeichen) im Verzeichnis,
|
||
gefiltert um bereits lokal bekannte Benutzernamen. So kann ein Admin einer
|
||
Person schon **vor** ihrem ersten Login eine Gruppe zuweisen, statt sich auf
|
||
die Standardgruppe zu verlassen — das Konto landet dabei sofort mit
|
||
`auth_source='ldap'` und demselben Platzhalter-Passwort-Mechanismus in der
|
||
DB, ganz so, als hätte die Person sich bereits einmal angemeldet.
|
||
|
||
**Lokale Konten bleiben vollständig erhalten** — der lokale Login-Pfad ist
|
||
unverändert, LDAP kommt nur als zusätzlicher Weg für Benutzernamen ohne
|
||
lokales Konto dazu. Wichtig als Fallback (Break-Glass-Zugang, falls
|
||
AD/LDAP mal nicht erreichbar ist) und für den allerersten
|
||
Admin-Bootstrap (`create_admin.py`).
|
||
|
||
Für AD-Konten wird die Passwort-Änderung sowohl unter „Mein Konto" als auch
|
||
im Admin-Bereich „Benutzer" ausgeblendet bzw. serverseitig ignoriert (ein
|
||
kleines "AD"-Symbol markiert sie in der Benutzerliste) — das Passwort wird
|
||
ausschließlich in Active Directory verwaltet.
|
||
|
||
**Löschen eines AD-Kontos** in „Benutzer" entfernt nur das lokale
|
||
Schatten-Konto — **AD-Konten landen dabei bewusst NIE im Papierkorb**
|
||
(anders als lokale Konten, siehe [Papierkorb](#papierkorb)), da sie sich bei
|
||
jedem gültigen Login ohnehin automatisch wieder anlegen würden und ein
|
||
"gelöscht, aber weiter nutzbar"-Zwischenzustand hier keinen Sinn ergibt.
|
||
Meldet sich die Person danach erneut mit gültigen AD-Zugangsdaten an, wird
|
||
automatisch wieder ein (neues, rechteloses) Konto angelegt. Um jemandem den
|
||
Zugriff wirklich zu entziehen, sperrt man das Konto entweder direkt in der
|
||
App (siehe oben) oder deaktiviert es in AD selbst (blockiert dann auch den
|
||
LDAP-Bind). Aus demselben Grund werden AD-Konten beim
|
||
[Export](#importexport) grundsätzlich nie mit exportiert — nur lokale
|
||
Konten landen in der Export-Datei.
|
||
|
||
**Sicherheitshinweis**: ohne LDAPS/StartTLS wird das Passwort unverschlüsselt
|
||
über das Netzwerk übertragen — nur für interne Tests in einem vertrauenswürdigen
|
||
Netz geeignet. Vor Produktivbetrieb sollte LDAPS auf dem Domain Controller
|
||
eingerichtet und in den Einstellungen aktiviert werden (das Server-Zertifikat
|
||
wird dafür noch nicht validiert — reine Transportverschlüsselung).
|
||
|
||
Live gegen einen echten Active-Directory-Domain-Controller verifiziert:
|
||
automatische Base-DN-Ermittlung, "Verbindung testen", erfolgreicher
|
||
Erst-Login mit automatischer Konto-Anlage und korrekter
|
||
Standardgruppen-Zuweisung, Ablehnung bei falschem Passwort, erneuter Login
|
||
verwendet das bereits angelegte Konto (keine Duplikate), unveränderter
|
||
lokaler Admin-Login, sowie dass das Bind-Passwort nie im Klartext im
|
||
gerenderten HTML landet.
|
||
|
||
## Zugangsdaten (wiederverwendbare SSH-Logins)
|
||
|
||
Zugangsdaten (Username + verschlüsseltes Passwort) sind eine eigene Entität
|
||
(`credentials`-Tabelle) statt direkt am Switch/Gerät zu hängen — mehrere
|
||
Switche/Geräte mit demselben Login teilen sich einen Eintrag. Beim Anlegen/
|
||
Bearbeiten eines Switches **oder Geräts** lässt sich per Dropdown ein
|
||
bestehender Eintrag auswählen oder über „+ Neue Zugangsdaten anlegen“ direkt
|
||
im selben Formular ein neuer anlegen, ohne vorher zur eigenen
|
||
**Zugangsdaten**-Seite wechseln zu müssen. Löschen ist nur möglich, wenn
|
||
weder ein Switch noch ein Gerät mehr auf den Eintrag verweist. Bestehende
|
||
Datenbanken aus einer älteren Version (Username/Passwort direkt am Switch)
|
||
werden beim ersten Start automatisch migriert.
|
||
|
||
Jede Zugangsdaten-Zeile hat außerdem eine explizite **Kategorie** (Switch /
|
||
Linux-Client / Windows-Client / Router-Sonstiges), vom Admin beim Anlegen
|
||
gesetzt — bewusst **nicht** automatisch per Ping/TTL-Heuristik erkannt (zu
|
||
unzuverlässig, siehe Diskussion in der Commit-Historie). Aktuell wertet nur
|
||
die Kategorie **Linux-Client** etwas aus: nur Geräte mit einer so
|
||
kategorisierten Zugangsdaten-Zuordnung erscheinen unter **Wartung** (siehe
|
||
unten). Windows/PowerShell-Wartung ist als eigener, separater Schritt
|
||
vorgesehen (kein Testsystem dafür verfügbar) — die Kategorie existiert
|
||
bereits, die Aktion dahinter noch nicht.
|
||
|
||
Sowohl die Switche- als auch die Clients-Tabelle zeigen dieselbe
|
||
„Zugangsdaten“-Spalte (Name + Username, oder „— keine —“), damit auf einen
|
||
Blick erkennbar ist, welches Gerät bereits per SSH verwaltet werden kann,
|
||
ohne extra zur Zugangsdaten-Seite wechseln zu müssen.
|
||
|
||
Jeder Switch kann außerdem einen individuellen **SSH-Port** hinterlegen
|
||
(Feld „SSH-Port“ beim Anlegen/Bearbeiten) — bleibt er leer, wird überall
|
||
automatisch **Port 22** angenommen (`SWITCH_DEFAULT_SSH_PORT` in `app.py`).
|
||
Der Port wird konsistent an allen drei Stellen verwendet, an denen die App
|
||
sich per SSH mit einem Switch verbindet: dem Web-Terminal (Verbindungstest),
|
||
`generate_ips.py` (liefert ihn als eigenes Feld an `poe.sh`) und den
|
||
`expect`-Aufrufen in `poe.sh` selbst (`ssh -p <port> ...`) für automatische
|
||
wie manuelle PoE-Neustarts.
|
||
|
||
## Wartung (SSH-Bulk-Update & -Neustart)
|
||
|
||
Eigene Seite unter **Geräte → Wartung** (eigenes Rechtepaar
|
||
`maintenance.view`/`maintenance.edit`), komplett getrennt vom PoE-Neustart
|
||
auf dem Dashboard:
|
||
|
||
- **Dashboard-Neustart** = immer PoE-Reset über den Switch (`poe.sh`),
|
||
funktioniert unabhängig davon, ob das Gerät SSH-Zugangsdaten hat.
|
||
- **Wartung** = immer eine direkte SSH-Verbindung zum Gerät selbst,
|
||
funktioniert unabhängig davon, ob/an welchem Switch es hängt.
|
||
|
||
Nur Geräte mit hinterlegten SSH-Zugangsdaten der Kategorie **Linux-Client**
|
||
(siehe oben) erscheinen in der Liste. Zwei Aktionen:
|
||
|
||
- **Update** (Mehrfachauswahl per Checkbox, ein Klick startet alle
|
||
ausgewählten parallel als eigene Hintergrund-Threads): führt serverseitig
|
||
`env DEBIAN_FRONTEND=noninteractive apt-get update && env
|
||
DEBIAN_FRONTEND=noninteractive apt-get -y -o Dpkg::Options::="--force-confdef"
|
||
-o Dpkg::Options::="--force-confold" upgrade` per SSH aus — nicht
|
||
interaktiv (kein Terminal, keine Rückfragemöglichkeit), die
|
||
`Dpkg::Options`-Flags verhindern, dass ein Paket-Postinst-Skript auf eine
|
||
Config-Datei-Rückfrage wartet, die nie kommt und sonst bis zum Timeout
|
||
hängen bliebe.
|
||
- **Neustart** (pro Gerät): `sudo reboot` per SSH, danach automatisch
|
||
weiterverfolgt bis das Gerät wieder online ist — ohne das bliebe der
|
||
Status für immer auf „Neustart ausgelöst“ stehen, obwohl der eigentlich
|
||
interessante Zeitpunkt (kommt das Gerät zurück?) noch gar nicht erfasst
|
||
wäre. Ablauf: das SSH-Neustart-Kommando selbst gilt als erfolgreich
|
||
ausgelöst, sobald die Verbindung abbricht (erwartet — die Maschine fährt
|
||
herunter, bevor sie noch antworten kann; nur ein Verbindungs-/Auth-Fehler
|
||
**vor** dem Kommando ist ein echter Fehler). Danach wartet eine kurze
|
||
Schonfrist (15s, damit ein zu früher Ping nicht noch die absterbende alte
|
||
Instanz erreicht) und pingt anschließend alle 5s (`ping -c 1 -W 2` — exakt
|
||
dieselbe Invocation wie das bestehende Online/Offline-Monitoring in
|
||
`poe.sh`), bis eine Antwort kommt oder ein Timeout von 5 Minuten erreicht
|
||
ist; erst dann wechselt der Status auf „wieder online“ (Erfolg) bzw. auf
|
||
einen Fehler, falls das Gerät nicht rechtzeitig zurückkommt.
|
||
|
||
Live-Status pro Gerät (läuft/erfolgreich/fehlgeschlagen inkl. Ausgabe der
|
||
letzten Aktion, per Klick ein-/ausblendbar) wird per Polling (`/maintenance/status`,
|
||
alle 2s solange etwas läuft, sonst alle 8s) aktualisiert, ohne die Seite neu
|
||
zu laden. Der Job-Status lebt bewusst nur im Prozessspeicher (wie beim
|
||
SSH-Terminal auch keine Sitzung persistiert wird) — ein Neustart von
|
||
`tesm.service` verwirft nur die Anzeige, nicht die auf dem Zielgerät
|
||
bereits laufende Aktion selbst. Ein abgeschlossener **erfolgreicher** Job
|
||
wird zusätzlich bei jedem (erneuten) Laden der Seite zurückgesetzt (Status
|
||
wieder „Noch keine Aktion“) — ein Erfolg ist nur so lange relevant, wie man
|
||
ihn noch live mitverfolgt, ein späterer Seitenaufruf soll keine ggf.
|
||
stunden-/tagealte Erfolgsmeldung dauerhaft zeigen. Fehlgeschlagene Aktionen
|
||
bleiben dagegen bewusst sichtbar, bis eine neue Aktion sie überschreibt, um
|
||
nicht durch einen einfachen Reload versehentlich übersehen zu werden.
|
||
|
||
**Sicherheitsmodell**: dieselbe `known_hosts`-Datei wie beim
|
||
Browser-SSH-Terminal (`SSH_KNOWN_HOSTS_PATH`) — ein Host muss vorher
|
||
mindestens einmal interaktiv über „Verbindung testen“ bestätigt worden sein.
|
||
Anders als das interaktive Terminal (dort kann bei einem unbekannten
|
||
Host-Key wie bei einem echten `ssh`-Client live nachgefragt werden) läuft
|
||
hier nichts vor einem Menschen ab — ein unbekannter oder nicht vorher
|
||
bestätigter Host-Key wird deshalb per `paramiko.RejectPolicy` hart
|
||
abgelehnt statt automatisch akzeptiert, da ohne Rückfragemöglichkeit ein
|
||
automatisches Akzeptieren ein MITM-Risiko ohne jede Prüfung wäre.
|
||
|
||
Live gegen ein dediziertes Testsystem verifiziert (separat vom gemeinsam
|
||
genutzten App-Host, um dort kein echtes `apt upgrade` auszulösen): ein
|
||
echtes `apt update && apt upgrade` (inkl. Kernel-/systemd-/netplan-Paketen)
|
||
lief nicht-interaktiv vollständig durch (`status: success`, komplette
|
||
`apt`-Ausgabe im Job-Verlauf sichtbar); der SSH-Neustart samt
|
||
Online-Watchdog wurde zweimal gegen dasselbe Testsystem verifiziert —
|
||
jeweils per `uptime -s` bestätigt, dass der Boot-Zeitpunkt exakt zum
|
||
Auslöse-Zeitpunkt des jeweiligen Jobs passte (kein False-Positive-Ping
|
||
gegen die noch nicht heruntergefahrene alte Instanz), und dass der
|
||
Job-Status korrekt von „wartet auf Online“ auf „wieder online“
|
||
umgeschaltet hat. Das Zurücksetzen auf „Noch keine Aktion“ bei einem
|
||
erneuten Seitenaufruf wurde ebenfalls live bestätigt (Status vor einem
|
||
`GET /maintenance` noch sichtbar, danach wieder leer).
|
||
|
||
Windows-Clients/PowerShell-Wartung ist bewusst **noch nicht** umgesetzt
|
||
(kein Testsystem verfügbar) — die Kategorie „Windows-Client“ existiert in
|
||
den Zugangsdaten bereits als Vorbereitung für einen späteren, separaten
|
||
Ausbauschritt.
|
||
|
||
## DHCP-Reservierungen & eigene Options (Systemeinstellungen → DHCP)
|
||
|
||
Erzeugt aus den vorhandenen Client-Stammdaten (MAC + IP + Name) automatisch
|
||
feste DHCP-Reservierungen inkl. eigener/herstellerspezifischer Options, ohne
|
||
die Daten ein zweites Mal pflegen zu müssen. Eigenes Rechtepaar
|
||
`settings_dhcp.view`/`settings_dhcp.edit`, gespiegelt in der Navbar unter
|
||
Einstellungen.
|
||
|
||
**Backend: Kea DHCP** (`kea-dhcp4-server`), nicht das ältere
|
||
`isc-dhcp-server` — ISC hat isc-dhcp-server 2022 als EOL erklärt, Kea ist
|
||
der aktiv weiterentwickelte Nachfolger und bildet „globaler Wert, pro Client
|
||
überschreibbar“ nativ ab (siehe unten).
|
||
|
||
- **Status-Erkennung** (rein lesend): `shutil.which("kea-dhcp4")` +
|
||
`systemctl is-active`/`is-enabled`. Zeigt Version, Lauf- und
|
||
Autostart-Status.
|
||
- **Installation & Dienststeuerung**: eigene, jeweils einzeln bestätigte
|
||
Buttons — „kea-dhcp4-server installieren“ (`apt-get install -y`),
|
||
„Aktivieren & (neu) starten“ (`systemctl enable` + `restart`) und
|
||
„Stoppen“ (`systemctl stop` **+ `disable`**, damit ein bewusst gestoppter
|
||
Dienst nach einem Host-Neustart nicht trotzdem automatisch wieder
|
||
hochfährt — ein Host-Neustart übernimmt so genau den zuletzt bewusst
|
||
gewählten Zustand, an oder aus). Bewusst **nicht** automatisch an
|
||
„Konfiguration speichern“ oder „In Datei schreiben“ gekoppelt — ein
|
||
unerwartet aktiver oder fehlkonfigurierter DHCP-Server kann ein ganzes
|
||
Netzsegment stören, jede dieser Aktionen soll ein bewusster, separater
|
||
Klick bleiben.
|
||
- **Mehrere Subnetze gleichzeitig**: ein Host kann mehrere IPs/Interfaces
|
||
mit jeweils eigenem Netz haben, für die alle DHCP angeboten werden soll
|
||
(`dhcp_subnets`-Tabelle statt einer einzelnen globalen Range) — jedes
|
||
Subnetz hat eigene Interface/Range sowie optional eigenes Gateway/DNS,
|
||
generiert wird ein eigener `subnet4`-Block je Subnetz in der Kea-Config.
|
||
Domain, Lease-Zeiten und Ausgabepfad bleiben global (Kea-weit gültig).
|
||
- **Netzwerk-Kopplung**: Subnet und Netzmaske werden **nie manuell
|
||
eingegeben**, sondern bei jeder Anzeige/Generierung live aus der
|
||
System-Netzwerkkonfiguration des jeweiligen Interfaces gelesen
|
||
(`_detect_interface_network()`/`_detect_interface_networks()`:
|
||
`ip -4 addr show dev <iface>` + `ip route show default`) — berücksichtigt
|
||
dabei ALLE IPv4-Adressen eines Interfaces, nicht nur die erste (ein
|
||
Interface kann mehrere IPs/Subnetze gleichzeitig tragen, z.B. Alias-IPs).
|
||
Die generierte Kea-Konfiguration ist dadurch immer an das Netz gekoppelt,
|
||
in dem der Host tatsächlich hängt, auch wenn sich dessen IP/Subnet mal
|
||
ändert.
|
||
- **Range ist Pflicht und muss zu einem echten Netz passen**: kein
|
||
vorausgefüllter Default für Range/DNS (nur Platzhaltertext) — ein
|
||
Subnetz ohne zu einem tatsächlich vorhandenen Netz passende Range wird
|
||
beim Anlegen/Bearbeiten abgelehnt (`_dhcp_matching_network()`), da Kea
|
||
ohne eine echte, physisch vorhandene IP im gewünschten Bereich keine
|
||
gültige Subnet-CIDR zugeordnet werden kann. Dieselbe Prüfung läuft
|
||
zusätzlich bei „In Datei schreiben“ (Subnetze, die inzwischen zu keinem
|
||
echten Netz mehr passen — z.B. nach einem Netzwechsel des Hosts — werden
|
||
übersprungen und in der Erfolgsmeldung benannt, statt die ganze Aktion
|
||
abzubrechen) und bei „Aktivieren & (neu) starten“ (verweigert den Start,
|
||
falls kein einziges gültiges Subnetz mehr existiert). Live auf einer
|
||
echten Fehlkonfiguration reproduziert und verifiziert.
|
||
- **Subnetze einzeln deaktivierbar**: ein Subnetz lässt sich ohne Löschen
|
||
pausieren (`enabled`-Flag) — wird dann bei „In Datei schreiben“ und
|
||
„Aktivieren & (neu) starten“ komplett ignoriert (kein Fehler, bewusst
|
||
kein "ungültig", da das Fehlen des Netzes hier Absicht ist), statt es
|
||
jedes Mal neu anlegen zu müssen.
|
||
- **DHCP-Options** (`dhcp_option_defs`/`dhcp_option_values`): eine kuratierte
|
||
Auswahl an **56 Standard-Optionen** (analog den „Vordefinierten Optionen“
|
||
eines Windows-DHCP-Servers — u.a. NTP-/WINS-/TFTP-/SMTP-Server, Boot-Datei,
|
||
Domain-Search, IP-Forwarding) ist automatisch vorbefüllt und **nicht
|
||
löschbar** (Backend lehnt es ab, UI zeigt statt eines Löschen-Buttons ein
|
||
Schloss-Symbol) — Router/DNS-Server/Domain-Name/Lease-Zeiten sind bewusst
|
||
ausgenommen, da dafür bereits eigene, dedizierte Felder existieren
|
||
(Subnetze bzw. globale Einstellungen oben); ebenso client-/
|
||
protokollinterne Optionen (message-type, parameter-request-list,
|
||
requested-address, ...), die serverseitig nicht sinnvoll setzbar sind.
|
||
„host-name“ (12) ist ebenfalls bewusst **nicht** enthalten (war es
|
||
zwischenzeitlich, wurde aber wieder entfernt): Kea beantwortet die
|
||
Host-Name-Anfrage eines Clients bei einer Reservierung bereits automatisch
|
||
über deren eigenes `hostname`-Feld (aus dem Namen der Reservierung
|
||
abgeleitet) — ein zusätzlicher `option-data`-Override für dieselbe Option
|
||
wäre laut Kea-Dokumentation eine Dopplung und Quelle für Widersprüche,
|
||
live an einer echten Reservierung mit beiden gleichzeitig gesetzten
|
||
Werten nachvollzogen. Alle verbleibenden Namen sind live gegen eine echte
|
||
Kea-2.4.1-Instanz verifiziert (`kea-dhcp4 -t`, jede Option einzeln
|
||
getestet). Standard-Optionen bekommen bewusst **kein eigenes
|
||
`option-def`** in der generierten Konfiguration — Kea kennt sie bereits
|
||
nativ, eine Neudefinition würde die eingebaute duplizieren; nur
|
||
zusätzlich angelegte, wirklich eigene/herstellerspezifische Options (z.B.
|
||
eine Terminal-Boot-URL) bekommen eines und bleiben frei löschbar.
|
||
In der Tabelle sind grundsätzlich nur **tatsächlich genutzte** Options
|
||
sichtbar (eigene immer, Standard-Optionen nur mit gesetztem globalen Wert)
|
||
— weitere Standard-Optionen kommen über ein Dropdown „+ Standard-Option
|
||
hinzufügen“ dazu, statt alle 56 dauerhaft als leere Zeilen anzuzeigen.
|
||
Jede Option hat einen **globalen** Wert sowie optional einen **Wert pro
|
||
Client** — ein Client-Override überschreibt den globalen Wert
|
||
ausschließlich für dieses eine Gerät (technisch: globale Werte landen im
|
||
Top-Level `option-data`, Overrides im `option-data` der jeweiligen
|
||
Kea-Reservierung, was Kea automatisch pro Client Vorrang geben lässt). Im
|
||
Reservierungs-Options-Dialog gilt dasselbe Prinzip: nur Options mit
|
||
gesetztem Override sind sichtbar, weitere kommen über ein Dropdown dazu.
|
||
Das ausgewählte Feld wird dabei direkt unter das Dropdown verschoben und
|
||
fokussiert, statt an seiner ursprünglichen Stelle irgendwo in der u.U.
|
||
langen Liste (bis zu 56 mögliche Options) zu erscheinen — sonst lag es
|
||
oft außerhalb des sichtbaren Modal-Ausschnitts und wirkte, als wäre nichts
|
||
passiert (live genau so reproduziert und behoben).
|
||
- **Reservierungen aus zwei Quellen**: automatisch für jedes aktive Gerät mit
|
||
gültiger MAC + IP (Client-Stammdaten dieser App) sowie zusätzlich **manuell
|
||
anlegbar** („Reservierung hinzufügen“, MAC/IP/Name) — für Geräte, die
|
||
nicht als Client gepflegt werden (z.B. ein externes Gerät), sodass ihnen
|
||
bewusst und mit voller Kontrolle eine feste IP gegeben werden kann, statt
|
||
sie unüberwacht automatisch anzubinden. Automatische Reservierungen können
|
||
in dieser Tabelle **nicht gelöscht** werden (Schloss-Symbol statt Button —
|
||
sie entstehen/verschwinden mit den Client-Stammdaten), manuelle schon. Bei
|
||
einer MAC in beiden Quellen gewinnt immer die automatische. Jede
|
||
Reservierung wird ins **passende Subnetz** einsortiert (die IP entscheidet,
|
||
zu welchem der ggf. mehreren konfigurierten Subnetze sie gehört); der
|
||
Hostname wird aus dem Namen abgeleitet (nur `a-z0-9-`, Kollisionen
|
||
innerhalb desselben Subnetzes bekommen automatisch ein `-2`/`-3`-Suffix).
|
||
Einträge ohne MAC/IP oder mit einer IP außerhalb aller konfigurierten
|
||
Subnetze werden mit Hinweis übersprungen — Kea lehnt eine Reservierung
|
||
außerhalb ihres Subnets als Konfigurationsfehler ab (mit einer echten
|
||
Kea-Instanz verifiziert, inkl. einer Mischung aus automatischer und
|
||
manueller Reservierung im selben Subnetz).
|
||
- **Aktive Leases**: eigene Karte, liest die tatsächlich vergebenen Adressen
|
||
direkt aus Keas Lease-Datenbank (`kea-leases4.csv`, memfile-Backend —
|
||
kein Control-Agent nötig, reiner Lesezugriff auf die Datei). Zeigt damit
|
||
auch Clients **ohne eigene Reservierung**, die sich einfach eine freie IP
|
||
aus dem Pool genommen haben — deutlich als „Ohne Reservierung“ markiert,
|
||
statt nur die (unvollständige) Reservierungsliste zu zeigen. Die
|
||
Ablaufzeit wird explizit als **UTC** ausgewiesen (Kea arbeitet intern in
|
||
UTC-Epoch) und zusätzlich als live tickender Countdown angezeigt — eine
|
||
reine Absolutzeit auf einer nicht selbst aktualisierenden Seite wirkte bei
|
||
den kurzen Standard-Lease-Zeiten (600s) schnell „schon abgelaufen“, sobald
|
||
etwas Zeit vergangen war, bis man tatsächlich hinschaut (live auf der
|
||
Test-VM als genau dieses Missverständnis reproduziert).
|
||
- **Schreiben**: „Speichern“ bei den Reservierungen (nur mit
|
||
`settings_dhcp.edit`) schreibt die vollständige, generierte Kea-JSON-
|
||
Konfiguration an den konfigurierten Ausgabepfad. Wirksam wird sie erst
|
||
nach einem Dienst-Neustart über den separaten Button.
|
||
- **Ampel im Topbar**: neben dem Prüfintervall-Timer zeigt ein grüner/roter
|
||
Punkt, ob der Kea-Dienst läuft — nur sichtbar mit `settings_dhcp.view`.
|
||
- Reservierungen mit aktiven eigenen Options zeigen in der Tabelle, welche
|
||
Option greift (global oder Client-Override).
|
||
|
||
Live gegen eine echte, frisch installierte Kea-Instanz verifiziert:
|
||
Installation, generierte Konfiguration besteht `kea-dhcp4 -t`, Dienststart
|
||
übernimmt sie fehlerfrei (`DHCP4_CONFIG_COMPLETE`), globaler Options-Wert
|
||
sowie Client-spezifischer Override erscheinen korrekt getrennt in der
|
||
Konfiguration.
|
||
|
||
Die Mehrfach-Subnetz-Unterstützung selbst (Anlegen/Bearbeiten/Löschen samt
|
||
Validierung, mehrere generierte `subnet4`-Blöcke mit korrekten Pools/
|
||
Interfaces, Migration einer bestehenden Einzel-Range) ist live auf einem
|
||
echten Host verifiziert. Zwei tatsächlich **unabhängige** Netze (z.B. über
|
||
zwei physische NICs) standen beim Test nicht zur Verfügung — verifiziert
|
||
wurde daher zusätzlich zwei Ranges innerhalb desselben real vorhandenen
|
||
Netzes (identischer `subnet`-CIDR, unterschiedliche Pools); ob Kea zwei
|
||
`subnet4`-Blöcke mit identischem `subnet`-Wert akzeptiert oder als Fehler
|
||
ablehnt, wurde nicht gegen eine echte Kea-Instanz geprüft — bei tatsächlich
|
||
unterschiedlichen Netzen (der eigentliche Anwendungsfall) besteht dieses
|
||
Risiko nicht. Die Leases-Anzeige wurde vollständig End-to-End im echten
|
||
Betrieb bestätigt: ein tatsächlicher, unreservierter Client im Testnetz
|
||
(„shellyhtg3-...“) hat sich selbstständig eine IP aus dem Pool geholt und
|
||
wurde korrekt mit der „Ohne Reservierung“-Markierung angezeigt.
|
||
|
||
## Geräte-Prüfung (`poe.sh`)
|
||
|
||
Der Hintergrund-Check-Dienst (`tesm-check.service`, ruft `poe.sh` ohne
|
||
Argument auf) prüft in einer Endlosschleife alle Geräte per Ping und
|
||
startet bei Ausfall automatisch den PoE-Port neu, über den ein Gerät am
|
||
zugeordneten Switch hängt (`expect`, siehe unten). Zwei Korrekturen nach
|
||
einem echten Import mit deutlich mehr Geräten (46 statt einer Handvoll
|
||
Testgeräte):
|
||
|
||
- **Parallele statt sequentielle Prüfung**: vorher wurde jedes Gerät strikt
|
||
nacheinander in einer einzigen Schleife geprüft — ein einzelnes bereits
|
||
ausgefallenes Gerät mit ebenfalls nicht erreichbarem Switch (jeder
|
||
`expect`-Verbindungsschritt bis zu 5s Timeout) hat dadurch die Prüfung
|
||
aller nachfolgenden Geräte in der Liste verzögert; bei vielen gleichzeitig
|
||
ausgefallenen Geräten konnte ein Prüfdurchlauf so erheblich länger dauern
|
||
als das eigentlich konfigurierte Intervall. Jetzt läuft jede
|
||
Geräte-Prüfung (Ping + ggf. PoE-Neustart) als eigener Hintergrund-Job
|
||
(`&` + `wait` am Ende des Durchlaufs, über Process Substitution statt
|
||
einer Pipe, damit `wait` die Jobs auch tatsächlich erfasst) — mehrere
|
||
Geräte, auch an unterschiedlichen Switchen, werden dadurch gleichzeitig
|
||
geprüft statt einzeln nacheinander.
|
||
- **Echte Erfolgskontrolle statt blind angenommenem Erfolg**: `disable_poe()`/
|
||
`enable_poe()` haben vorher unabhängig vom tatsächlichen Ergebnis der
|
||
SSH/`expect`-Sitzung immer wie ein Erfolg gewirkt — lief die Verbindung
|
||
zum Switch in einen Timeout (Switch nicht erreichbar, falsche
|
||
Zugangsdaten, falscher Port), endete das `expect`-Skript trotzdem mit
|
||
Exit-Code 0, und `poe.sh` loggte fälschlich „PoE deaktiviert“/„PoE
|
||
aktiviert", obwohl am Switch nichts passiert war. Jeder kritische
|
||
`expect`-Schritt (Verbindungsaufbau, `configure terminal`-Prompt,
|
||
Interface-Prompt, Bestätigung des `power-over-ethernet`-Kommandos) hat
|
||
jetzt einen eigenen `timeout`-Zweig mit explizitem `exit 1`; `poe.sh`
|
||
wertet diesen Rückgabewert aus und loggt bei einem nicht erreichbaren
|
||
oder ablehnenden Switch stattdessen klar „Switch ... nicht erreichbar
|
||
oder Kommando abgelehnt — PoE-Deaktivierung/-Reaktivierung
|
||
fehlgeschlagen“, ohne einen Neustart vorzugaukeln, der nie stattgefunden
|
||
hat. Gilt für die automatische Prüfschleife, den manuellen Neustart
|
||
(`poe.sh restart <mac>`) und damit auch für den Dashboard-Neustart-Button.
|
||
|
||
Live nach einem echten Import (46 Geräte, 8 Switche aus dem Altsystem)
|
||
verifiziert: alle 46 Geräte wurden innerhalb von ~2 Sekunden geprüft (statt
|
||
sequentiell), und alle 43 als offline erkannten Geräte mit Switch-Zuordnung
|
||
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
|
||
die Netzwerkkonfiguration des Hosts, auf dem TESM selbst läuft
|
||
(IP-Adresse, DNS-Server, DHCP-Client vs. statisch, Hostname) — nicht um
|
||
Adressen, die er an andere Geräte vergibt. Der Hostname wird separat und
|
||
risikolos über `hostnamectl set-hostname` gesetzt (kein Revert-Timer nötig,
|
||
da eine Änderung nicht die Erreichbarkeit kappt).
|
||
|
||
- **Backend-Erkennung**: prüft, ob NetworkManager oder dhcpcd aktiv ist
|
||
(`systemctl is-active`); ist keins von beiden aktiv, aber `systemd-networkd`
|
||
aktiv und `netplan` installiert, wird **netplan** angenommen — der Standard
|
||
auf einem frisch installierten Ubuntu (Server wie Cloud-Image). Ist keins
|
||
der drei erkennbar, bleibt die Seite bewusst **rein lesend** (Status wird
|
||
trotzdem angezeigt) — ohne zuverlässige Erkennung des Backends keine
|
||
Änderung, um nicht ins Blaue zu raten.
|
||
- **Anzeige**: aktuelles Interface, IP/Prefix, Gateway, DNS-Server sowie
|
||
Modus (DHCP/Statisch) — Modus wird backend-spezifisch ermittelt
|
||
(`nmcli ipv4.method`, ein `static ip_address=`-Eintrag im betreffenden
|
||
`interface`-Block von `/etc/dhcpcd.conf`, bzw. bei netplan am `dynamic`-Flag
|
||
des Kernels auf der aktuellen Adresse — zuverlässiger als die u.U. mehreren
|
||
überlagernden YAML-Dateien unter `/etc/netplan/` selbst zu parsen).
|
||
**DNS-Server-Anzeige**: nicht aus `/etc/resolv.conf` gelesen — unter
|
||
systemd-resolved (Standard auf aktuellem Ubuntu) steht dort immer nur der
|
||
lokale Stub-Resolver `127.0.0.53`, nie die echten Upstream-Server (live
|
||
genau so reproduziert). Stattdessen `resolvectl dns <interface>`, dessen
|
||
mehrzeilige Ausgabe bei vielen Servern korrekt geparst wird (eine
|
||
Fortsetzungszeile ohne `Link N (iface):`-Label trägt selbst
|
||
Doppelpunkte — eine IPv6-Adresse — ein naives Aufteilen an jedem
|
||
Doppelpunkt hätte sie sonst am Anfang abgeschnitten, live genau so
|
||
reproduziert und gefixt). Zusätzlich auf **IPv4** gefiltert, da diese
|
||
Seite ausschließlich die IPv4-Konfiguration des Hosts verwaltet — von
|
||
`resolvectl` ungefragt mitgelieferte, per IPv6-Router-Advertisement
|
||
gelernte DNS-Server gehören nicht dazu und wären hier nur irreführend.
|
||
- **Änderung**: Interface, Modus (DHCP/Statisch), bei Statisch zusätzlich
|
||
IP/Prefix/Gateway, sowie unabhängig davon eigene DNS-Server (funktioniert
|
||
in beiden Modi — z.B. IP per DHCP, aber feste DNS-Server). Bei netplan
|
||
landet die Änderung in einer eigenen, niedrig priorisierten Override-Datei
|
||
(`/etc/netplan/90-tesm.yaml`) statt in der vom Distributor bzw.
|
||
cloud-init mitgelieferten — netplan wendet Dateien alphabetisch an, spätere
|
||
Werte gewinnen pro Schlüssel, damit bleibt die Original-Datei unangetastet.
|
||
- **Sicherheitsnetz** (wie `netplan try`): vor jeder Änderung wird die
|
||
vorherige Konfiguration gesichert. Nach dem Anwenden läuft ein
|
||
Hintergrund-Timer (Standard 45s) — wird die Änderung nicht **explizit
|
||
bestätigt** (nur möglich, wenn die Seite über die neue Konfiguration noch
|
||
erreichbar ist), rollt die App automatisch auf die gesicherte
|
||
Konfiguration zurück. Verhindert, dass ein Tippfehler bei IP/Gateway den
|
||
Host von der Web-UI/SSH abschneidet.
|
||
- **DHCP-Server wird vorsorglich gestoppt UND deaktiviert**: eine vorher
|
||
passende Range (siehe DHCP-Konfiguration oben) kann nach einer
|
||
IP-/Subnetzänderung plötzlich zum falschen Netz gehören. Läuft Kea beim
|
||
Anwenden oder beim Auto-Rollback, wird der Dienst deshalb gestoppt und
|
||
deaktiviert (nicht automatisch neu gestartet — die Range muss der Admin
|
||
aktiv auf der DHCP-Seite prüfen und den Dienst dort bewusst wieder
|
||
starten). Das zusätzliche Deaktivieren verhindert, dass ein Host-Neustart
|
||
vor dieser Prüfung den Dienst trotzdem automatisch mit der ggf. falschen
|
||
Range wieder hochfährt, statt dass die App mit einer möglicherweise
|
||
falschen Konfiguration weiterläuft oder blind rät.
|
||
- Live gegen eine echte Ubuntu-24.04-VM verifiziert (Standard-Installation,
|
||
Netz per DHCP über netplan/systemd-networkd bezogen) — sowohl die
|
||
Erkennung/Statusanzeige als auch der komplette Anwenden-Pfad inkl. der
|
||
eigenen Override-Datei. NetworkManager und dhcpcd sind zusätzlich per
|
||
gemockten `subprocess.run`/Dateizugriffen verifiziert (inkl. eines echten
|
||
`dhcpcd.conf`-Rewrites und eines vollständigen Anwenden-dann-Auto-
|
||
Rollback-Durchlaufs), da in der WSL-Entwicklungsumgebung keins von beiden
|
||
verfügbar ist.
|
||
|
||
## Änderungslog
|
||
|
||
Jede Anlage, Bearbeitung, Löschung sowie jedes Aktivieren/Deaktivieren von
|
||
Geräten, Switchen, Zugangsdaten, Benutzern und Gruppen wird in der Tabelle
|
||
`audit_log` protokolliert (Zeitpunkt, Benutzer, Aktion, Ziel, Details) —
|
||
einsehbar unter **Logs → Änderungen** (Recht `logs_activity.view`). Zusätzlich merken
|
||
sich Geräte und Switche direkt am Datensatz (`last_modified_by`,
|
||
`last_modified_at`), wer sie zuletzt geändert hat, damit man das nicht erst
|
||
im Log nachschlagen muss. Jeder Eintrag wird zusätzlich in eine eigene Datei
|
||
gespiegelt (siehe [Logs & Log-Rotation](#logs--log-rotation)) — die
|
||
`audit_log`-Tabelle bleibt dabei die Quelle für diese durchsuchbare/
|
||
sortierbare Seite.
|
||
|
||
PoE-Neustarts werden **bewusst nicht** im Änderungslog erfasst — die stehen
|
||
bereits im Live-Log von `poe.sh`. Eine Ausnahme ist die Änderung des
|
||
Prüfintervalls selbst (eine Einstellungsänderung, kein Neustart), die wird
|
||
mitprotokolliert.
|
||
|
||
## Logs & Log-Rotation
|
||
|
||
Vier getrennte, eigene Log-Dateien statt allem in `/var/log` vermischt oder
|
||
nur im journalctl-Output — jede mit derselben, unter **Systemeinstellungen →
|
||
Logs** einstellbaren Rotation (Intervall täglich/wöchentlich/monatlich,
|
||
Standard wöchentlich; Aufbewahrung als Anzahl Rotationen, Standard 4 ≈ 1
|
||
Monat Historie):
|
||
|
||
| Log | Datei | Einsehbar unter |
|
||
|-----|-------|------------------|
|
||
| Live (Ping-Check/PoE-Neustart) | `/var/log/tesm/live.log` | Logs → Live |
|
||
| Änderungen (Audit-Trail) | `/var/log/tesm/changes.log` | Logs → Änderungen (aus der DB) |
|
||
| App (Fehler/Warnungen der Flask-App) | `/var/log/tesm/app.log` | nur Datei, keine eigene Seite |
|
||
| Kea-DHCP | `/var/log/kea/kea-dhcp4.log` | Logs → Kea-DHCP (Recht `logs_kea.view`) |
|
||
|
||
- **Live-Log ist jetzt eine einzige, durchgehend fortgeschriebene Datei**
|
||
statt vorher bei jedem (auch täglich per `tesm-check-restart.timer`
|
||
automatisch ausgelösten) Dienst-Neustart ein neues `rpi-<timestamp>.log`
|
||
— `poe.sh` räumt nicht mehr selbst nach 30 Tagen auf, das übernimmt jetzt
|
||
zentral logrotate. Bestehende alte `rpi-*.log`-Dateien werden beim ersten
|
||
Start dieser Version einmalig aufgeräumt (Migration).
|
||
- **Kea-DHCP-Log liegt bewusst NICHT unter `/var/log/tesm/`**: Keas
|
||
AppArmor-Profil (`/etc/apparmor.d/usr.sbin.kea-dhcp4`) erlaubt
|
||
Schreibzugriff exklusiv auf den Dateinamen `/var/log/kea/kea-dhcp4.log`
|
||
(kein Verzeichnis-Wildcard) — ein anderer Pfad wird mit `DENIED ... mknod
|
||
... .lock` abgelehnt (live so reproduziert). Statt das AppArmor-Profil
|
||
eines Systemdienstes aufzuweichen, nutzt die App Keas eigenen, bereits
|
||
erlaubten Pfad; die logrotate-Konfiguration deckt trotzdem beide
|
||
Verzeichnisse mit derselben Regel ab.
|
||
- **`copytruncate` statt Signal-/Neustart-basierter Rotation**: keiner der
|
||
drei schreibenden Prozesse (`poe.sh`, Flask, `kea-dhcp4-server`)
|
||
implementiert ein Log-Reopen-Signal (SIGHUP o.ä.) — `copytruncate`
|
||
funktioniert ohne jede Kooperation des schreibenden Prozesses und läuft
|
||
als root, ist also auch von Keas AppArmor-Profil nicht betroffen.
|
||
- **App-Log** (`app.log`) fängt Fehler/Warnungen der Flask-App ab (vorher
|
||
nur im journalctl-Output von `tesm.service` sichtbar, ohne von der App
|
||
selbst kontrollierbare Aufbewahrung) — bewusst nur `WARNING` und höher,
|
||
kein Access-/Request-Rauschen.
|
||
- **Zeitzone** (Systemeinstellungen, zusammen mit dem Hostnamen in der
|
||
Kachel „Host“): setzt die System-Zeitzone per `timedatectl
|
||
set-timezone`, damit alle vier Logs sowie das Änderungslog die
|
||
tatsächliche lokale Zeit statt UTC zeigen. Wirkt über `time.tzset()`
|
||
sofort für die bereits laufende `tesm.service`-Instanz (kein
|
||
Neustart nötig) — `poe.sh`/`date` liest die Zeitzone ohnehin bei jedem
|
||
Prüf-Durchlauf frisch, da es ein neuer Prozess pro Durchlauf ist.
|
||
- **Systemeinstellungen neu sortiert**: Host (Hostname + Zeitzone in einer
|
||
Kachel) und Netzwerkeinstellungen stehen als thematisch zusammengehörige
|
||
Host-Konfiguration nebeneinander, gefolgt von Logs und Prüfintervall.
|
||
|
||
Live verifiziert: alle vier Log-Dateien werden geschrieben, die
|
||
logrotate-Konfiguration korrekt generiert und bei einer Änderung der
|
||
Einstellung neu geschrieben, ein Zeitzonenwechsel (UTC → Europe/Berlin)
|
||
wirkte sofort — die Log-Zeile, die den Wechsel selbst protokolliert, trug
|
||
bereits den neuen Zeitstempel.
|
||
|
||
## Import/Export
|
||
|
||
Unter **Einstellungen → Im-/Export** lassen sich acht Kategorien einzeln
|
||
auswählen und als ein JSON-Bundle exportieren: Geräte, Switche, Zugangsdaten
|
||
(inkl. entschlüsselter Passwörter), Benutzer, Gruppen, LDAP-Einstellungen
|
||
(inkl. Gruppenzuordnungen), DHCP-Konfiguration und Logs. **AD/LDAP-
|
||
Benutzerkonten werden dabei nie mitexportiert** — nur lokale Konten landen
|
||
in der Datei, siehe [LDAP-Kapitel](#ldap--active-directory-same-sign-on).
|
||
Das Bundle wird mit einem aus einer selbstgewählten Passphrase abgeleiteten
|
||
Schlüssel verschlüsselt (PBKDF2-HMAC-SHA256 + Fernet) — unabhängig vom
|
||
`fernet.key` dieser Installation, damit die Datei auf einer komplett neuen
|
||
Umgebung (mit eigenem, neu generiertem `fernet.key`) wieder eingelesen
|
||
werden kann. Gedacht für einen Umzug auf einen neuen Server oder ein
|
||
selektives Backup einzelner Kategorien.
|
||
|
||
**Zweistufiger Import.** Datei hochladen + Passphrase eingeben lädt das
|
||
Bundle serverseitig nur kurzzeitig in den Speicher (Token-basiert, läuft
|
||
nach einer Weile automatisch ab) und zeigt eine **Vorschau**: nur die
|
||
Kategorien, die tatsächlich in der Datei enthalten sind, jeweils mit Anzahl
|
||
der Einträge. Erst danach wählt man aus, welche der vorhandenen Kategorien
|
||
tatsächlich übernommen werden sollen — ein Import kann also z.B. nur die
|
||
Geräte aus einer Datei ziehen, ohne versehentlich auch Benutzer oder
|
||
LDAP-Einstellungen zu überschreiben. Abgeglichen wird pro Kategorie nach dem
|
||
jeweiligen natürlichen Schlüssel (Zugangsdaten nach Name, Switche nach
|
||
Hostname, Geräte nach MAC-Adresse, Benutzer nach Benutzername, Gruppen nach
|
||
Name) — bestehende Einträge werden aktualisiert, neue angelegt. Switche
|
||
referenzieren ihre Zugangsdaten dabei über den Namen (nicht die interne ID),
|
||
damit der Import auch bei abweichender ID-Vergabe auf dem Zielsystem korrekt
|
||
verknüpft.
|
||
|
||
**Berechtigungen.** Die Im-/Export-Seite hat drei eigene Rechte: **R**
|
||
(`settings_importexport.view`) zum Ansehen der Seite (Standardbedeutung wie
|
||
überall), **E** (`settings_importexport.edit`) zum Ausführen eines Imports,
|
||
und **X** (`settings_importexport.export`) — eine eigene, dedizierte
|
||
Berechtigung nur zum Ausführen eines Exports, da die Export-Datei Passwörter
|
||
im Klartext enthält (entschlüsselt vor der eigenen Passphrasen-
|
||
Verschlüsselung) und deshalb bewusst nicht automatisch an jeden mit
|
||
Änderungsrecht gekoppelt ist. Admins haben ohnehin immer alle drei; für
|
||
andere Gruppen lassen sich Import und Export unabhängig voneinander vergeben.
|
||
Zusätzlich sind die drei Kategorien **Benutzer, Gruppen und LDAP**
|
||
unabhängig von R/E/X grundsätzlich Admins vorbehalten (sowohl beim Export
|
||
als auch beim Import) — ein Nicht-Admin mit E/X sieht und nutzt dort nur die
|
||
übrigen fünf Kategorien.
|
||
|
||
## Papierkorb
|
||
|
||
Löschen ist für Geräte, Switche, Zugangsdaten, (lokale) Benutzer und
|
||
(benutzerdefinierte) Gruppen ein **Soft-Delete** statt eines echten
|
||
`DELETE` — der Eintrag bekommt nur einen Zeitstempel (`deleted_at`) und
|
||
verschwindet dadurch aus allen normalen Listen, Dropdowns und
|
||
Berechtigungsprüfungen, bleibt aber vollständig erhalten und wiederherstellbar.
|
||
Erreichbar über einen eigenen Navigationspunkt **„Papierkorb"** direkt
|
||
unterhalb von „Wartung" (sichtbar, sobald man für mindestens eine der fünf
|
||
Kategorien Leserecht hat) — dort ist jede Kategorie als eigene, permissions-
|
||
gegated Kachel mit „Wiederherstellen"- und „Endgültig löschen"-Aktion
|
||
aufgeführt.
|
||
|
||
**Ausnahmen**: System-/Standardgruppen (aktuell nur „Benutzer") lassen sich
|
||
grundsätzlich nicht löschen, landen also auch nie im Papierkorb — unverändert
|
||
gegenüber vorher. **AD/LDAP-Benutzerkonten landen ebenfalls nie im
|
||
Papierkorb** und werden bei „Löschen" weiterhin sofort endgültig entfernt, da
|
||
sie sich bei jedem gültigen Login automatisch neu anlegen (siehe
|
||
[LDAP-Kapitel](#ldap--active-directory-same-sign-on)) — ein
|
||
Papierkorb-Zwischenzustand wäre hier bedeutungslos.
|
||
|
||
**Reservierter Schlüssel, solange etwas im Papierkorb liegt**: MAC-Adresse,
|
||
Hostname, Zugangsdaten-Name, Benutzername bzw. Gruppenname eines
|
||
gelöschten Eintrags bleiben bewusst reserviert und blockieren die
|
||
Neuanlage eines gleichnamigen Eintrags, bis der alte entweder
|
||
wiederhergestellt oder endgültig gelöscht (purged) wird — der Fehlertext
|
||
beim Anlegen weist entsprechend auf den Papierkorb hin. Kind-Datensätze
|
||
(Gruppenmitgliedschaften, Gruppenrechte, DHCP-Werte pro Gerät) werden beim
|
||
Soft-Delete bewusst **nicht** mit entfernt, damit eine Wiederherstellung
|
||
exakt den vorherigen Zustand zurückbringt — aufgeräumt werden sie erst beim
|
||
**endgültigen** Löschen (Purge).
|
||
|
||
**Aufbewahrungsfrist**: konfigurierbar unter Systemeinstellungen
|
||
(`trash_retention_days`, Standard 30 Tage) — abgelaufene Einträge werden
|
||
beim Aufruf der Papierkorb-Seite automatisch endgültig gelöscht (kein
|
||
eigener Scheduler nötig), zusätzlich gibt es dort einen Button, um
|
||
abgelaufene Einträge sofort manuell zu bereinigen.
|
||
|
||
## Eigenes Konto & Profilbild
|
||
|
||
Das Zahnrad-Symbol neben dem eigenen Namen unten in der Sidebar führt zu
|
||
**„Mein Konto“** (`/account`): Vor-/Nachname ändern, ein Profilbild
|
||
hochladen (PNG/JPG/GIF/WEBP) und das eigene Passwort ändern (erfordert
|
||
Eingabe des aktuellen Passworts). Das ist bewusst getrennt vom Admin-Bereich
|
||
„Benutzer“, der andere Konten verwaltet. Ist ein Profilbild hinterlegt,
|
||
erscheint es statt der Initialen sowohl in der Sidebar als auch im
|
||
Änderungslog neben dem Benutzernamen. Die Navbar-Reihenfolge (für Admins)
|
||
liegt dagegen unter Systemeinstellungen, siehe
|
||
[Navigation anpassen](#navigation-anpassen).
|
||
|
||
## Warnung bei ungespeicherten Änderungen
|
||
|
||
Generischer, app-weiter Mechanismus (kein Opt-in pro Seite nötig): erkennt
|
||
automatisch, ob ein Formular mit echten Eingabefeldern (nicht nur
|
||
versteckten Aktions-Feldern wie bei Löschen-/Toggle-Buttons) seit dem Laden
|
||
der Seite verändert wurde, und fragt beim Verlassen (Klick auf einen Link,
|
||
z.B. in der Navigation) per eigenem **Ja/Nein-Dialog**, ob zuerst gespeichert
|
||
werden soll, statt Änderungen stillschweigend zu verwerfen:
|
||
|
||
- **„Ja, speichern“**: sendet das betroffene Formular ganz normal ab (echter
|
||
POST, kein AJAX-Nachbau) und merkt sich das eigentliche Navigationsziel in
|
||
`sessionStorage`, um nach dem Speichern automatisch dorthin
|
||
weiterzuleiten — der Link muss dafür nicht ein zweites Mal angeklickt
|
||
werden.
|
||
- **„Nein, verwerfen“**: verwirft die Änderungen und wechselt direkt zur
|
||
Zielseite.
|
||
- Klickt man stattdessen auf das eigene „Speichern“/„Anlegen“ der Seite
|
||
direkt (ohne über einen Link zu einer anderen Seite zu gehen), passiert
|
||
**nichts zusätzlich** — kein Dialog, kein Browser-Popup, es wird einfach
|
||
ganz normal gespeichert.
|
||
- Für Browser-eigene Navigation (Tab schließen, Neuladen, Adresszeile) gibt
|
||
es zusätzlich `beforeunload` — dort erlaubt der Browser aus
|
||
Sicherheitsgründen nur eine generische Warnung ohne eigene Buttons, kein
|
||
Ja/Nein/Speichern-Dialog. Sie wird bewusst unterdrückt, sobald die Seite
|
||
ohnehin schon bewusst verlassen wird (eigenes Speichern oder der
|
||
Ja/Nein-Dialog) — sie soll ausschließlich bei tatsächlich ungefragter
|
||
Navigation erscheinen, nicht zusätzlich bei einem ganz normalen Speichern
|
||
(live als genau dieses Doppel-Popup reproduziert und behoben).
|
||
- Ausnahmen über `data-no-unsaved-guard` auf Formular oder Link (z.B. das
|
||
Login-Formular — Zugangsdaten eintippen ist keine "Einstellung").
|
||
|
||
Live per Playwright auf mehreren Seiten (Systemeinstellungen, DHCP-Options,
|
||
Gruppen anlegen) verifiziert: eigener Dialog erscheint nur bei tatsächlich
|
||
ungespeicherten Änderungen, „Ja“ speichert und leitet danach automatisch
|
||
zum ursprünglichen Ziel weiter, „Nein“ verwirft und wechselt direkt, ein
|
||
normales Speichern über den Seiten-eigenen Button zeigt weder eigenen noch
|
||
Browser-Dialog.
|
||
|
||
Nebenbei wurde ein generisches Cache-Busting für `app.js`/`style.css`
|
||
ergänzt (`asset_url()`-Helper, hängt die Änderungszeit der Datei als
|
||
`?v=...` an) — vorher konnte ein bereits gecachtes, veraltetes `app.js` im
|
||
Browser nach einem Update scheinbar "nicht funktionierende" neue Funktionen
|
||
verursachen, obwohl der Code auf dem Server längst aktuell war.
|
||
|
||
## Navigation anpassen
|
||
|
||
Die Sidebar besteht aus zwei aufklappbaren Gruppen und mehreren flachen
|
||
Top-Level-Punkten dazwischen:
|
||
|
||
- **Geräte** (Gruppe) — Clients (die eigentlichen Devices), Switche,
|
||
Zugangsdaten, Wartung (SSH-Bulk-Update/-Neustart), **Papierkorb**
|
||
(direkt unterhalb von Wartung, siehe [Papierkorb](#papierkorb))
|
||
- **Benutzer**, **Gruppen** — eigene, flache Top-Level-Punkte direkt nach
|
||
„Geräte“ (bewusst nicht in einer Untergruppe versteckt)
|
||
- **Einstellungen** (Gruppe) — Systemeinstellungen (Host/Prüfintervall/
|
||
Navbar-Reihenfolge/Papierkorb-Aufbewahrung), LDAP, DHCP, Im-/Export
|
||
- **Logs** (Gruppe) — Live, Änderungen, Kea-DHCP
|
||
|
||
Die Reihenfolge der Top-Level-Punkte **und** der Unterpunkte innerhalb jeder
|
||
Gruppe ist für Admins unter **Systemeinstellungen** (eigene, nicht
|
||
einklappbare Karte „Navbar-Reihenfolge") per Auf-/Ab-Buttons konfigurierbar
|
||
(gespeichert als JSON unter `settings.nav_order` bzw.
|
||
`settings.nav_child_order`) und gilt global für alle Benutzer. Unabhängig
|
||
von der Reihenfolge sieht jeder Benutzer ohnehin nur, wofür er berechtigt
|
||
ist — sowohl auf Gruppenebene als auch pro Unterpunkt: hat ein Benutzer z.B.
|
||
nur `devices.view`, erscheint die Gruppe „Geräte“ nur mit dem Unterpunkt
|
||
„Clients“ (ohne Switche/Zugangsdaten/Wartung/Papierkorb); eine Gruppe
|
||
verschwindet komplett, wenn kein Unterpunkt sichtbar ist.
|
||
|
||
## Entwicklung unter WSL
|
||
|
||
Da das Projekt ausschließlich für Linux gebaut ist, empfiehlt sich die
|
||
Entwicklung direkt unter WSL2 (Ubuntu), auch wenn der Editor unter Windows
|
||
läuft — das Projektverzeichnis liegt dabei ganz normal unter
|
||
`C:\...\aruba-poe-modern` und wird aus WSL über `/mnt/c/...` erreicht:
|
||
|
||
```bash
|
||
wsl --install -d Ubuntu # einmalig, falls noch nicht vorhanden
|
||
wsl -d Ubuntu
|
||
sudo apt update && sudo apt install -y python3 python3-venv python3-pip sqlite3 expect openssh-client
|
||
cd /mnt/c/Users/<dich>/aruba-poe-modern/srv/tesm
|
||
python3 -m venv venv
|
||
./venv/bin/pip install -r requirements.txt
|
||
./venv/bin/python create_db.py
|
||
./venv/bin/python create_admin.py
|
||
./venv/bin/python app.py # http://localhost:5000 — via WSL2-Portweiterleitung
|
||
# auch aus dem Windows-Browser erreichbar
|
||
```
|
||
|
||
**Hinweis zu `tesm-check.service`:** Der „Speichern & Service neustarten“-Button
|
||
unter Systemeinstellungen sowie das „Jetzt prüfen“-Icon funktionieren nur,
|
||
wenn `tesm-check.service` tatsächlich als echter systemd-Service läuft (viele
|
||
aktuelle WSL2-Installationen unterstützen systemd, siehe `wsl.conf`
|
||
`[boot] systemd=true`). Für lokale Tests reicht es, `etc/systemd/system/
|
||
tesm-check.service` sowie `usr/local/bin/custom/poe.sh` per Symlink an ihre
|
||
Produktivpfade (`/etc/systemd/system/`, `/usr/local/bin/custom/`) zu legen
|
||
und `/srv/tesm` auf den Checkout zu verlinken, dann `systemctl
|
||
daemon-reload && systemctl enable --now tesm-check.service`. Ohne systemd
|
||
(z.B. `systemctl` nicht vorhanden) scheitert der Neustart-Aufruf lautlos —
|
||
das Intervall in der DB wird trotzdem aktualisiert, greift dann aber erst
|
||
nach einem manuellen Neustart des Prozesses.
|
||
|
||
## Lokal starten (Entwicklung)
|
||
|
||
```bash
|
||
cd srv/tesm
|
||
python3 -m venv venv
|
||
source venv/bin/activate
|
||
pip install -r requirements.txt
|
||
|
||
python create_db.py # legt sqlite.db + Tabellen an
|
||
python create_admin.py # legt ersten Admin-Benutzer an
|
||
|
||
python app.py # http://localhost:5000
|
||
```
|
||
|
||
## Produktiv-Deployment (auf der Ziel-VM, App landet unter `/srv/tesm`)
|
||
|
||
Das Repo wird bewusst **nicht** direkt nach `/srv/tesm` geklont —
|
||
`install.sh` kopiert (`rsync`) den Inhalt von `srv/tesm/` aus dem Repo
|
||
gezielt dorthin und lässt Quell-Checkout und Zielverzeichnis damit sauber
|
||
getrennt (kein doppeltes `/srv/tesm/srv/tesm/…`):
|
||
|
||
```bash
|
||
sudo git clone <dieses-repo> /opt/aruba-poe-modern-src
|
||
cd /opt/aruba-poe-modern-src
|
||
sudo bash install.sh
|
||
sudo /srv/tesm/venv/bin/python3 /srv/tesm/create_admin.py
|
||
```
|
||
|
||
Nach der Installation liegt die laufende App vollständig unter
|
||
**`/srv/tesm`** (so wie es `tesm.service`, `tesm-check.service` und
|
||
`POE_SCRIPT`/`TESM_DB_PATH` als Default erwarten) — `/opt/aruba-poe-modern-src`
|
||
wird nur als Checkout-Quelle für `install.sh` gebraucht und kann danach
|
||
liegen bleiben (für spätere Updates: `git pull` dort, dann `install.sh`
|
||
erneut ausführen) oder gelöscht werden.
|
||
|
||
`install.sh` installiert Systempakete, richtet die venv ein, initialisiert die
|
||
Datenbank (falls nicht vorhanden), kopiert `poe.sh` nach
|
||
`/usr/local/bin/custom/`, registriert die systemd-Units
|
||
(`tesm.service`, `tesm-check.service`, `tesm-check-restart.timer`) und
|
||
konfiguriert nginx als Reverse-Proxy auf Port 80.
|
||
|
||
### Relevante Umgebungsvariablen (optional)
|
||
|
||
| Variable | Zweck | Default |
|
||
|-----------------------|------------------------------------------|---------------------------------------|
|
||
| `TESM_DB_PATH` | Pfad zur SQLite-DB | `<app-dir>/sqlite.db` |
|
||
| `POE_SCRIPT` | Pfad zum PoE-Restart-Skript | `/usr/local/bin/custom/poe.sh` |
|
||
| `TESM_LOG_DIR` | Verzeichnis für `live.log`/`changes.log`/`app.log` | `/var/log/tesm` |
|
||
| `TESM_FERNET_KEY` | Pfad zum Fernet-Schlüssel | `<app-dir>/fernet.key` (auto-erzeugt)|
|
||
| `TESM_KNOWN_HOSTS` | Pfad zur SSH-known_hosts-Datei des Terminals| `<app-dir>/known_hosts` (auto-erzeugt)|
|
||
| `TESM_SECRET_KEY` | Flask Secret Key (überschreibt Datei) | zufällig, in `secret.key` gespeichert|
|
||
| `PORT`, `FLASK_DEBUG` | Flask-Serverport / Debug-Modus | `5000` / aus |
|
||
|
||
### Frische Neuinstallation aus einem Release (`update.sh`)
|
||
|
||
Für einen Host, dessen `/srv/tesm` bislang ad-hoc gepflegt wurde (z.B. per
|
||
manuellem Datei-Upload statt über `install.sh`), oder um einen sauberen
|
||
Stand aus einem getaggten Release herzustellen, gibt es zusätzlich zum
|
||
`git pull` + `install.sh`-Update oben `update.sh`:
|
||
|
||
```bash
|
||
sudo ./update.sh
|
||
# oder mit einer anderen Version/Repo-URL:
|
||
sudo TESM_RELEASE_TAG=v1.0.0 TESM_REPO_URL=https://gitea.../alientim/tesm.git ./update.sh
|
||
```
|
||
|
||
Das Skript stoppt und deaktiviert zunächst die laufenden Dienste
|
||
(`tesm.service`, `tesm-check.service`, `tesm-check-restart.timer`),
|
||
verschiebt das bestehende `/srv/tesm` **ohne es zu löschen** nach
|
||
`/srv/tesm-backup-<Zeitstempel>` (bleibt inkl. `sqlite.db` als Rückfall-
|
||
option und Datenquelle erhalten), klont dann den angegebenen Git-Tag frisch
|
||
und führt darin `install.sh` aus.
|
||
|
||
**Ergebnis ist eine komplett frische Installation** — neue, leere Datenbank,
|
||
keine Geräte/Switche/Zugangsdaten/Benutzer/Einstellungen aus der alten
|
||
Instanz. Vor dem Ausführen deshalb unbedingt unter **Einstellungen →
|
||
Im-/Export** ein Backup der alten Instanz exportieren; danach entweder alles
|
||
manuell neu einrichten oder dieses Backup in die neue Installation
|
||
importieren (siehe [Import/Export](#importexport)). Das Skript weist sowohl
|
||
vor dem Start als auch am Ende explizit darauf hin.
|
||
|
||
## Browser-SSH-Terminal (Switch-Verbindungstest)
|
||
|
||
„Verbindung testen“ im Switch-Modal öffnet eine WebSocket-Verbindung zu
|
||
`/ws/ssh_terminal`. Serverseitig baut **paramiko** eine echte SSH-Sitzung
|
||
zum Switch auf (inkl. interaktiver Host-Key-Bestätigung beim Erstkontakt,
|
||
danach dauerhaft in `known_hosts` gemerkt) und reicht eine interaktive Shell
|
||
1:1 an ein xterm.js-Terminal im Browser durch. Die Zugangsdaten aus dem
|
||
Formular werden direkt für die SSH-Authentifizierung verwendet (nicht als
|
||
Tastatureingabe in die Shell getippt) — Erfolg/Fehlschlag der Anmeldung wird
|
||
sofort und eindeutig im Terminal angezeigt. Ein Klick auf „Passwort
|
||
einfügen“ sendet den aktuell im Formular eingetragenen Wert zusätzlich als
|
||
Tastatureingabe (z.B. für ein `sudo`-Passwort in der geöffneten Sitzung).
|
||
|
||
Voraussetzungen:
|
||
- Läuft auf **jedem Betriebssystem** (paramiko ist reines Python) — die
|
||
SSH-Verbindung wird dabei immer vom PoE-Server aus aufgebaut, nie vom
|
||
Browser/Client.
|
||
- Nur für eingeloggte **Admins** erreichbar.
|
||
- Der Flask-Entwicklungsserver muss mit `threaded=True` laufen (ist bereits
|
||
so konfiguriert) — für einen echten Produktivbetrieb empfiehlt sich
|
||
ohnehin ein WSGI-Server mit WebSocket-Unterstützung (z.B. Gunicorn mit
|
||
`gevent`- oder `eventlet`-Worker) statt des eingebauten Dev-Servers.
|
||
- **nginx muss den WebSocket-Upgrade für `/ws/` durchreichen** (`Upgrade`/
|
||
`Connection`-Header, siehe `etc/nginx/sites-available/tesm`) — ohne
|
||
das lehnt Flask den Handshake mit `400 Bad Request` ab und das Terminal
|
||
bleibt leer auf „Getrennt“ hängen, obwohl derselbe Aufruf direkt gegen
|
||
Port 5000 (an nginx vorbei) funktioniert. War auf einer frisch per
|
||
`install.sh` aufgesetzten Ubuntu-VM live reproduzierbar, da alle bisherigen
|
||
Terminal-Tests ausschließlich direkt gegen Flask liefen, nie durch nginx.
|
||
- Da `tesm.service` (wie im Original) als `root` läuft, kann über dieses
|
||
Terminal im Prinzip jeder erreichbare Host per SSH kontaktiert werden —
|
||
bewusst so gewählt, da Admins über die App ohnehin schon vollen Zugriff auf
|
||
alle hinterlegten Switch-Zugangsdaten haben. Für eine strengere Trennung
|
||
empfiehlt es sich, `tesm.service` künftig unter einem eigenen,
|
||
unprivilegierten Systembenutzer laufen zu lassen (im Original wie auch hier
|
||
aktuell `root`, um Änderungen am produktiven Setup nicht zu vergrößern).
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
srv/tesm/ Flask-App (Backend + Templates + Static-Assets)
|
||
usr/local/bin/custom/ poe.sh — Ping-Check & PoE-Restart per SSH (expect)
|
||
etc/systemd/system/ Service-/Timer-Units (Web-App + Check-Dienst)
|
||
etc/nginx/ Reverse-Proxy-Konfiguration
|
||
install.sh Produktiv-Installer
|
||
```
|