Workspace an der OE, Distro-Konfliktregel, Ziel-OE fuer Installationsmedien

- PUT /api/v1/organisationseinheiten/{id}/workspace: Workspace (sichtbar fuer
  die Organisation, mit Distro) an eine OE haengen, automatisch oder auf eine
  Version gepinnt.
- distro_konflikte(): simuliert OE-Workspace, OE umhaengen und Geraet
  verschieben und blockiert, wenn ein aktives Geraet danach einen Workspace
  mit anderer Distro bekaeme (Meldung nennt Geraete und OE).
- Sicherheitsnetz in geraete_basis_herkunft(): passt der OE-Workspace nicht
  zur Geraete-Distro, gilt der Workspace der Installation.
- Enrollment-Sessions mit Ziel-OE (geprueft gegen die Profil-Distro);
  aktivierte Geraete landen dort, resolve_template liefert die Blueprints der
  wirksamen Grundausstattung.
- Profil anlegen prueft, dass Workspace und Profil dieselbe Distro haben.
- OE-Liste mit eigenem und wirksamem (geerbtem) Workspace, Geraeteliste mit
  wirksamem Workspace und Distro.
Workspace-Creator neu, Schritt F.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-07 14:12:33 +02:00
1 parent 5187ad7be7
commit 1288097927
1 file changed
+355 -60
+355 -60
View File
@@ -320,6 +320,13 @@ class CreateEnrollmentSessionRequest(BaseModel):
max_devices: int
expires_at: str
neuinstallation_ohne_rueckfrage: bool = False
ziel_oe_id: str | None = None
class SetOeWorkspaceRequest(BaseModel):
workspace_id: str | None = None
version_id: str | None = None
automatisch: bool = True
class RechargeEnrollmentSessionRequest(BaseModel):
@@ -653,7 +660,7 @@ def find_enrollment_session(code: str):
"""
SELECT es.id, es.organization_id, o.name, es.bereitstellungsvorlage_id,
es.max_devices, es.devices_used, es.expires_at, es.revoked_at,
es.neuinstallation_ohne_rueckfrage
es.neuinstallation_ohne_rueckfrage, es.ziel_oe_id
FROM enrollment_sessions es
JOIN organizations o ON o.id = es.organization_id
WHERE es.id = %s
@@ -667,7 +674,7 @@ def find_enrollment_session(code: str):
(
session_id, organization_id, organization_name, bereitstellungsvorlage_id,
max_devices, devices_used, expires_at, revoked_at, neuinstallation_ohne_rueckfrage,
max_devices, devices_used, expires_at, revoked_at, neuinstallation_ohne_rueckfrage, ziel_oe_id,
) = row
if revoked_at is not None:
@@ -683,8 +690,25 @@ def find_enrollment_session(code: str):
"organization_name": organization_name,
"bereitstellungsvorlage_id": str(bereitstellungsvorlage_id),
"neuinstallation_ohne_rueckfrage": bool(neuinstallation_ohne_rueckfrage),
"ziel_oe_id": str(ziel_oe_id) if ziel_oe_id else None,
}
def geraet_in_ziel_oe(device_id: str, ziel_oe_id: str) -> None:
"""Gerät aus einem Installationsmedium mit Ziel-OE dorthin setzen
(Schritt F) - auch bei einer Neuinstallation eines bekannten Geräts:
wer mit diesem Stick installiert, will es dort haben. Die Distro-
Verträglichkeit wurde beim Erstellen des Mediums geprüft."""
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute(
"""
UPDATE devices SET oe_id = %s
WHERE id = %s AND organization_id = (SELECT organization_id FROM organisationseinheiten WHERE id = %s)
""",
(ziel_oe_id, device_id, ziel_oe_id),
)
def consume_enrollment_session(session_id: str) -> None:
with get_database_connection() as conn:
with conn.cursor() as cur:
@@ -1355,6 +1379,19 @@ def geraete_basis_herkunft(cur, device_id: str):
while aktuell is not None and aktuell in oes:
parent_id, workspace_id, version_id, automatisch = oes[aktuell]
if workspace_id is not None:
# Sicherheitsnetz (Schritt F): passt die Distro des
# OE-Workspace nicht zur Distro des Geräts (z. B. nach einer
# Neuinstallation mit anderer Distro), gilt der Workspace aus
# der Installation - statt still fehlender Apps. Zuweisungen
# und Verschiebungen verhindern diesen Fall sonst vorab
# (distro_konflikte()).
cur.execute("SELECT backend_id FROM workspaces WHERE id = %s", (workspace_id,))
ws_backend = cur.fetchone()[0]
if ws_backend is not None and ws_backend != backend_id:
return (
veroeffentlichte_version_id(cur, installations_workspace_id), backend_id,
{"art": "installation", "oe_konflikt": aktuell},
)
automatisch = bool(automatisch or version_id is None)
if automatisch:
version_id = veroeffentlichte_version_id(cur, workspace_id)
@@ -1645,22 +1682,46 @@ def fetch_organisationseinheiten(organization_id: str):
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, parent_id, name, ist_standard
FROM organisationseinheiten
WHERE organization_id = %s
ORDER BY name
SELECT oe.id, oe.parent_id, oe.name, oe.ist_standard,
oe.workspace_id, w.name, b.name, b.version, oe.automatisch_aktualisieren, v.nummer
FROM organisationseinheiten oe
LEFT JOIN workspaces w ON w.id = oe.workspace_id
LEFT JOIN backends b ON b.id = w.backend_id
LEFT JOIN workspace_versionen v ON v.id = oe.workspace_version_id
WHERE oe.organization_id = %s
ORDER BY oe.name
""",
(organization_id,),
)
return [
{
"id": str(oe_id),
"parent_id": str(parent_id) if parent_id is not None else None,
"name": name,
"ist_standard": ist_standard,
}
for oe_id, parent_id, name, ist_standard in cur.fetchall()
]
zeilen = cur.fetchall()
oes = [
{
"id": str(oe_id),
"parent_id": str(parent_id) if parent_id is not None else None,
"name": name,
"ist_standard": ist_standard,
# Workspace-Creator neu, Schritt F: eigener Workspace der OE
# (oder None) und der wirksame, ggf. geerbte.
"workspace": (
{"id": str(ws_id), "name": ws_name, "distro": f"{b_name} {b_version}",
"automatisch": automatisch or nummer is None, "version": nummer}
if ws_id else None
),
}
for oe_id, parent_id, name, ist_standard, ws_id, ws_name, b_name, b_version, automatisch, nummer in zeilen
]
nach_id = {oe["id"]: oe for oe in oes}
for oe in oes:
aktuell, gesehen = oe, set()
while aktuell is not None and aktuell["workspace"] is None and aktuell["id"] not in gesehen:
gesehen.add(aktuell["id"])
aktuell = nach_id.get(aktuell["parent_id"])
oe["wirksamer_workspace"] = (
{**aktuell["workspace"], "geerbt_von": None if aktuell is oe else aktuell["name"]}
if aktuell is not None and aktuell["workspace"] else None
)
return oes
def create_organisationseinheit(organization_id: str, name: str, parent_id: str | None):
"""
@@ -1734,6 +1795,10 @@ def update_organisationseinheit(oe_id: str, name: str, parent_id: str | None):
return None
current = parent_by_id.get(current)
konflikte = distro_konflikte(cur, str(organization_id), oe_parent={oe_id: parent_id})
if konflikte:
return {"konflikt": konflikt_meldung(konflikte)}
cur.execute(
"""
UPDATE organisationseinheiten
@@ -1897,15 +1962,139 @@ def move_device_to_oe(device_id: str, oe_id: str):
with conn.cursor() as cur:
cur.execute(
"""
UPDATE devices
SET oe_id = %s
WHERE id = %s
AND organization_id = (SELECT organization_id FROM organisationseinheiten WHERE id = %s)
RETURNING id
SELECT d.organization_id FROM devices d JOIN organisationseinheiten oe ON oe.id = %s
WHERE d.id = %s AND d.organization_id = oe.organization_id
""",
(oe_id, device_id, oe_id),
(oe_id, device_id),
)
return cur.fetchone() is not None
row = cur.fetchone()
if row is None:
return False
konflikte = distro_konflikte(cur, str(row[0]), geraet_oe={device_id: oe_id})
if konflikte:
return konflikt_meldung(konflikte)
cur.execute("UPDATE devices SET oe_id = %s WHERE id = %s", (oe_id, device_id))
return True
# --- Workspace an der OE und Distro-Konfliktregel (Schritt F, 07.10.2026) ---
def distro_konflikte(cur, organization_id, oe_workspace=None, oe_parent=None, geraet_oe=None) -> list[dict]:
"""
Simuliert eine Änderung und meldet jedes aktive Gerät der Organisation,
das danach einen Workspace mit anderer Distro bekäme als seine eigene.
oe_workspace: {oe_id: workspace_id oder None}, oe_parent: {oe_id:
parent_id}, geraet_oe: {device_id: oe_id}. Grundlage der Regel
"blockieren mit klarer Meldung" (ADR-0038) - eine andere Distro hieße
Neuinstallation.
"""
oe_workspace = {str(k): v for k, v in (oe_workspace or {}).items()}
oe_parent = {str(k): v for k, v in (oe_parent or {}).items()}
geraet_oe = {str(k): v for k, v in (geraet_oe or {}).items()}
cur.execute(
"SELECT id::text, parent_id::text, workspace_id::text, name FROM organisationseinheiten WHERE organization_id = %s",
(organization_id,),
)
oes = {}
for oe_id, parent_id, workspace_id, name in cur.fetchall():
oes[oe_id] = {
"parent": oe_parent.get(oe_id, parent_id),
"workspace": oe_workspace.get(oe_id, workspace_id) if oe_id in oe_workspace else workspace_id,
"name": name,
}
cur.execute("SELECT w.id::text, w.name, w.backend_id, b.name FROM workspaces w LEFT JOIN backends b ON b.id = w.backend_id")
workspaces = {wid: (name, backend_id, bname) for wid, name, backend_id, bname in cur.fetchall()}
cur.execute(
"""
SELECT d.id::text, COALESCE(d.hostname, d.id::text), d.oe_id::text, bv.backend_id, b.name
FROM devices d
JOIN assignments a ON a.device_id = d.id
JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
JOIN backends b ON b.id = bv.backend_id
WHERE d.organization_id = %s AND d.archived_at IS NULL AND d.deprovisioned_at IS NULL
""",
(organization_id,),
)
konflikte = []
for device_id, hostname, oe_id, backend_id, distro in cur.fetchall():
aktuell, gesehen = geraet_oe.get(device_id, oe_id), set()
while aktuell is not None and aktuell in oes and aktuell not in gesehen:
gesehen.add(aktuell)
ws = oes[aktuell]["workspace"]
if ws is not None:
ws_name, ws_backend, ws_distro = workspaces.get(str(ws), ("?", None, None))
if ws_backend is not None and ws_backend != backend_id:
konflikte.append({
"geraet": hostname, "geraet_distro": distro,
"oe": oes[aktuell]["name"], "workspace": ws_name, "workspace_distro": ws_distro,
})
break
aktuell = oes[aktuell]["parent"]
return konflikte
def konflikt_meldung(konflikte: list[dict]) -> str:
beispiele = "; ".join(
f"{k['geraet']} ({k['geraet_distro']}) bekäme „{k['workspace']}“ ({k['workspace_distro']}) über OE {k['oe']}"
for k in konflikte[:5]
)
rest = f" und {len(konflikte) - 5} weitere" if len(konflikte) > 5 else ""
return (
f"Nicht möglich – {len(konflikte)} Gerät(e) hätten danach einen Workspace mit anderer Distro, "
f"das ginge nur per Neuinstallation: {beispiele}{rest}."
)
def set_oe_workspace(oe_id: str, workspace_id: str | None, version_id: str | None, automatisch: bool):
"""Workspace (oder keinen) an eine OE hängen. Rückgabe (True, None) oder
(None, (fehlercode, nachricht))."""
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT organization_id FROM organisationseinheiten WHERE id = %s", (oe_id,))
row = cur.fetchone()
if row is None:
return None, ("not_found", "OE wurde nicht gefunden.")
organization_id = str(row[0])
if workspace_id is not None:
sichtbar = {w["id"] for w in fetch_available_workspaces_for_organization(organization_id)}
if workspace_id not in sichtbar:
return None, ("workspace_nicht_verfuegbar", "Dieser Workspace steht der Organisation nicht zur Verfügung.")
cur.execute("SELECT backend_id FROM workspaces WHERE id = %s", (workspace_id,))
if cur.fetchone()[0] is None:
return None, ("ohne_distro", "Der Workspace hat noch keine Distro und kann nicht zugewiesen werden.")
if not automatisch:
if not version_id:
return None, ("version_fehlt", "Bitte eine Version wählen oder „automatisch aktualisieren“ setzen.")
cur.execute(
"SELECT 1 FROM workspace_versionen WHERE id = %s AND workspace_id = %s AND status = 'veroeffentlicht'",
(version_id, workspace_id),
)
if cur.fetchone() is None:
return None, ("version_ungueltig", "Die gewählte Version gehört nicht zu diesem Workspace.")
konflikte = distro_konflikte(cur, organization_id, oe_workspace={oe_id: workspace_id})
if konflikte:
return None, ("distro_konflikt", konflikt_meldung(konflikte))
cur.execute(
"""
UPDATE organisationseinheiten
SET workspace_id = %s, workspace_version_id = %s, automatisch_aktualisieren = %s
WHERE id = %s
""",
(
workspace_id,
None if (workspace_id is None or automatisch) else version_id,
True if workspace_id is None else automatisch,
oe_id,
),
)
return True, None
def fetch_gruppen(organization_id: str):
with get_database_connection() as conn:
@@ -2143,44 +2332,56 @@ def fetch_devices_for_organization(organization_id: str):
cur.execute(
f"""
SELECT d.id, d.hostname, d.device_fingerprint, d.agent_last_checkin,
d.deprovisioned_at, d.oe_id, oe.name, w.name, {GERAETE_HEALTH_SPALTEN_SQL}
d.deprovisioned_at, d.oe_id, oe.name, w.name, {GERAETE_HEALTH_SPALTEN_SQL}, be.name
FROM devices d
LEFT JOIN organisationseinheiten oe ON oe.id = d.oe_id
LEFT JOIN assignments a ON a.device_id = d.id
LEFT JOIN bereitstellungsvorlagen bv ON bv.id = a.bereitstellungsvorlage_id
LEFT JOIN workspaces w ON w.id = bv.workspace_id
LEFT JOIN backends be ON be.id = bv.backend_id
WHERE d.organization_id = %s AND d.archived_at IS NULL
ORDER BY d.hostname NULLS LAST, d.created_at
""",
(organization_id,),
)
return [
{
"id": str(device_id),
"hostname": hostname,
"device_fingerprint": fingerprint,
"agent_last_checkin": (
last_checkin.isoformat() if last_checkin is not None else None
),
"deprovisioned_at": (
deprovisioned_at.isoformat() if deprovisioned_at is not None else None
),
"oe_id": str(oe_id) if oe_id is not None else None,
"oe_name": oe_name,
# Install-time zugewiesener Workspace (GUI-Umbau
# 10.09.2026, Geräteliste-Spalte) - bei einer nachträglichen
# Gerätegruppen-Workspace-Übersteuerung (ADR-0028) weicht
# der tatsächlich wirksame Katalog davon ab, für den
# genauen Stand siehe der Auftragskatalog-Seite des Geräts.
"workspace_name": workspace_name,
**_health_felder_aus_row(*health_spalten),
}
for (
device_id, hostname, fingerprint, last_checkin, deprovisioned_at, oe_id, oe_name,
workspace_name,
*health_spalten,
) in cur.fetchall()
]
zeilen = cur.fetchall()
# Wirksamer Workspace (Schritt F): über OE oder Installation,
# wie ihn der Agent anwendet - nicht mehr nur der der Installation.
wirksam = {}
for zeile in zeilen:
basis = geraete_basis(cur, str(zeile[0]))
if basis is not None and basis[0] is not None:
cur.execute(
"SELECT w.name, v.nummer FROM workspace_versionen v JOIN workspaces w ON w.id = v.workspace_id WHERE v.id = %s",
(basis[0],),
)
wirksam[zeile[0]] = cur.fetchone()
return [
{
"id": str(device_id),
"hostname": hostname,
"device_fingerprint": fingerprint,
"agent_last_checkin": (
last_checkin.isoformat() if last_checkin is not None else None
),
"deprovisioned_at": (
deprovisioned_at.isoformat() if deprovisioned_at is not None else None
),
"oe_id": str(oe_id) if oe_id is not None else None,
"oe_name": oe_name,
"workspace_name": wirksam[device_id][0] if device_id in wirksam else workspace_name,
"workspace_version": wirksam[device_id][1] if device_id in wirksam else None,
"distro": rest[-1],
**_health_felder_aus_row(*rest[:-1]),
}
for (
device_id, hostname, fingerprint, last_checkin, deprovisioned_at, oe_id, oe_name,
workspace_name,
*rest,
) in zeilen
]
def fetch_all_devices():
"""
@@ -4160,26 +4361,71 @@ def fetch_enrollment_sessions(organization_id: str):
) in cur.fetchall()
]
def ziel_oe_pruefen(cur, organization_id: str, ziel_oe_id: str, bereitstellungsvorlage_id: str) -> str | None:
"""Passt die Ziel-OE zum Installationsprofil? Fehlermeldung oder None.
Gilt in der OE (auch geerbt) ein Workspace, muss das Profil dieselbe
Distro installieren."""
cur.execute(
"SELECT 1 FROM organisationseinheiten WHERE id = %s AND organization_id = %s",
(ziel_oe_id, organization_id),
)
if cur.fetchone() is None:
return "Die Ziel-OE gehört nicht zu dieser Organisation."
cur.execute(
"SELECT bv.backend_id, b.name FROM bereitstellungsvorlagen bv JOIN backends b ON b.id = bv.backend_id WHERE bv.id = %s",
(bereitstellungsvorlage_id,),
)
profil = cur.fetchone()
if profil is None:
return "Das Installationsprofil wurde nicht gefunden."
cur.execute("SELECT id, parent_id, workspace_id FROM organisationseinheiten WHERE organization_id = %s", (organization_id,))
oes = {oe: (parent, ws) for oe, parent, ws in cur.fetchall()}
aktuell = UUID(str(ziel_oe_id))
while aktuell is not None and aktuell in oes:
parent, ws = oes[aktuell]
if ws is not None:
cur.execute(
"SELECT w.name, w.backend_id, b.name FROM workspaces w LEFT JOIN backends b ON b.id = w.backend_id WHERE w.id = %s",
(ws,),
)
ws_name, ws_backend, ws_distro = cur.fetchone()
if ws_backend is not None and ws_backend != profil[0]:
return (
f"Die Ziel-OE nutzt „{ws_name}“ ({ws_distro}), das Installationsprofil installiert aber "
f"{profil[1]}. Bitte ein {ws_distro}-Profil oder eine andere OE wählen."
)
return None
aktuell = parent
return None
def create_enrollment_session(
organization_id: str, bereitstellungsvorlage_id: str, max_devices: int, expires_at: str,
neuinstallation_ohne_rueckfrage: bool = False,
neuinstallation_ohne_rueckfrage: bool = False, ziel_oe_id: str | None = None,
):
"""Rückgabe: ({"id": ...}, None) oder (None, fehlermeldung)."""
session_id = uuid4()
with get_database_connection() as conn:
with conn.cursor() as cur:
if ziel_oe_id:
fehler = ziel_oe_pruefen(cur, organization_id, ziel_oe_id, bereitstellungsvorlage_id)
if fehler:
return None, fehler
cur.execute(
"""
INSERT INTO enrollment_sessions
(id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at,
neuinstallation_ohne_rueckfrage)
VALUES (%s, %s, %s, %s, %s, %s)
neuinstallation_ohne_rueckfrage, ziel_oe_id)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""",
(session_id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at,
neuinstallation_ohne_rueckfrage),
neuinstallation_ohne_rueckfrage, ziel_oe_id or None),
)
return {"id": str(session_id)}
return {"id": str(session_id)}, None
def revoke_enrollment_session(session_id: str):
with get_database_connection() as conn:
@@ -4638,6 +4884,8 @@ def activate(payload: ActivationRequest, request: Request):
if enrollment_session is not None:
consume_enrollment_session(enrollment_session["session_id"])
if enrollment_session.get("ziel_oe_id"):
geraet_in_ziel_oe(device["id"], enrollment_session["ziel_oe_id"])
write_log({
"timestamp": datetime.now(timezone.utc).isoformat(),
@@ -4747,8 +4995,6 @@ def resolve_template(template_id: str, payload: ResolveRequest):
disk_encryption, partitioning, secure_boot_required,
) = template_row
blueprints = version_blueprints(cur, veroeffentlichte_version_id(cur, workspace_id), backend_id)
cur.execute(
"""
INSERT INTO assignments (id, device_id, bereitstellungsvorlage_id)
@@ -4759,6 +5005,24 @@ def resolve_template(template_id: str, payload: ResolveRequest):
(uuid4(), payload.device_id, template_id),
)
# Schritt F: dieselbe Grundausstattung, die der Agent später
# anwendet - liegt das Gerät (z. B. über die Ziel-OE des Mediums)
# in einer OE mit Workspace, gilt deren Workspace, sonst wie
# bisher der Workspace des Profils.
basis = geraete_basis(cur, payload.device_id)
if basis is not None:
version_id, _backend = basis
blueprints = version_blueprints(cur, version_id, backend_id)
cur.execute(
"SELECT w.key FROM workspace_versionen v JOIN workspaces w ON w.id = v.workspace_id WHERE v.id = %s",
(version_id,),
)
key_row = cur.fetchone()
if key_row:
workspace_key = key_row[0]
else:
blueprints = version_blueprints(cur, veroeffentlichte_version_id(cur, workspace_id), backend_id)
return {
"success": True,
"runtime_blueprint": {
@@ -5906,6 +6170,18 @@ def create_bereitstellungsvorlage_endpoint(
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).",
}
vorlage = create_bereitstellungsvorlage(
organization_id,
payload.workspace_id,
@@ -5948,10 +6224,12 @@ def create_enrollment_session_endpoint(
if not require_service_token(authorization):
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
session = create_enrollment_session(
session, fehler = create_enrollment_session(
organization_id, payload.bereitstellungsvorlage_id, payload.max_devices, payload.expires_at,
payload.neuinstallation_ohne_rueckfrage,
payload.neuinstallation_ohne_rueckfrage, payload.ziel_oe_id,
)
if fehler:
return {"success": False, "error": "ziel_oe_konflikt", "message": fehler}
return {"success": True, "enrollment_session": session}
@@ -6131,6 +6409,17 @@ def create_organisationseinheit_endpoint(
return {"success": True, "organisationseinheit": oe}
@app.put("/api/v1/organisationseinheiten/{oe_id}/workspace")
def set_oe_workspace_endpoint(oe_id: str, payload: SetOeWorkspaceRequest, authorization: str | None = Header(default=None)):
if not require_service_token(authorization):
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
_ok, fehler = set_oe_workspace(oe_id, payload.workspace_id, payload.version_id, payload.automatisch)
if fehler:
return {"success": False, "error": fehler[0], "message": fehler[1]}
return {"success": True}
@app.patch("/api/v1/organisationseinheiten/{oe_id}")
def update_organisationseinheit_endpoint(
oe_id: str,
@@ -6142,6 +6431,9 @@ def update_organisationseinheit_endpoint(
oe = update_organisationseinheit(oe_id, payload.name, payload.parent_id)
if oe is not None and "konflikt" in oe:
return {"success": False, "error": "distro_konflikt", "message": oe["konflikt"]}
if oe is None:
return {
"success": False,
@@ -6219,7 +6511,10 @@ def move_device_to_oe_endpoint(device_id: str, payload: MoveDeviceOeRequest, aut
if not require_service_token(authorization):
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
if not move_device_to_oe(device_id, payload.oe_id):
ergebnis = move_device_to_oe(device_id, payload.oe_id)
if isinstance(ergebnis, str):
return {"success": False, "error": "distro_konflikt", "message": ergebnis}
if not ergebnis:
return {
"success": False,
"error": "invalid_move",