From c918edb0937133be648c44d54dcc51b6e5bc9fc3 Mon Sep 17 00:00:00 2001 From: Admin MPCZ Date: Thu, 7 May 2026 20:58:54 +0200 Subject: [PATCH] feat(snapshots): nouveau format _YYYY-MM-DD_HH-MM_avant_patch + filtre PatchCenter only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probleme initial: nom snap base sur 'intervenant' (champ libre Excel modifiable) -> peu fiable pour identifier qui a cree le snap. De plus, sans heure dans le nom, collisions si meme serveur patche 2x dans la journee. Solution: - Format snap PatchCenter v2: _YYYY-MM-DD_HH-MM_avant_patch user = login JWT (sub) immutable, traçable cote AD HH-MM ajoute pour eviter collisions - Service: nouveau regex SNAP_PATCHCENTER_V2_RE (avec heure), v1 conservee pour les snapshots existants legacy - Router iexec_snapshot: utilise user.get('sub') au lieu de row.intervenant - UI: * Renomme checkbox 'Format gere' -> 'Snapshots PatchCenter uniquement' * Filtre origin === 'patchcenter' (exclut SLPM .exe par defaut) * Combine avec 'Mes snapshots' (author = login user) -> seulement TES snapshots PatchCenter visibles, parfait pour le cleanup post-patching --- app/routers/planning_import.py | 14 +++++++++----- app/services/snapshot_mgmt_service.py | 21 +++++++++++++++------ app/templates/snapshots.html | 19 +++++++++---------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/app/routers/planning_import.py b/app/routers/planning_import.py index 17e0575..99f587f 100644 --- a/app/routers/planning_import.py +++ b/app/routers/planning_import.py @@ -843,7 +843,8 @@ async def iexec_check(request: Request, row_id: int, db=Depends(get_db)): @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. + Nom snapshot : ___avant_patch + (user = login du compte connecté, traçable, immutable). Réutilise quickwin_snapshot_service.snapshot_server.""" user = get_current_user(request) if not user: @@ -880,10 +881,13 @@ async def iexec_snapshot(request: Request, row_id: int, db=Depends(get_db)): PROD_PREFIXES = ("vp", "sp", "lp") branch = "prod" if prefix in PROD_PREFIXES 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" + # Nom snapshot : ___avant_patch + # On utilise le username du JWT (sub) — login immutable, traçable par compte AD. + # Heure ajoutée pour éviter collision si on patche le même serveur 2x dans la journée. + snap_user = ((user.get("sub") or user.get("username") or "patcheur") + .strip().replace(" ", "_")) + snap_dt = datetime.now().strftime("%Y-%m-%d_%H-%M") + snap_name = f"{snap_user}_{snap_dt}_avant_patch" # On cherche la VM dans vCenter par son hostname (pas par s.vcenter_vm_name # qui peut être faux en base). Si plus tard on a un cas où la VM porte un diff --git a/app/services/snapshot_mgmt_service.py b/app/services/snapshot_mgmt_service.py index e51ecde..0dec007 100644 --- a/app/services/snapshot_mgmt_service.py +++ b/app/services/snapshot_mgmt_service.py @@ -26,9 +26,13 @@ except ImportError: # Formats gérés par les outils SecOps SANEF : -# PatchCenter (web) : `_YYYY-MM-DD_avant_patch` -# Sanef Patch Manager : `SLPM__YYYYMMDD_HHMM` -SNAP_PATCHCENTER_RE = re.compile( +# PatchCenter v2 : `_YYYY-MM-DD_HH-MM_avant_patch` (user = login JWT, depuis 2026-05-07) +# PatchCenter v1 : `_YYYY-MM-DD_avant_patch` (legacy, basé sur intervenant) +# .exe SLPM : `SLPM__YYYYMMDD_HHMM` +SNAP_PATCHCENTER_V2_RE = re.compile( + r"^(?P[A-Za-z0-9_\-\.]+)_(?P\d{4}-\d{2}-\d{2})_\d{2}-\d{2}_avant_patch$" +) +SNAP_PATCHCENTER_V1_RE = re.compile( r"^(?P[A-Za-z0-9_\-\.]+)_(?P\d{4}-\d{2}-\d{2})_avant_patch$" ) SNAP_SLPM_RE = re.compile( @@ -38,11 +42,16 @@ SNAP_SLPM_RE = re.compile( def _detect_snap_origin(name: str): """Renvoie (origin, author) ou (None, None) si format inconnu. - origin in {'patchcenter', 'slpm'} ; author = préfixe utilisateur.""" - m = SNAP_PATCHCENTER_RE.match(name or "") + origin in {'patchcenter', 'slpm'} ; author = préfixe utilisateur. + PatchCenter v2 (avec heure) testé en premier pour ne pas matcher v1.""" + n = name or "" + m = SNAP_PATCHCENTER_V2_RE.match(n) if m: return "patchcenter", m.group("author") - m = SNAP_SLPM_RE.match(name or "") + m = SNAP_PATCHCENTER_V1_RE.match(n) + if m: + return "patchcenter", m.group("author") + m = SNAP_SLPM_RE.match(n) if m: return "slpm", m.group("author") return None, None diff --git a/app/templates/snapshots.html b/app/templates/snapshots.html index 69f7556..f60e31c 100644 --- a/app/templates/snapshots.html +++ b/app/templates/snapshots.html @@ -58,8 +58,8 @@
- - + +
@@ -113,7 +113,7 @@ const fVc = document.getElementById('f-vcenter'); const fAuthor = document.getElementById('f-author'); const fOnlyMine = document.getElementById('f-only-mine'); - const fPcFormat = document.getElementById('f-pc-format'); + const fOnlyPc = document.getElementById('f-only-pc'); const fMinAge = document.getElementById('f-min-age'); const btnRefresh = document.getElementById('btn-refresh'); const btnDelete = document.getElementById('btn-delete'); @@ -139,15 +139,14 @@ function applyFilters() { const author = (fAuthor.value || '').trim().toLowerCase(); const onlyMine = fOnlyMine.checked; - const pcOnly = fPcFormat.checked; + const pcOnly = fOnlyPc.checked; const minAge = parseFloat(fMinAge.value) || 0; return allSnaps.filter(s => { - // Format géré (PatchCenter OU SLPM .exe) - if (pcOnly && !s.is_managed_format) return false; - // Mes snapshots uniquement (auteur doit etre present et matcher) + // Snapshots PatchCenter uniquement (origin === 'patchcenter') + if (pcOnly && s.origin !== 'patchcenter') return false; + // Mes snapshots uniquement if (onlyMine) { if (!author) { - // pas d'auteur indique -> on prend uniquement ceux dont l'auteur est connu if (!s.author) return false; } else if (!s.author || s.author.toLowerCase() !== author) { return false; @@ -290,8 +289,8 @@ } }); - [fAuthor, fMinAge, fOnlyMine, fPcFormat].forEach(el => el.addEventListener('input', render)); - [fOnlyMine, fPcFormat].forEach(el => el.addEventListener('change', render)); + [fAuthor, fMinAge, fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('input', render)); + [fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('change', render)); selAll.addEventListener('change', () => { tbody.querySelectorAll('.row-cb').forEach(cb => {