platform-docs/adr/0014-self-service-iso-bau-orchestrierung.md
Thomas Stallinger eb828fcd06 ADR-0014 Nachtrag: zeitbasierter ISO-Ablauf nach 7 Tagen
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 09:55:10 +02:00

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.