65 lines
6.7 KiB
Markdown
65 lines
6.7 KiB
Markdown
# ADR-0014: Self-Service-ISO-Bau-Orchestrierung
|
|
|
|
**Status:** Beschlossen
|
|
**Datum:** 07.08.2026
|
|
|
|
## Kontext
|
|
|
|
`13-live-provisioning-boot.md` beschreibt bereits das konkrete Modell für personalisierte ISOs mit eingebetteten WLAN-Zugangsdaten (Abschnitt „WLAN-Zugangsdaten", 21.07.2026): Self-Service, ausgelöst über einen Kundenprofil-Wunsch. Offen war zu diesem Zeitpunkt, wie der eigentliche ISO-Bau technisch orchestriert wird — er dauert mehrere Minuten (`build_customer_iso.sh` patcht die Quell-ISO mit `xorriso`), kann also nicht synchron innerhalb eines HTTP-Requests laufen, und muss sowohl vom internen Betrieb als auch später direkt von Kund:innen auslösbar sein.
|
|
|
|
Zusätzlich bestehen zwei praktische Randbedingungen: anodes Root-Dateisystem ist ein eng bemessener 28-GB-Loop-Mount (jede gebaute ISO mehrere GB groß), und der Browser einer Kund:in besitzt keinen `TUXFLOTTE_KUNDENPLATTFORM_TOKEN`, kann `provisioning-server` also nicht direkt für den Download ansprechen.
|
|
|
|
## Entscheidung
|
|
|
|
Der ISO-Bau läuft als **Background-Thread innerhalb von `provisioning-server`** (`run_iso_build()`, per `threading.Thread(daemon=True)` aus dem auslösenden Request heraus gestartet), der `build_customer_iso.sh` als Subprocess aufruft. Eine neue Tabelle `iso_builds` (`pending`/`running`/`completed`/`failed`) ist der gemeinsame Status-Speicher zwischen dem auslösenden POST-Endpoint und den Status-/Download-Endpunkten — kein Message-Queue-System, weil ein einzelner Prozess (anode) sowohl Bau als auch Auslieferung übernimmt und die erwartete Nebenläufigkeit gering ist (kleine Kundenzahl, seltene Bau-Auslösung).
|
|
|
|
Nach jedem erfolgreichen Bau werden **ältere ISO-Builds derselben Organisation aufgeräumt** (`cleanup_old_iso_builds()`) — direkte Konsequenz aus anodes begrenztem Plattenplatz, nicht aus einer allgemeinen Aufbewahrungsrichtlinie.
|
|
|
|
**WLAN-Zugangsdaten werden bewusst nicht in `provisioning-server`s Datenbank persistiert**, nur transient während des Baus verwendet. Die dauerhafte, verschlüsselte Ablage (Fernet, `installationsmedium_konfiguration.wifi_psk_encrypted`) liegt in Kundenplattforms eigener, getrennter Datenbank (siehe ADR-0011) — ein WLAN-Passwort ist ein echtes Geheimnis, kein Hash-Fall, muss also entschlüsselbar bleiben, um erneut in einen Bau eingehen zu können; die Trennung der Datenbanken bedeutet aber, dass `provisioning-server` selbst kein Klartext-Passwort dauerhaft vorhält.
|
|
|
|
**Zwei getrennte Aktionen statt einer:** „Neues Installationsmedium erstellen" (neuer ISO-Bau, nötig bei geänderten WLAN-Zugangsdaten, da diese in die ISO eingebrannt sind) und „Aufladen" (`PATCH /api/v1/enrollment-sessions/{id}`, erweitert Gerätekontingent/Gültigkeit einer bestehenden Enrollment Session, siehe `11-device-enrollment.md`, ohne die bereits verteilte ISO ungültig zu machen oder neu zu bauen). Eine bereits an Schüler:innen/Klassenräume verteilte ISO bleibt so nutzbar, auch wenn sich nur das Kontingent ändert.
|
|
|
|
Der **Download läuft durch Kundenplattform hindurch gestreamt** (`GET /installationsmedium/download` → `anode_stream()` → `StreamingResponse`), nicht als direkter Link auf `provisioning-server` — der Browser hat kein Service-Token, Kundenplattform ist der einzige Client, der beide Seiten (Kunden-Session, anode-Token) kennt. Kein Read-Timeout auf dieser Verbindung, da eine mehrere GB große Datei über die WireGuard-Verbindung zu anode länger dauern kann als der sonst genutzte 15-Sekunden-Timeout erlaubt.
|
|
|
|
`GET /api/v1/organizations/{id}` (Einzelabruf) wurde ergänzt, weil der bestehende Listen-Endpunkt alle Organisationen zurückgibt — für den internen, organisationsübergreifenden Admin-Bereich unproblematisch, für eine reguläre Kund:in im Kundenportal ein Datenleck.
|
|
|
|
## Konsequenzen
|
|
|
|
`ISO_BUILD_OUTPUT_DIR` und `MINT_ISO_PATH` müssen zu absoluten Pfaden aufgelöst werden (`Path(...).resolve()` beim Modul-Laden) — `run_iso_build()` startet den Subprocess mit `cwd=TUXFLOTTE_INSTALLER_DIR`, ein relativer Default-Pfad würde dort statt relativ zum eigentlichen Arbeitsverzeichnis des Diensts gesucht. Ein erster Live-Test auf anode ist genau daran gescheitert (lokale Tests mit absoluten Pfaden hatten das nicht aufgedeckt).
|
|
|
|
Ein ISO-Bau blockiert den anfragenden Request nicht, aber es gibt aktuell keine Begrenzung gleichzeitig laufender Bau-Threads — bei der aktuellen Kundenzahl kein praktisches Problem, bei deutlich mehr gleichzeitigen Bauten (mehrere Organisationen zur gleichen Zeit) müsste das nachgerüstet werden (Warteschlange oder Parallelitätslimit).
|
|
|
|
Details zum technischen Payload-Mechanismus für das Mint/Casper-Backend (inklusive eines dabei gefundenen Casper-Bugs): `13-live-provisioning-boot.md`. Details zum Datenmodell (`iso_builds`): `09-data-model-v1.md`. `installationsmedium_konfiguration` (Kundenplattform-eigene Datenbank, WLAN-Zugangsdaten, gewählte Bereitstellungsvorlage) ist nicht Teil dieses provisioning-server-Datenmodells, siehe ADR-0011 zur Datenbanktrennung.
|
|
|
|
## Nachtrag 26.08.2026: Zeitbasierter ISO-Ablauf nach 7 Tagen
|
|
|
|
`cleanup_old_iso_builds()` (siehe oben) hat bewusst keine Zeitdimension —
|
|
sie greift ausschließlich, wenn dieselbe Organisation erneut baut. Eine
|
|
Organisation, die nur einmal baut und ihr Medium nie erneuert, behielt den
|
|
Download dadurch faktisch für immer. Ergänzt (nicht ersetzt) um eine feste
|
|
Ablauffrist: `iso_builds` bekommt eine neue Spalte `expires_at` (Migration
|
|
`0021_iso_build_expiry.sql`), die `update_iso_build()` bei erfolgreichem
|
|
Abschluss auf `finished_at + 7 Tage` setzt (fest ab Fertigstellung, nicht
|
|
verlängerbar durch erneuten Download — bewusst einfach gehalten). Ein
|
|
periodischer Sweep (`run_iso_build_expiry_sweep()`, Daemon-Thread,
|
|
stündlich, gestartet über `@app.on_event("startup")`) löscht Datei + Zeile
|
|
jedes `completed`-Builds mit abgelaufenem `expires_at`, unabhängig davon,
|
|
ob die Organisation je erneut baut.
|
|
|
|
Bewusst getrennt von der Enrollment-Session-Gültigkeit
|
|
(`enrollment_sessions.expires_at`/`max_devices`, „Aufladen"-Flow oben): der
|
|
ISO-Ablauf betrifft nur die **Downloadbarkeit der Datei**, nicht die
|
|
Berechtigung, sich mit einer bereits verteilten ISO zu registrieren — ein
|
|
abgelaufener Download-Link zwingt nicht zu einer neuen Registrierungsrunde
|
|
für Geräte, die die ISO schon vor dem Ablauf heruntergeladen haben.
|
|
|
|
`iso_build_row_to_dict()` und damit alle iso-builds-Endpunkte geben
|
|
`expires_at` jetzt mit aus; Kundenplattform zeigt es auf
|
|
`/installationsmedium` als Hinweistext beim Download-Button an.
|
|
|
|
Live auf anode verifiziert: Migration eingespielt, ein Testbuild mit
|
|
künstlich zurückdatiertem `expires_at` per SQL angelegt, Sweep-Funktion
|
|
manuell aufgerufen (nicht die volle Stunde abgewartet) — Datei und
|
|
Datenbankzeile wurden korrekt entfernt, ein nicht abgelaufener Build blieb
|
|
unangetastet.
|