From fb175bf861e7608939ed9a0d09a414bcc2331c33 Mon Sep 17 00:00:00 2001 From: Thomas Stallinger Date: Fri, 7 Aug 2026 01:59:21 +0200 Subject: [PATCH] feat: Bereitstellungsvorlage-Erstellung per API (schliesst Phase-5-Luecke) Bislang gab es dafuer keinen API-/UI-Weg - Bereitstellungsvorlagen kamen nur per Seed-Migration (0005/0011) fuer die "Default Lab"-Organisation zustande. Eine wirklich neue Kundenorganisation hatte dadurch eine leere Vorlagenauswahl auf der Kundenplattform-Installationsmedium-Seite (in Phase 5 als offene Luecke dokumentiert). Neuer POST /api/v1/organizations/{id}/bereitstellungsvorlagen: waehlt Workspace + Backend, einfache Root-Dateisystem-Wahl (single-scheme, ext4/btrfs - ein custom-Schema mit extra_partitions bleibt Sache direkter DB-Pflege), optional Default/Diskverschluesselung/Secure-Boot. Beachtet den bestehenden Unique-Index (nur eine is_default=TRUE-Zeile je Organisation) durch expliziten Zwei-Schritte-Reset, gleiches Muster wie Migration 0011. Lokal end-to-end verifiziert: frische Organisation ohne jede Vorlage, zwei Vorlagen nacheinander angelegt (zweite mit is_default=true kippt die erste korrekt auf false), Partitioning/Diskverschluesselung/ Secure-Boot-Flags korrekt in der DB, Kundenplattform-Installationsmedium- Seite zeigt die neue Vorlage danach korrekt in der Auswahl. Co-Authored-By: Claude Sonnet 5 --- app.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/app.py b/app.py index 6e0a2d7..62a260f 100644 --- a/app.py +++ b/app.py @@ -116,6 +116,16 @@ class CreateWorkspaceRequest(BaseModel): organization_id: str | None = None +class CreateBereitstellungsvorlageRequest(BaseModel): + workspace_id: str + backend_id: str + label: str + is_default: bool = False + disk_encryption: bool = False + root_filesystem: str = "ext4" + secure_boot_required: bool = False + + class CreateEnrollmentSessionRequest(BaseModel): bereitstellungsvorlage_id: str max_devices: int @@ -467,6 +477,56 @@ def fetch_templates(organization_id: str) -> list[dict]: for row in rows ] +def create_bereitstellungsvorlage( + organization_id: str, + workspace_id: str, + backend_id: str, + label: str, + is_default: bool, + disk_encryption: bool, + root_filesystem: str, + secure_boot_required: bool, +) -> dict: + """ + Bislang gab es dafuer keinen API-/UI-Weg - Bereitstellungsvorlagen kamen + nur per Seed-Migration (0005/0011) fuer die "Default Lab"-Organisation + zustande (siehe Phase 5 des Self-Service-ISO-Plans, dort als offene + Luecke notiert). partitioning bekommt bewusst nur eine einfache + "root_filesystem"-Wahl im single-scheme (siehe Migration 0015 fuer die + volle Form) - ein custom-Schema mit extra_partitions ist Sache der + direkten DB-Pflege, nicht dieses Formulars. + """ + + vorlage_id = uuid4() + partitioning = {"scheme": "single", "root_filesystem": root_filesystem} + + with get_database_connection() as conn: + with conn.cursor() as cur: + if is_default: + # Der Unique-Index bereitstellungsvorlagen_one_default_per_org + # erlaubt nur eine is_default=TRUE-Zeile je Organisation - + # bestehenden Default zuerst explizit zuruecksetzen (gleiches + # Zwei-Schritte-Muster wie Migration 0011). + cur.execute( + "UPDATE bereitstellungsvorlagen SET is_default = FALSE WHERE organization_id = %s AND is_default", + (organization_id,), + ) + + cur.execute( + """ + INSERT INTO bereitstellungsvorlagen + (id, organization_id, workspace_id, backend_id, label, is_default, + disk_encryption, partitioning, secure_boot_required) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + """, + ( + vorlage_id, organization_id, workspace_id, backend_id, label, is_default, + disk_encryption, Jsonb(partitioning), secure_boot_required, + ), + ) + + return {"id": str(vorlage_id), "label": label, "is_default": is_default} + def require_service_token(authorization: str | None) -> bool: """ Akzeptiert entweder den Betreiber-Admin-Token oder das separate @@ -1899,6 +1959,32 @@ def list_bereitstellungsvorlagen(organization_id: str, authorization: str | None return {"success": True, "bereitstellungsvorlagen": fetch_templates(organization_id)} +@app.post("/api/v1/organizations/{organization_id}/bereitstellungsvorlagen") +def create_bereitstellungsvorlage_endpoint( + organization_id: str, + payload: CreateBereitstellungsvorlageRequest, + authorization: str | None = Header(default=None), +): + if not require_service_token(authorization): + return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."} + + if not payload.label.strip(): + return {"success": False, "error": "invalid_request", "message": "label darf nicht leer sein."} + + vorlage = create_bereitstellungsvorlage( + organization_id, + payload.workspace_id, + payload.backend_id, + payload.label, + payload.is_default, + payload.disk_encryption, + payload.root_filesystem, + payload.secure_boot_required, + ) + + return {"success": True, "bereitstellungsvorlage": vorlage} + + @app.get("/api/v1/organizations/{organization_id}/devices/unassigned") def list_unassigned_devices(organization_id: str, authorization: str | None = Header(default=None)): if not require_service_token(authorization):