Rebrand to TESM: infra rename, LDAP/AD login, selective import/export, Papierkorb

- Vollständiges Rebranding "PoE Manager" -> TESM (TimEShepManager): neue
  Logos/Favicon (theme-aware), Sidebar/Login-Branding, Copyright-Zeile.
- Infrastruktur-Umbenennung: srv/poe_manager -> srv/tesm, alle POE_*-Env-Vars
  -> TESM_* (POE_SCRIPT bewusst unverändert, echtes PoE-Skript), systemd
  Units poe_web/rpi-check* -> tesm/tesm-check*, nginx-Site, netplan/logrotate
  Configs, Gitea-Repo alientim/Aruba-PoE-Modern -> alientim/tesm.
- LDAP/Active-Directory Same-Sign-On: Search+Bind, AD-Gruppen->App-Gruppen-
  Zuordnung (additiv), Konto-Sperren, AD-Vorab-Suche/-Anlage, eigene
  Einstellungsseite.
- Selektives Import/Export (8 Kategorien, Zwei-Schritt-Vorschau) mit eigenem
  R/E/X-Rechtemodell (X = Export, getrennt von E = Import).
- Papierkorb: Soft-Delete statt Hard-Delete für Geräte/Switche/Zugangsdaten/
  Benutzer/Gruppen (AD-Nutzer ausgenommen), eigene Seite unterhalb Wartung,
  konfigurierbare Aufbewahrungsfrist.
- rpi_ip -> ip Spalten-/Code-Umbenennung (Geräte sind längst nicht mehr auf
  Raspberry Pis beschränkt).
