feat(snapshots): nouveau format <user>_YYYY-MM-DD_HH-MM_avant_patch + filtre PatchCenter only

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: <user_jwt>_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
This commit is contained in:
Pierre & Lumière 2026-05-07 20:58:54 +02:00
parent d8d803fb48
commit c918edb093
3 changed files with 33 additions and 21 deletions

View File

@ -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 : <intervenant>_<YYYY-MM-DD>_avant_patch.
Nom snapshot : <user>_<YYYY-MM-DD>_<HH-MM>_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 : <intervenant>_<YYYY-MM-DD>_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 : <user_connecté>_<YYYY-MM-DD>_<HH-MM>_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

View File

@ -26,9 +26,13 @@ except ImportError:
# Formats gérés par les outils SecOps SANEF :
# PatchCenter (web) : `<auteur>_YYYY-MM-DD_avant_patch`
# Sanef Patch Manager : `SLPM_<auteur>_YYYYMMDD_HHMM`
SNAP_PATCHCENTER_RE = re.compile(
# PatchCenter v2 : `<user>_YYYY-MM-DD_HH-MM_avant_patch` (user = login JWT, depuis 2026-05-07)
# PatchCenter v1 : `<auteur>_YYYY-MM-DD_avant_patch` (legacy, basé sur intervenant)
# .exe SLPM : `SLPM_<auteur>_YYYYMMDD_HHMM`
SNAP_PATCHCENTER_V2_RE = re.compile(
r"^(?P<author>[A-Za-z0-9_\-\.]+)_(?P<date>\d{4}-\d{2}-\d{2})_\d{2}-\d{2}_avant_patch$"
)
SNAP_PATCHCENTER_V1_RE = re.compile(
r"^(?P<author>[A-Za-z0-9_\-\.]+)_(?P<date>\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

View File

@ -58,8 +58,8 @@
<div class="col-span-3">
<label class="text-xs text-gray-500">Auteur (préfixe nom)</label>
<input type="text" id="f-author" value="{{ intervenant_default }}" placeholder="ex: khalid" class="w-full">
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-mine" checked> Mes snapshots uniquement</label>
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-pc-format" checked> Formats gérés uniquement (PatchCenter <code>&lt;auteur&gt;_YYYY-MM-DD_avant_patch</code> ou .exe <code>SLPM_&lt;auteur&gt;_YYYYMMDD_HHMM</code>)</label>
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-mine" checked> Mes snapshots uniquement (login = auteur)</label>
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-only-pc" checked> Snapshots PatchCenter uniquement (exclut SLPM .exe et manuels)</label>
</div>
<div class="col-span-3">
<label class="text-xs text-gray-500">Âge minimum (jours)</label>
@ -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 => {