diff --git a/app/routers/settings.py b/app/routers/settings.py index ba110aa..e55376a 100644 --- a/app/routers/settings.py +++ b/app/routers/settings.py @@ -139,6 +139,14 @@ def _build_context(db, user, saved=None): FROM teams_channels ORDER BY is_default DESC, name """)).fetchall() + server_clusters = db.execute(text(""" + SELECT c.id, c.name, c.description, c.reboot_strategy, c.is_active, c.created_at, + COUNT(s.id) AS server_count + FROM server_clusters c + LEFT JOIN servers s ON s.cluster_id = c.id + GROUP BY c.id, c.name, c.description, c.reboot_strategy, c.is_active, c.created_at + ORDER BY c.name + """)).fetchall() # Filtrer les sections visibles selon le role visible = {s: s in SECTION_ACCESS and role in SECTION_ACCESS[s]["visible"] for s in SECTIONS} @@ -151,6 +159,7 @@ def _build_context(db, user, saved=None): "q_tags": q_tags, "q_assets": q_assets, "q_linked": q_linked, "vcenters": vcenters, "saved": saved, "teams_channels": teams_channels, + "server_clusters": server_clusters, "visible": visible, "editable": editable, } @@ -346,6 +355,105 @@ async def teams_channel_test(request: Request, tc_id: int, db=Depends(get_db)): return JSONResponse({"ok": False, "msg": f"Erreur: {e}"}, status_code=500) +# --- Server clusters CRUD --- + +@router.post("/settings/server-cluster/add", response_class=HTMLResponse) +async def server_cluster_add(request: Request, db=Depends(get_db), + sc_name: str = Form(...), + sc_description: str = Form(""), + sc_reboot_strategy: str = Form("sequential")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_edit(perms, "settings"): + return RedirectResponse(url="/settings") + strat = sc_reboot_strategy if sc_reboot_strategy in ("sequential", "parallel") else "sequential" + try: + db.execute(text(""" + INSERT INTO server_clusters (name, description, reboot_strategy) + VALUES (:n, :d, :s) + """), {"n": sc_name.strip(), "d": (sc_description or None), "s": strat}) + db.commit() + except Exception as e: + db.rollback() + # Probablement violation unique sur name + logger.warning(f"server_cluster_add failed: {e}") + ctx = _build_context(db, user, saved="cluster") + ctx["request"] = request + return templates.TemplateResponse("settings.html", ctx) + + +@router.post("/settings/server-cluster/{sc_id}/edit", response_class=HTMLResponse) +async def server_cluster_edit(request: Request, sc_id: int, db=Depends(get_db), + sc_name: str = Form(...), + sc_description: str = Form(""), + sc_reboot_strategy: str = Form("sequential"), + sc_is_active: str = Form("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_edit(perms, "settings"): + return RedirectResponse(url="/settings") + strat = sc_reboot_strategy if sc_reboot_strategy in ("sequential", "parallel") else "sequential" + db.execute(text(""" + UPDATE server_clusters + SET name=:n, description=:d, reboot_strategy=:s, is_active=:ia + WHERE id=:id + """), {"n": sc_name.strip(), "d": (sc_description or None), "s": strat, + "ia": bool(sc_is_active), "id": sc_id}) + db.commit() + ctx = _build_context(db, user, saved="cluster") + ctx["request"] = request + return templates.TemplateResponse("settings.html", ctx) + + +@router.post("/settings/server-cluster/{sc_id}/delete", response_class=HTMLResponse) +async def server_cluster_delete(request: Request, sc_id: int, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_edit(perms, "settings"): + return RedirectResponse(url="/settings") + # Check qu'aucun serveur n'y est encore rattaché + cnt = db.execute(text("SELECT COUNT(*) FROM servers WHERE cluster_id=:id"), + {"id": sc_id}).scalar() or 0 + if cnt > 0: + # Désactivation seule (ne pas casser les FK et l'historique) + db.execute(text("UPDATE server_clusters SET is_active=false WHERE id=:id"), {"id": sc_id}) + else: + db.execute(text("DELETE FROM server_clusters WHERE id=:id"), {"id": sc_id}) + db.commit() + ctx = _build_context(db, user, saved="cluster") + ctx["request"] = request + return templates.TemplateResponse("settings.html", ctx) + + +@router.get("/settings/server-cluster/{sc_id}/servers") +async def server_cluster_servers(request: Request, sc_id: int, db=Depends(get_db)): + """Liste des serveurs du cluster avec leur cluster_order — JSON pour panneau détail.""" + from fastapi.responses import JSONResponse + user = get_current_user(request) + if not user: + return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401) + perms = get_user_perms(db, user) + if not can_view(perms, "settings"): + return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403) + rows = db.execute(text(""" + SELECT id, hostname, cluster_order, machine_type, etat + FROM servers + WHERE cluster_id = :id + ORDER BY cluster_order NULLS LAST, hostname + """), {"id": sc_id}).fetchall() + return JSONResponse({"ok": True, "count": len(rows), + "servers": [{"id": r.id, "hostname": str(r.hostname), + "order": r.cluster_order, + "machine_type": r.machine_type, + "etat": r.etat} for r in rows]}) + + # --- Secret individuel --- @router.post("/settings/secret/update", response_class=HTMLResponse) diff --git a/app/templates/settings.html b/app/templates/settings.html index 5b04dda..c0d7ae8 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -362,6 +362,99 @@ {% endif %} + + {% if visible.vsphere %} +
+ Les clusters permettent de patcher plusieurs serveurs liés (ex : DB master + slaves,
+ HAProxy + backends) avec un ordre de redémarrage. Le champ
+ cluster_order sur chaque serveur fixe la
+ séquence (1 = premier patché). Stratégie sequential = un par un (recommandé)
+ ou parallel = tous en même temps.
+
| Nom | +Description | +Stratégie | +# Serveurs | +Actif | + {% if editable.vsphere %}Actions | {% endif %} +
|---|---|---|---|---|---|
| {{ sc.name }} | +{{ sc.description or '-' }} | +{{ sc.reboot_strategy }} | ++ {{ sc.server_count }} + | +{{ 'Oui' if sc.is_active else 'Non' }} | + {% if editable.vsphere %} ++ + | + {% endif %} +
| Aucun cluster configuré. | |||||
| Ordre | Hostname | ' + + 'Type | État |
|---|