- README auf aktuellen Stand gebracht.
This commit is contained in:
2026-08-12 22:21:52 +02:00
parent 49634da0d9
commit d1b10bd970
50 changed files with 3641 additions and 728 deletions
+303 -85
View File
@@ -1,4 +1,4 @@
# PoE Manager — Modern UI Rebuild
# 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)
@@ -47,6 +47,9 @@ Die App ermöglicht:
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
@@ -113,8 +116,8 @@ Die App ermöglicht:
ü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 (`POE_DB_PATH`,
`POE_SCRIPT`, `POE_LOG_GLOB`, …) statt hart codierter/inkonsistenter Pfade;
- **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.
@@ -137,29 +140,43 @@ Ein Benutzer kann mehreren Gruppen angehören, die Rechte addieren sich
### Zweistufiger Rechtebaum
Der Baum hat genau zwei Ebenen, exakt gespiegelt an den drei Sidebar-Gruppen
(Geräte/Logs/Einstellungen). Auf der Gruppen-Seite stehen sie 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. Eine vierte Spalte **D**
gibt es nur bei Geräte → Clients, und sie bedeutet dort **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). Für Zeilen ohne passendes Recht (z.B. „D“ bei Switche/
Zugangsdaten/Benutzer/Gruppen) wird eine ausgegraute, nicht anklickbare
Checkbox angezeigt statt die Zelle wegzulassen:
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
Clients ☑ ☑ ☑ ☑ Live ☑ ☑ ☑ Benutzer ☑ ☑ ☑
Switche ☑ ☑ ☑ ▢ Änd. ☑ ☑ ☑ Gruppen ☑ ☑ ☑
Zugangsd. ☑ ☑ ☑ ▢ Systemeinst. ☑ ▢ ☑
Im-/Export ☑ ▢ ☑
☑ 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` /
@@ -217,6 +234,136 @@ sämtliche `*.delete`-Rechte → `*.edit` (Löschen wurde nachträglich überall
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
@@ -297,7 +444,7 @@ letzten Aktion, per Klick ein-/ausblendbar) wird per Polling (`/maintenance/stat
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
`poe_web.service` verwirft nur die Anzeige, nicht die auf dem Zielgerät
`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
@@ -497,7 +644,7 @@ wurde korrekt mit der „Ohne Reservierung“-Markierung angezeigt.
## Geräte-Prüfung (`poe.sh`)
Der Hintergrund-Check-Dienst (`rpi-check.service`, ruft `poe.sh` ohne
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
@@ -543,7 +690,7 @@ fälschlich als 43× „PoE deaktiviert“/„PoE aktiviert“ geloggt.
## Netzwerkeinstellungen des Hosts (Systemeinstellungen)
Eigene, klar getrennte Funktion von obigem DHCP-*Server*: hier geht es um
die Netzwerkkonfiguration des Hosts, auf dem der PoE Manager selbst läuft
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,
@@ -578,7 +725,7 @@ da eine Änderung nicht die Erreichbarkeit kappt).
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-poe-manager.yaml`) statt in der vom Distributor bzw.
(`/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
@@ -635,18 +782,18 @@ Monat Historie):
| Log | Datei | Einsehbar unter |
|-----|-------|------------------|
| Live (Ping-Check/PoE-Neustart) | `/var/log/poe-manager/live.log` | Logs → Live |
| Änderungen (Audit-Trail) | `/var/log/poe-manager/changes.log` | Logs → Änderungen (aus der DB) |
| App (Fehler/Warnungen der Flask-App) | `/var/log/poe-manager/app.log` | nur Datei, keine eigene Seite |
| 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 `rpi-check-restart.timer`
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/poe-manager/`**: Keas
- **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
@@ -660,14 +807,14 @@ Monat Historie):
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 `poe_web.service` sichtbar, ohne von der App
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 `poe_web.service`-Instanz (kein
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
@@ -682,20 +829,85 @@ bereits den neuen Zeitstempel.
## Import/Export
Unter **Einstellungen** lassen sich Geräte, Switche und Zugangsdaten
(inkl. entschlüsselter Passwörter) als ein einziges JSON-Bundle exportieren.
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.
werden kann. Gedacht für einen Umzug auf einen neuen Server oder ein
selektives Backup einzelner Kategorien.
Beim Import wird dieselbe Passphrase erneut abgefragt; Zugangsdaten werden
nach Name, Switche nach Hostname und Geräte nach MAC-Adresse abgeglichen —
bestehende Einträge mit gleichem Schlüssel werden aktualisiert, neue werden
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.
**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
@@ -705,8 +917,9 @@ 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. Admins sehen auf derselben Seite
zusätzlich die Navbar-Reihenfolge (siehe unten).
Änderungslog neben dem Benutzernamen. Die Navbar-Reihenfolge (für Admins)
liegt dagegen unter Systemeinstellungen, siehe
[Navigation anpassen](#navigation-anpassen).
## Warnung bei ungespeicherten Änderungen
@@ -754,23 +967,28 @@ verursachen, obwohl der Code auf dem Server längst aktuell war.
## Navigation anpassen
Die Sidebar ist in drei aufklappbare Gruppen strukturiert:
Die Sidebar besteht aus zwei aufklappbaren Gruppen und mehreren flachen
Top-Level-Punkten dazwischen:
- **Geräte** — Clients (die eigentlichen Devices), Switche, Zugangsdaten,
Wartung (SSH-Bulk-Update/-Neustart)
- **Einstellungen** — Benutzer, Gruppen, Systemeinstellungen (Prüfintervall),
Im-/Export
- **Logs** — Live, Änderungen (Änderungslog)
- **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 dieser drei Gruppen (und von „Dashboard“) ist für Admins
unter **Mein Konto** (Zahnrad-Symbol in der Sidebar) per Auf-/Ab-Buttons
konfigurierbar (gespeichert als JSON-Liste unter `settings.nav_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); eine Gruppe verschwindet komplett, wenn kein Unterpunkt
sichtbar ist.
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
@@ -783,7 +1001,7 @@ läuft — das Projektverzeichnis liegt dabei ganz normal unter
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/poe_manager
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
@@ -792,15 +1010,15 @@ python3 -m venv venv
# auch aus dem Windows-Browser erreichbar
```
**Hinweis zu `rpi-check.service`:** Der „Speichern & Service neustarten“-Button
**Hinweis zu `tesm-check.service`:** Der „Speichern & Service neustarten“-Button
unter Systemeinstellungen sowie das „Jetzt prüfen“-Icon funktionieren nur,
wenn `rpi-check.service` tatsächlich als echter systemd-Service läuft (viele
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/
rpi-check.service` sowie `usr/local/bin/custom/poe.sh` per Symlink an ihre
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/poe_manager` auf den Checkout zu verlinken, dann `systemctl
daemon-reload && systemctl enable --now rpi-check.service`. Ohne systemd
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.
@@ -808,7 +1026,7 @@ nach einem manuellen Neustart des Prozesses.
## Lokal starten (Entwicklung)
```bash
cd srv/poe_manager
cd srv/tesm
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
@@ -819,23 +1037,23 @@ 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/poe_manager`)
## Produktiv-Deployment (auf der Ziel-VM, App landet unter `/srv/tesm`)
Das Repo wird bewusst **nicht** direkt nach `/srv/poe_manager` geklont —
`install.sh` kopiert (`rsync`) den Inhalt von `srv/poe_manager/` aus dem Repo
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/poe_manager/srv/poe_manager/…`):
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/poe_manager/venv/bin/python3 /srv/poe_manager/create_admin.py
sudo /srv/tesm/venv/bin/python3 /srv/tesm/create_admin.py
```
Nach der Installation liegt die laufende App vollständig unter
**`/srv/poe_manager`** (so wie es `poe_web.service`, `rpi-check.service` und
`POE_SCRIPT`/`POE_DB_PATH` als Default erwarten) — `/opt/aruba-poe-modern-src`
**`/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.
@@ -843,19 +1061,19 @@ 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
(`poe_web.service`, `rpi-check.service`, `rpi-check-restart.timer`) und
(`tesm.service`, `tesm-check.service`, `tesm-check-restart.timer`) und
konfiguriert nginx als Reverse-Proxy auf Port 80.
### Relevante Umgebungsvariablen (optional)
| Variable | Zweck | Default |
|-----------------------|------------------------------------------|---------------------------------------|
| `POE_DB_PATH` | Pfad zur SQLite-DB | `<app-dir>/sqlite.db` |
| `TESM_DB_PATH` | Pfad zur SQLite-DB | `<app-dir>/sqlite.db` |
| `POE_SCRIPT` | Pfad zum PoE-Restart-Skript | `/usr/local/bin/custom/poe.sh` |
| `POE_LOG_GLOB` | Glob-Pattern der Check-Logs | `/var/log/rpi-*.log` |
| `POE_FERNET_KEY` | Pfad zum Fernet-Schlüssel | `<app-dir>/fernet.key` (auto-erzeugt)|
| `POE_KNOWN_HOSTS` | Pfad zur SSH-known_hosts-Datei des Terminals| `<app-dir>/known_hosts` (auto-erzeugt)|
| `POE_SECRET_KEY` | Flask Secret Key (überschreibt Datei) | zufällig, in `secret.key` gespeichert|
| `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 |
## Browser-SSH-Terminal (Switch-Verbindungstest)
@@ -881,24 +1099,24 @@ Voraussetzungen:
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/poe_manager`) — ohne
`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 `poe_web.service` (wie im Original) als `root` läuft, kann über dieses
- 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, `poe_web.service` künftig unter einem eigenen,
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/poe_manager/ Flask-App (Backend + Templates + Static-Assets)
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