patchcenter/app/routers/snapshots.py
Admin MPCZ c63b3a9119 feat(snapshots): page de gestion snapshots VM (listing + filtre auteur/age + suppression)
Service snapshot_mgmt_service.py:
- list_snapshots(db, vcenter_filter_id=None): itere les vCenters actifs, walk recursif
  des snapshot tree de chaque VM, retourne (vcenter, vm, snap_name, snap_id, vm_moid,
  created_at, age_days, author, description, is_current)
- delete_snapshot(db, vcenter_id, vm_moid, snap_id, remove_children=False): supprime
  un snapshot par moRef, attend la fin de la task vCenter
- Auteur deduit du prefixe du nom (format PatchCenter '<auteur>_YYYY-MM-DD_<suffixe>')

Router /snapshots:
- GET /snapshots: page principale (filtres + table)
- POST /snapshots/list: AJAX scan vCenters, retourne JSON
- POST /snapshots/delete: AJAX suppression batch, double confirmation cote UI

Template snapshots.html:
- Filtres: vCenter, auteur, 'Mes snapshots uniquement' (preselectionne user courant),
  age min en jours (defaut 3)
- Table avec checkboxes, sel-all, badge age (vert <3j, orange 3-7j, rouge >7j)
- Bouton 'Charger/Refresh' (lazy load, eviter scan auto au pageload)
- Bouton 'Supprimer la selection' avec 2 confirmations + liste des snapshots
- Recharge auto apres suppression

Nav: lien '📸 Snapshots VM' ajoute dans le menu Patching.
2026-05-07 20:13:29 +02:00

122 lines
4.5 KiB
Python

"""Router /snapshots — listing + suppression des snapshots VM existants sur les vCenters."""
import logging
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, base_context
from ..config import APP_NAME
log = logging.getLogger("patchcenter.snapshots_router")
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
def _can_view_snaps(perms):
return can_view(perms, "campaigns") or can_view(perms, "planning") or can_view(perms, "settings")
def _can_delete_snaps(perms):
return can_edit(perms, "campaigns") or can_edit(perms, "planning") or can_edit(perms, "settings")
def _get_user_intervenant_name(db, user):
"""Retourne le nom utilisé comme préfixe dans les snapshots PatchCenter de cet utilisateur.
Priorité : settings 'patcher' > username."""
try:
from ..services.secrets_service import get_secret
patcher = (get_secret(db, "patcher") or "").strip()
if patcher:
return patcher
except Exception:
pass
return user.get("username", "")
@router.get("/snapshots", response_class=HTMLResponse)
async def snapshots_page(request: Request, 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_view_snaps(perms):
return RedirectResponse(url="/dashboard")
intervenant = _get_user_intervenant_name(db, user)
vcenters = db.execute(text(
"SELECT id, name, endpoint FROM vcenters WHERE is_active = true ORDER BY name"
)).fetchall()
ctx = base_context(request, db, user)
ctx.update({
"app_name": APP_NAME,
"intervenant_default": intervenant,
"vcenters": vcenters,
"can_delete": _can_delete_snaps(perms),
})
return templates.TemplateResponse("snapshots.html", ctx)
@router.post("/snapshots/list")
async def snapshots_list(request: Request, db=Depends(get_db),
vcenter_id: str = Form("")):
"""Endpoint AJAX : liste les snapshots (tous vCenters ou un seul)."""
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_snaps(perms):
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
from ..services.snapshot_mgmt_service import list_snapshots
vc_id = int(vcenter_id) if vcenter_id and vcenter_id.isdigit() else None
res = list_snapshots(db, vcenter_filter_id=vc_id)
return JSONResponse(res)
@router.post("/snapshots/delete")
async def snapshots_delete(request: Request, db=Depends(get_db)):
"""Endpoint AJAX : supprime une liste de snapshots.
Body JSON : [{vcenter_id, vm_moid, snap_id}, ...]"""
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_delete_snaps(perms):
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
try:
body = await request.json()
except Exception:
return JSONResponse({"ok": False, "msg": "Body JSON invalide"}, status_code=400)
items = body.get("items") if isinstance(body, dict) else None
if not isinstance(items, list) or not items:
return JSONResponse({"ok": False, "msg": "Aucun snapshot ciblé"}, status_code=400)
from ..services.snapshot_mgmt_service import delete_snapshot
results = []
n_ok = 0
for it in items:
try:
r = delete_snapshot(
db,
vcenter_id=int(it["vcenter_id"]),
vm_moid=str(it["vm_moid"]),
snap_id=str(it["snap_id"]),
remove_children=bool(it.get("remove_children", False)),
)
except Exception as e:
r = {"ok": False, "msg": f"Param error: {e}"}
r["vm_name"] = it.get("vm_name", "?")
r["snap_name"] = it.get("snap_name", "?")
results.append(r)
if r.get("ok"):
n_ok += 1
return JSONResponse({
"ok": n_ok == len(items),
"summary": f"{n_ok}/{len(items)} snapshots supprimés",
"results": results,
})