diff --git a/app/routers/planning_import.py b/app/routers/planning_import.py index d206e2d..c6d8686 100644 --- a/app/routers/planning_import.py +++ b/app/routers/planning_import.py @@ -660,6 +660,77 @@ async def iexec_check(request: Request, row_id: int, db=Depends(get_db)): return JSONResponse({"ok": True, "row_id": row_id, **result}) +@router.post("/patching/iexec/snapshot/{row_id}") +async def iexec_snapshot(request: Request, row_id: int, db=Depends(get_db)): + """Step 2 — prend un snapshot vCenter pour 1 row éligible Linux. + Nom snapshot : __avant_patch. + Réutilise quickwin_snapshot_service.snapshot_server.""" + user = get_current_user(request) + if not user: + return JSONResponse({"ok": False, "detail": "Non authentifié"}, status_code=401) + perms = get_user_perms(db, user) + if not _can_import(perms): + return JSONResponse({"ok": False, "detail": "Permission refusée"}, status_code=403) + + row = db.execute(text(""" + SELECT r.id, r.asset_name, r.intervenant, r.environnement, r.os, r.is_eligible, + s.hostname, s.vcenter_vm_name + FROM patch_planning_import_rows r + LEFT JOIN servers s ON s.id = r.server_id + WHERE r.id = :id + """), {"id": row_id}).fetchone() + if not row: + return JSONResponse({"ok": False, "detail": "Ligne introuvable"}, status_code=404) + if not row.is_eligible: + return JSONResponse({"ok": False, "detail": "Ligne non éligible"}, status_code=400) + + os_str = str(row.os or "").lower() + if "windows" in os_str: + return JSONResponse({"ok": False, "detail": "OS Windows non géré"}, status_code=400) + + hostname = (row.hostname or row.asset_name or "").strip() + if not hostname: + return JSONResponse({"ok": False, "detail": "Pas de hostname"}, status_code=400) + + # Branche prod / hprod selon l'environnement Excel + env = str(row.environnement or "").lower() + branch = "prod" if env.startswith("prod") else "hprod" + + # Nom snapshot : __avant_patch + intervenant = (row.intervenant or "patcheur").strip().replace(" ", "_") + today = datetime.now().strftime("%Y-%m-%d") + snap_name = f"{intervenant}_{today}_avant_patch" + + vm_name = row.vcenter_vm_name or hostname + + from ..services.quickwin_snapshot_service import snapshot_server + result = snapshot_server(hostname, vm_name, branch, db, snap_name=snap_name) + # result = {ok, vcenter, detail, skipped?} + + # Audit log + try: + db.execute(text(""" + INSERT INTO patch_planning_row_log (row_id, action, details, performed_by) + VALUES (:rid, 'snapshot', :de, :uid) + """), {"rid": row_id, + "de": json.dumps({**result, "snap_name": snap_name, "branch": branch, + "vm_name": vm_name}, + ensure_ascii=False), + "uid": user.get("uid")}) + db.commit() + except Exception as e: + log_msg = f"audit log snapshot failed: {e}" + print(f"[iexec_snapshot] {log_msg}") + + result.update({ + "row_id": row_id, + "snap_name": snap_name, + "branch": branch, + "vm_name": vm_name, + }) + return JSONResponse(result) + + @router.post("/patching/import/{import_id}/delete") async def import_delete(request: Request, import_id: int, db=Depends(get_db)): user = get_current_user(request) diff --git a/app/templates/patching_iexec.html b/app/templates/patching_iexec.html index 09dc92a..d6b0790 100644 --- a/app/templates/patching_iexec.html +++ b/app/templates/patching_iexec.html @@ -16,7 +16,7 @@
1. Vérifications - 2. Snapshot + 2. Snapshot 3. Patch yum
@@ -49,6 +49,7 @@ Disque Satellite Verdict + Snapshot @@ -65,9 +66,10 @@ · · en attente + · {% else %} - Aucune ligne éligible. + Aucune ligne éligible. {% endfor %} @@ -82,7 +84,7 @@
@@ -172,6 +174,42 @@ btnStep2.disabled = (okCount === 0); }); + btnStep2.addEventListener('click', async () => { + const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]')); + const okTrs = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok'); + if (!okTrs.length) { alert('Aucune ligne avec verdict OK'); return; } + if (!confirm('Lancer snapshot vCenter pour ' + okTrs.length + ' serveur(s) ?\n(nom = _YYYY-MM-DD_avant_patch)')) return; + btnStep2.disabled = true; + btnRun.disabled = true; + let okCount = 0, koCount = 0; + for (const tr of okTrs) { + const cell = tr.querySelector('.cell-snap'); + cell.innerHTML = '… snapshot'; + try { + const r = await fetch('/patching/iexec/snapshot/' + tr.dataset.rowId, {method:'POST'}); + const j = await r.json(); + if (j.ok) { + okCount++; + cell.innerHTML = '✓ ' + escapeHTML(j.snap_name||'OK') + '' + + ' (' + escapeHTML(j.vcenter||'') + ')'; + } else if (j.skipped) { + koCount++; + cell.innerHTML = '⚠ ' + escapeHTML(j.detail||'skip') + ''; + } else { + koCount++; + cell.innerHTML = '✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + ''; + } + tr._snapData = j; + } catch(e) { + koCount++; + cell.innerHTML = '✗ erreur réseau'; + } + } + btnStep2.disabled = false; + btnRun.disabled = false; + summary.innerHTML += ' · Snapshot : ✓ ' + okCount + ' / ✗ ' + koCount; + }); + // Click sur une ligne → afficher les détails tbody.addEventListener('click', (ev) => { const tr = ev.target.closest('tr[data-row-id]');