Gestuftes Ausrollen: Ausrollstufe je Organisation (Migration 0059)
Der Check-in liefert Organisationen mit Stufe "stabil" die Adresse des freigegebenen Repos (ansible-content-stabil), "vorab" die von main. Neue Organisationen sind stabil; Default Lab, Liebherr und WLAN-Test vorab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
26e26e1b3b
commit
0c268e9088
2 files changed
+93
-4
No files matched your search
@@ -38,6 +38,16 @@ AGENT_POLL_INTERVAL_SECONDS = int(os.environ.get("TUXFLOTTE_AGENT_POLL_INTERVAL"
|
||||
DEBUG_MODE_POLL_INTERVAL_SECONDS = 60
|
||||
DEBUG_MODE_DURATION_HOURS = 6
|
||||
ANSIBLE_CONTENT_REPO = os.environ.get("TUXFLOTTE_ANSIBLE_CONTENT_REPO", "")
|
||||
# Gestuftes Ausrollen (13.10.2026, Migration 0059, ADR-0043): Organisationen
|
||||
# mit Ausrollstufe "stabil" bekommen im Check-in die Adresse des
|
||||
# freigegebenen Repos, "vorab" die des Entwicklungsstands. Zwei Repos statt
|
||||
# zwei Zweige, weil der Agent immer den Standardzweig der gelieferten Adresse
|
||||
# zieht und sich auf installierten Geräten nicht aktualisiert. Ohne eigene
|
||||
# Vorgabe gilt die Namenskonvention <repo>-stabil.
|
||||
ANSIBLE_CONTENT_REPO_STABIL = os.environ.get("TUXFLOTTE_ANSIBLE_CONTENT_REPO_STABIL", "") or (
|
||||
re.sub(r"(\.git)?$", r"-stabil\1", ANSIBLE_CONTENT_REPO, count=1) if ANSIBLE_CONTENT_REPO else ""
|
||||
)
|
||||
AUSROLLSTUFEN = ("stabil", "vorab")
|
||||
ADMIN_TOKEN = os.environ.get("TUXFLOTTE_ADMIN_TOKEN", "")
|
||||
KUNDENPLATTFORM_TOKEN = os.environ.get("TUXFLOTTE_KUNDENPLATTFORM_TOKEN", "")
|
||||
AGENT_REPORTABLE_EVENT_TYPES = {"applied", "apply_failed", "removed", "remove_failed"}
|
||||
@@ -3172,7 +3182,8 @@ def fetch_organizations():
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, created_at, kontakt_name, adresse, telefon, kontakt_email, notizen
|
||||
SELECT id, name, created_at, kontakt_name, adresse, telefon, kontakt_email, notizen,
|
||||
ausrollstufe
|
||||
FROM organizations ORDER BY name
|
||||
"""
|
||||
)
|
||||
@@ -3186,19 +3197,50 @@ def fetch_organizations():
|
||||
"telefon": telefon,
|
||||
"kontakt_email": kontakt_email,
|
||||
"notizen": notizen,
|
||||
"ausrollstufe": ausrollstufe,
|
||||
}
|
||||
for (
|
||||
org_id, name, created_at,
|
||||
kontakt_name, adresse, telefon, kontakt_email, notizen,
|
||||
kontakt_name, adresse, telefon, kontakt_email, notizen, ausrollstufe,
|
||||
) in cur.fetchall()
|
||||
]
|
||||
|
||||
def ansible_repo_fuer_geraet(device_id: str) -> str:
|
||||
"""Adresse des Rollen-Repos nach der Ausrollstufe der Organisation.
|
||||
Unbekanntes Gerät: der freigegebene Stand."""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT o.ausrollstufe FROM devices d
|
||||
JOIN organizations o ON o.id = d.organization_id
|
||||
WHERE d.id = %s
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
return ANSIBLE_CONTENT_REPO if row and row[0] == "vorab" else ANSIBLE_CONTENT_REPO_STABIL
|
||||
|
||||
|
||||
def set_ausrollstufe(organization_id: str, ausrollstufe: str) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"UPDATE organizations SET ausrollstufe = %s, updated_at = now() WHERE id = %s RETURNING id",
|
||||
(ausrollstufe, organization_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def fetch_organization(organization_id: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, created_at, kontakt_name, adresse, telefon, kontakt_email, notizen
|
||||
SELECT id, name, created_at, kontakt_name, adresse, telefon, kontakt_email, notizen,
|
||||
ausrollstufe
|
||||
FROM organizations WHERE id = %s
|
||||
""",
|
||||
(organization_id,),
|
||||
@@ -3217,6 +3259,7 @@ def fetch_organization(organization_id: str):
|
||||
"telefon": row[5],
|
||||
"kontakt_email": row[6],
|
||||
"notizen": row[7],
|
||||
"ausrollstufe": row[8],
|
||||
}
|
||||
|
||||
def create_organization(name: str):
|
||||
@@ -5650,7 +5693,7 @@ def agent_checkin(
|
||||
# behalten hat. Die neue "hostname"-Ansible-Rolle setzt ihn nur,
|
||||
# wenn er sich vom lokal tatsächlich gesetzten unterscheidet.
|
||||
"hostname": hostname,
|
||||
"ansible_repo": ANSIBLE_CONTENT_REPO,
|
||||
"ansible_repo": ansible_repo_fuer_geraet(payload.device_id),
|
||||
"poll_interval_seconds": effektives_intervall,
|
||||
}
|
||||
|
||||
@@ -5938,6 +5981,28 @@ def get_organization_endpoint(organization_id: str, authorization: str | None =
|
||||
return {"success": True, "organization": organization}
|
||||
|
||||
|
||||
class SetAusrollstufeRequest(BaseModel):
|
||||
ausrollstufe: str
|
||||
|
||||
|
||||
@app.put("/api/v1/organizations/{organization_id}/ausrollstufe")
|
||||
def put_ausrollstufe(
|
||||
organization_id: str,
|
||||
payload: SetAusrollstufeRequest,
|
||||
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 payload.ausrollstufe not in AUSROLLSTUFEN:
|
||||
return {"success": False, "error": "invalid_request", "message": "Unbekannte Ausrollstufe."}
|
||||
|
||||
if not set_ausrollstufe(organization_id, payload.ausrollstufe):
|
||||
return {"success": False, "error": "organization_not_found", "message": "Die Organisation wurde nicht gefunden."}
|
||||
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/dashboard")
|
||||
def get_customer_dashboard_endpoint(organization_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
|
||||
Reference in new issue
Block a user