diff --git a/adr/0014-self-service-iso-bau-orchestrierung.md b/adr/0014-self-service-iso-bau-orchestrierung.md new file mode 100644 index 0000000..b7e8d41 --- /dev/null +++ b/adr/0014-self-service-iso-bau-orchestrierung.md @@ -0,0 +1,32 @@ +# 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. diff --git a/architecture/09-data-model-v1.md b/architecture/09-data-model-v1.md index 95dbd3b..67b9bd9 100644 --- a/architecture/09-data-model-v1.md +++ b/architecture/09-data-model-v1.md @@ -25,6 +25,7 @@ Die konkrete SQL-Implementierung und das Migrationssystem werden getrennt von di - Auftragszuweisung - Auftragsereignis - Activation Code +- ISO Build - Network Profile - Secret Reference @@ -462,6 +463,31 @@ Beziehungen: - gehört zu genau einer Organization +## ISO Build + +Ein ISO Build beschreibt einen Bauvorgang einer personalisierten Kunden-Installations-ISO (siehe ADR-0014, `13-live-provisioning-boot.md`). Läuft als Hintergrund-Thread in `provisioning-server`, diese Tabelle ist dessen Status-Speicher. + +Nach jedem erfolgreichen Bau werden ältere ISO Builds derselben Organisation aufgeräumt (begrenzter Plattenplatz auf anode) — ein ISO Build ist damit kein dauerhaftes Archiv, sondern Status des jeweils aktuellen bzw. zuletzt versuchten Baus. + +Attribute: + +- id +- organization_id +- status (`pending`, `running`, `completed`, `failed`) +- output_filename (optional) +- error_message (optional) +- created_at +- started_at (optional) +- finished_at (optional) + +Beziehungen: + +- gehört zu genau einer Organization + +Constraints: + +- status ist eine der vier definierten Ausprägungen + ## Relationales Schema v1 ### organizations @@ -842,6 +868,27 @@ Constraints: - code ist Primärschlüssel +### iso_builds + +Spalten: + +- id +- organization_id +- status (Standard: pending) +- output_filename (nullable) +- error_message (nullable) +- created_at +- started_at (nullable) +- finished_at (nullable) + +Beziehungen: + +- organization_id referenziert organizations.id + +Constraints: + +- status ist eine von pending/running/completed/failed (CHECK-Constraint) + ## Beziehungskonsequenzen Wird eine Organization gelöscht, dürfen zugehörige Devices nicht implizit mitgelöscht werden. @@ -898,6 +945,7 @@ Für die relationale Umsetzung gilt daher: - device_merkmal_events.device_id → ON DELETE CASCADE - device_merkmal_events.merkmal_id → ON DELETE RESTRICT - activation_codes.organization_id → ON DELETE CASCADE +- iso_builds.organization_id → ON DELETE CASCADE ## ID-Erzeugung diff --git a/architecture/13-live-provisioning-boot.md b/architecture/13-live-provisioning-boot.md index bb27303..942ae5f 100644 --- a/architecture/13-live-provisioning-boot.md +++ b/architecture/13-live-provisioning-boot.md @@ -211,6 +211,20 @@ Noch nicht umgesetzt: `installer.sh` liest `tuxflotte.mode` noch nicht aus und d --- +## Mint/Casper-Payload-Mechanismus (04.–07.08.2026) + +Der Fokus verschob sich nach den obigen Fedora-Untersuchungen auf Mint als primäres Backend (siehe ADR-0009-Folgeentscheidung) — Mints Live-/Install-Medium basiert auf `casper`/`live-build`, nicht auf Fedoras `dracut`. Für das `/updates`-Payload-Prinzip (siehe „WLAN-Zugangsdaten" oben — dieselbe Grundidee: Dateien direkt auf dem Boot-Medium, außerhalb des komprimierten Images, landen beim Live-Boot automatisch im Root) existiert bei Casper kein Äquivalent zu Dracuts `20-apply-live-updates.sh`-Hook. + +**Falsche Annahme widerlegt:** Casper merged `/updates` vom ISO **nicht** automatisch ins gebootete Live-System — das war eine unverifizierte, projektweite Annahme (in Analogie zum Fedora/Dracut-Verhalten), die sich bei der tatsächlichen Umsetzung als falsch herausstellte. + +Die funktionierende Lösung ist ein `casper-bottom`-Initrd-Hook (`initrd-hooks/casper-bottom/99casperboot`, eingebunden über denselben Cpio-Schicht-Mechanismus wie das Kexec-Preseed in Phase 1, siehe `scripts/lib/initrd.sh`) — mit einer nicht offensichtlichen Einschränkung: Ein **neuer** Hook-Skriptname wird von Casper nie ausgeführt. `initramfs-tools` erzeugt beim ISO-Bau eine `ORDER`-Datei (`scripts/casper-bottom/ORDER`) mit der fest verdrahteten Aufrufreihenfolge aller `casper-bottom`-Skripte; ein per Cpio-Schicht neu hinzugefügtes Skript taucht darin nicht auf und wird deshalb ignoriert, selbst wenn es im initramfs physisch vorhanden ist (real bestätigt durch Extraktion des tatsächlich gebauten `initrd.lz` mit `unmkinitramfs` — die erste Version dieses Fixes ist an genau diesem Punkt gescheitert). Zuverlässig funktioniert stattdessen das **Überschreiben eines bereits in `ORDER` referenzierten Pfads** — spätere Cpio-Schichten überschreiben Dateiinhalte am selben Pfad. Der Fix überschreibt deshalb das originale `99casperboot` (das im Original nur `touch /run/.casper-boot` macht) und hängt den `/updates`-Kopiervorgang dort an; `99casperboot` ist laut `ORDER` das letzte `casper-bottom`-Skript überhaupt, garantiert nach `05mountpoints` (das `/cdrom` nach `/root/cdrom` umhängt, Voraussetzung für den Kopiervorgang). + +Dieselbe Erkenntnis (bestehenden, bereits referenzierten Pfad überschreiben statt neuen Hook-Namen einführen) gilt vermutlich für jeden künftigen Casper-Payload-Mechanismus, nicht nur für diesen einen Fix. + +Im Rahmen desselben Self-Service-ISO-Ausbauschritts entstand die eigentliche Bau-Orchestrierung (Hintergrund-Thread in `provisioning-server`, `iso_builds`-Statustabelle, Kundenportal-Self-Service-UI) — siehe ADR-0014. + +--- + ## Zusammenfassung Die Live-ISO ersetzt das direkte Booten in Anaconda durch eine vorgeschaltete, frei gestaltbare Live-Sitzung, in der `installer.sh` läuft.