Bereitstellungsvorlagen: Bearbeiten, Loeschen, Nutzung, Distro aus Workspace
Liste liefert Geraete-/Medien-Anzahl, Verschluesselung, Secure Boot. Neue Endpunkte PUT/DELETE (Loeschen gesperrt solange Geraete oder Installationsmedien verweisen). backend_id optional - ohne Angabe gilt die Distro des Workspace (ADR-0038). Pruefung fuer Anlegen und Bearbeiten in _bereitstellungsvorlage_pruefen(). GUI-Runde 10/2026 Punkt 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b038350e72
commit
0561691f20
1 file changed
+141
-39
@@ -305,7 +305,8 @@ class ExtraPartitionRequest(BaseModel):
|
||||
|
||||
class CreateBereitstellungsvorlageRequest(BaseModel):
|
||||
workspace_id: str
|
||||
backend_id: str
|
||||
# Seit ADR-0038 optional: ohne Angabe gilt die Distro des Workspace.
|
||||
backend_id: str | None = None
|
||||
label: str
|
||||
is_default: bool = False
|
||||
disk_encryption: bool = False
|
||||
@@ -753,7 +754,9 @@ def fetch_templates(organization_id: str) -> list[dict]:
|
||||
bv.id, bv.label, bv.is_default,
|
||||
w.id, w.name,
|
||||
b.id, b.name, b.version,
|
||||
bv.partitioning
|
||||
bv.partitioning, bv.disk_encryption, bv.secure_boot_required,
|
||||
(SELECT count(*) FROM assignments a WHERE a.bereitstellungsvorlage_id = bv.id),
|
||||
(SELECT count(*) FROM enrollment_sessions es WHERE es.bereitstellungsvorlage_id = bv.id)
|
||||
FROM bereitstellungsvorlagen bv
|
||||
JOIN workspaces w ON w.id = bv.workspace_id
|
||||
JOIN backends b ON b.id = bv.backend_id
|
||||
@@ -772,6 +775,12 @@ def fetch_templates(organization_id: str) -> list[dict]:
|
||||
"backend": {"id": str(row[5]), "name": row[6], "version": row[7]},
|
||||
"is_default": row[2],
|
||||
"partitioning": row[8],
|
||||
"disk_encryption": row[9],
|
||||
"secure_boot_required": row[10],
|
||||
# GUI-Runde 09.10.2026 (Punkt 5): Nutzung - solange Geräte oder
|
||||
# Installationsmedien darauf verweisen, ist Löschen gesperrt.
|
||||
"geraete_anzahl": row[11],
|
||||
"medien_anzahl": row[12],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
@@ -6333,6 +6342,51 @@ def list_blueprints(authorization: str | None = Header(default=None)):
|
||||
return {"success": True, "blueprints": fetch_blueprints()}
|
||||
|
||||
|
||||
def _bereitstellungsvorlage_pruefen(payload) -> tuple[str | None, dict | None]:
|
||||
"""Gemeinsame Prüfung für Anlegen und Bearbeiten (GUI-Runde 09.10.2026).
|
||||
Rückgabe: (backend_id, fehler). Seit ADR-0038 hat ein Workspace genau
|
||||
eine Distro - fehlt backend_id, gilt die des Workspace."""
|
||||
|
||||
if not payload.label.strip():
|
||||
return None, {"success": False, "error": "invalid_request", "message": "Die Bezeichnung darf nicht leer sein."}
|
||||
if payload.target_disk_rank < 1:
|
||||
return None, {"success": False, "error": "invalid_request", "message": "target_disk_rank muss mindestens 1 sein."}
|
||||
if payload.scheme not in ("single", "custom"):
|
||||
return None, {"success": False, "error": "invalid_request", "message": "scheme muss 'single' oder 'custom' sein."}
|
||||
if payload.scheme == "custom":
|
||||
mountpoints = [p.mountpoint for p in payload.extra_partitions]
|
||||
if len(mountpoints) != len(set(mountpoints)):
|
||||
return None, {"success": False, "error": "invalid_request", "message": "extra_partitions enthält doppelte Mountpoints."}
|
||||
if sum(p.percent for p in payload.extra_partitions) >= 100:
|
||||
return None, {"success": False, "error": "invalid_request", "message": "Die Summe der Partitionsanteile muss unter 100 % bleiben."}
|
||||
for p in payload.extra_partitions:
|
||||
if not p.mountpoint.startswith("/"):
|
||||
return None, {"success": False, "error": "invalid_request", "message": f"Mountpoint '{p.mountpoint}' muss mit '/' beginnen."}
|
||||
if p.percent <= 0:
|
||||
return None, {"success": False, "error": "invalid_request", "message": f"Der Anteil für '{p.mountpoint}' muss positiv sein."}
|
||||
if p.filesystem not in ("ext4", "btrfs"):
|
||||
return None, {"success": False, "error": "invalid_request", "message": f"Nicht unterstütztes Dateisystem für '{p.mountpoint}': {p.filesystem}"}
|
||||
elif payload.extra_partitions:
|
||||
return None, {"success": False, "error": "invalid_request", "message": "Eigene Partitionen gibt es nur bei scheme='custom'."}
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT backend_id FROM workspaces WHERE id = %s", (payload.workspace_id,))
|
||||
ws_row = cur.fetchone()
|
||||
if ws_row is None:
|
||||
return None, {"success": False, "error": "workspace_not_found", "message": "Der Workspace wurde nicht gefunden."}
|
||||
workspace_backend = str(ws_row[0]) if ws_row[0] is not None else None
|
||||
backend_id = payload.backend_id or workspace_backend
|
||||
if backend_id is None:
|
||||
return None, {"success": False, "error": "invalid_request", "message": "Der Workspace hat keine Distro - bitte eine Distro angeben."}
|
||||
if workspace_backend is not None and workspace_backend != backend_id:
|
||||
return None, {
|
||||
"success": False, "error": "distro_konflikt",
|
||||
"message": "Der Workspace gehört zu einer anderen Distro als das Profil (ein Workspace hat genau eine Distro).",
|
||||
}
|
||||
return backend_id, None
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/bereitstellungsvorlagen")
|
||||
def list_bereitstellungsvorlagen(organization_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
@@ -6350,47 +6404,14 @@ def create_bereitstellungsvorlage_endpoint(
|
||||
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."}
|
||||
|
||||
if payload.target_disk_rank < 1:
|
||||
return {"success": False, "error": "invalid_request", "message": "target_disk_rank muss mindestens 1 sein."}
|
||||
|
||||
if payload.scheme not in ("single", "custom"):
|
||||
return {"success": False, "error": "invalid_request", "message": "scheme muss 'single' oder 'custom' sein."}
|
||||
|
||||
if payload.scheme == "custom":
|
||||
mountpoints = [p.mountpoint for p in payload.extra_partitions]
|
||||
if len(mountpoints) != len(set(mountpoints)):
|
||||
return {"success": False, "error": "invalid_request", "message": "extra_partitions enthält doppelte Mountpoints."}
|
||||
if sum(p.percent for p in payload.extra_partitions) >= 100:
|
||||
return {"success": False, "error": "invalid_request", "message": "Summe der extra_partitions-Prozente muss unter 100 bleiben."}
|
||||
for p in payload.extra_partitions:
|
||||
if not p.mountpoint.startswith("/"):
|
||||
return {"success": False, "error": "invalid_request", "message": f"Mountpoint '{p.mountpoint}' muss mit '/' beginnen."}
|
||||
if p.percent <= 0:
|
||||
return {"success": False, "error": "invalid_request", "message": f"percent für '{p.mountpoint}' muss positiv sein."}
|
||||
if p.filesystem not in ("ext4", "btrfs"):
|
||||
return {"success": False, "error": "invalid_request", "message": f"Nicht unterstütztes Dateisystem für '{p.mountpoint}': {p.filesystem}"}
|
||||
elif payload.extra_partitions:
|
||||
return {"success": False, "error": "invalid_request", "message": "extra_partitions ist nur bei scheme='custom' erlaubt."}
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT backend_id FROM workspaces WHERE id = %s", (payload.workspace_id,))
|
||||
ws_row = cur.fetchone()
|
||||
if ws_row is None:
|
||||
return {"success": False, "error": "workspace_not_found", "message": "Der Workspace wurde nicht gefunden."}
|
||||
if ws_row[0] is not None and str(ws_row[0]) != payload.backend_id:
|
||||
return {
|
||||
"success": False, "error": "distro_konflikt",
|
||||
"message": "Der Workspace gehört zu einer anderen Distro als das Profil (ein Workspace hat genau eine Distro).",
|
||||
}
|
||||
backend_id, fehler = _bereitstellungsvorlage_pruefen(payload)
|
||||
if fehler:
|
||||
return fehler
|
||||
|
||||
vorlage = create_bereitstellungsvorlage(
|
||||
organization_id,
|
||||
payload.workspace_id,
|
||||
payload.backend_id,
|
||||
backend_id,
|
||||
payload.label,
|
||||
payload.is_default,
|
||||
payload.disk_encryption,
|
||||
@@ -6404,6 +6425,87 @@ def create_bereitstellungsvorlage_endpoint(
|
||||
return {"success": True, "bereitstellungsvorlage": vorlage}
|
||||
|
||||
|
||||
def update_bereitstellungsvorlage(organization_id: str, vorlage_id: str, payload, backend_id: str) -> bool:
|
||||
partitioning = {
|
||||
"scheme": payload.scheme,
|
||||
"root_filesystem": payload.root_filesystem,
|
||||
"target_disk_rank": payload.target_disk_rank,
|
||||
"extra_partitions": [p.model_dump() for p in payload.extra_partitions],
|
||||
}
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
if payload.is_default:
|
||||
cur.execute(
|
||||
"UPDATE bereitstellungsvorlagen SET is_default = FALSE WHERE organization_id = %s AND is_default AND id <> %s",
|
||||
(organization_id, vorlage_id),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE bereitstellungsvorlagen
|
||||
SET workspace_id = %s, backend_id = %s, label = %s, is_default = %s,
|
||||
disk_encryption = %s, partitioning = %s, secure_boot_required = %s, updated_at = now()
|
||||
WHERE id = %s AND organization_id = %s
|
||||
""",
|
||||
(
|
||||
payload.workspace_id, backend_id, payload.label.strip(), payload.is_default,
|
||||
payload.disk_encryption, Jsonb(partitioning), payload.secure_boot_required,
|
||||
vorlage_id, organization_id,
|
||||
),
|
||||
)
|
||||
return cur.rowcount == 1
|
||||
|
||||
|
||||
@app.put("/api/v1/organizations/{organization_id}/bereitstellungsvorlagen/{vorlage_id}")
|
||||
def update_bereitstellungsvorlage_endpoint(
|
||||
organization_id: str,
|
||||
vorlage_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."}
|
||||
|
||||
backend_id, fehler = _bereitstellungsvorlage_pruefen(payload)
|
||||
if fehler:
|
||||
return fehler
|
||||
if not update_bereitstellungsvorlage(organization_id, vorlage_id, payload, backend_id):
|
||||
return {"success": False, "error": "not_found", "message": "Die Bereitstellungsvorlage wurde nicht gefunden."}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.delete("/api/v1/organizations/{organization_id}/bereitstellungsvorlagen/{vorlage_id}")
|
||||
def delete_bereitstellungsvorlage_endpoint(
|
||||
organization_id: str,
|
||||
vorlage_id: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT (SELECT count(*) FROM assignments WHERE bereitstellungsvorlage_id = %s)
|
||||
+ (SELECT count(*) FROM enrollment_sessions WHERE bereitstellungsvorlage_id = %s)
|
||||
""",
|
||||
(vorlage_id, vorlage_id),
|
||||
)
|
||||
(verweise,) = cur.fetchone()
|
||||
if verweise:
|
||||
return {
|
||||
"success": False, "error": "in_benutzung",
|
||||
"message": "Die Vorlage wird noch von Geräten oder Installationsmedien verwendet und kann nicht gelöscht werden.",
|
||||
}
|
||||
cur.execute(
|
||||
"DELETE FROM bereitstellungsvorlagen WHERE id = %s AND organization_id = %s",
|
||||
(vorlage_id, organization_id),
|
||||
)
|
||||
if cur.rowcount != 1:
|
||||
return {"success": False, "error": "not_found", "message": "Die Bereitstellungsvorlage wurde nicht gefunden."}
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@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):
|
||||
|
||||
Reference in new issue
Block a user