feat(snapshots): support format SLPM (.exe Sanef Patch Manager) + colonne Origine

Probleme: tes 51 snapshots etaient au format SLPM_<auteur>_YYYYMMDD_HHMM (cree par le .exe)
non reconnu par PatchCenter qui n'attendait que le format <auteur>_YYYY-MM-DD_avant_patch.

- Service: nouveau regex SNAP_SLPM_RE + helper _detect_snap_origin retourne (origin, author)
- Champs ajoutes au snapshot: origin ('patchcenter'|'slpm'|None), is_managed_format
- Template:
  * Filtre 'Format gere uniquement' (renomme depuis 'PatchCenter uniquement')
  * Colonne 'Origine' avec badge: PatchCenter (bleu) / SLPM .exe (gris) / manuel (orange)
  * Colonne ajoutee dans header + cellules + colspan ajuste a 9
This commit is contained in:
Pierre & Lumière 2026-05-07 20:47:32 +02:00
parent 0a3fde36b7
commit d8d803fb48
2 changed files with 36 additions and 16 deletions

View File

@ -25,10 +25,27 @@ except ImportError:
log.warning("pyvmomi non disponible — listing snapshots impossible") log.warning("pyvmomi non disponible — listing snapshots impossible")
# Format strict PatchCenter : `<auteur>_YYYY-MM-DD_avant_patch` # Formats gérés par les outils SecOps SANEF :
SNAP_NAME_RE = re.compile( # PatchCenter (web) : `<auteur>_YYYY-MM-DD_avant_patch`
# Sanef Patch Manager : `SLPM_<auteur>_YYYYMMDD_HHMM`
SNAP_PATCHCENTER_RE = re.compile(
r"^(?P<author>[A-Za-z0-9_\-\.]+)_(?P<date>\d{4}-\d{2}-\d{2})_avant_patch$" r"^(?P<author>[A-Za-z0-9_\-\.]+)_(?P<date>\d{4}-\d{2}-\d{2})_avant_patch$"
) )
SNAP_SLPM_RE = re.compile(
r"^SLPM_(?P<author>[A-Za-z0-9_\-\.]+)_\d{8}_\d{4}$"
)
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 "")
if m:
return "patchcenter", m.group("author")
m = SNAP_SLPM_RE.match(name or "")
if m:
return "slpm", m.group("author")
return None, None
def _get_vcenter_creds(db): def _get_vcenter_creds(db):
@ -72,14 +89,9 @@ def _walk_snapshots(snapshot_list, vm, vcenter_name, vcenter_id, vm_moid, parent
created_iso = str(created) created_iso = str(created)
age_days = None age_days = None
# Auteur déduit du préfixe du nom (format strict PatchCenter) # Détection du format géré (PatchCenter ou .exe SLPM)
# `<auteur>_YYYY-MM-DD_avant_patch` origin, author = _detect_snap_origin(name)
author = None is_managed = origin is not None
is_pc_format = False
m = SNAP_NAME_RE.match(name)
if m:
author = m.group("author")
is_pc_format = True
yield { yield {
"vcenter_id": vcenter_id, "vcenter_id": vcenter_id,
@ -93,7 +105,9 @@ def _walk_snapshots(snapshot_list, vm, vcenter_name, vcenter_id, vm_moid, parent
"created_at": created_iso, "created_at": created_iso,
"age_days": round(age_days, 2) if age_days is not None else None, "age_days": round(age_days, 2) if age_days is not None else None,
"author": author, "author": author,
"is_patchcenter_format": is_pc_format, "origin": origin, # 'patchcenter' | 'slpm' | None
"is_managed_format": is_managed, # any des 2 formats SecOps
"is_patchcenter_format": origin == "patchcenter",
"is_current": bool(getattr(s, "id", None) and vm.snapshot and vm.snapshot.currentSnapshot "is_current": bool(getattr(s, "id", None) and vm.snapshot and vm.snapshot.currentSnapshot
and vm.snapshot.currentSnapshot._moId == snap_moid), and vm.snapshot.currentSnapshot._moId == snap_moid),
} }

View File

@ -59,7 +59,7 @@
<label class="text-xs text-gray-500">Auteur (préfixe nom)</label> <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"> <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-only-mine" checked> Mes snapshots uniquement</label>
<label class="text-xs text-gray-400 mt-1"><input type="checkbox" id="f-pc-format" checked> Format PatchCenter uniquement (<code>&lt;auteur&gt;_YYYY-MM-DD_avant_patch</code>)</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>
</div> </div>
<div class="col-span-3"> <div class="col-span-3">
<label class="text-xs text-gray-500">Âge minimum (jours)</label> <label class="text-xs text-gray-500">Âge minimum (jours)</label>
@ -97,6 +97,7 @@
<th class="p-2 text-left">vCenter</th> <th class="p-2 text-left">vCenter</th>
<th class="p-2 text-left">VM</th> <th class="p-2 text-left">VM</th>
<th class="p-2 text-left">Snapshot</th> <th class="p-2 text-left">Snapshot</th>
<th class="p-2 text-left">Origine</th>
<th class="p-2 text-left">Auteur</th> <th class="p-2 text-left">Auteur</th>
<th class="p-2 text-left">Créé le</th> <th class="p-2 text-left">Créé le</th>
<th class="p-2 text-left">Âge</th> <th class="p-2 text-left">Âge</th>
@ -141,8 +142,8 @@
const pcOnly = fPcFormat.checked; const pcOnly = fPcFormat.checked;
const minAge = parseFloat(fMinAge.value) || 0; const minAge = parseFloat(fMinAge.value) || 0;
return allSnaps.filter(s => { return allSnaps.filter(s => {
// Format PatchCenter uniquement // Format géré (PatchCenter OU SLPM .exe)
if (pcOnly && !s.is_patchcenter_format) return false; if (pcOnly && !s.is_managed_format) return false;
// Mes snapshots uniquement (auteur doit etre present et matcher) // Mes snapshots uniquement (auteur doit etre present et matcher)
if (onlyMine) { if (onlyMine) {
if (!author) { if (!author) {
@ -186,8 +187,8 @@
if (!rows.length) { if (!rows.length) {
const total = allSnaps.length; const total = allSnaps.length;
tbody.innerHTML = total === 0 tbody.innerHTML = total === 0
? '<tr><td colspan="8" class="p-4 text-center text-gray-500">Aucun snapshot dans le scan. Vérifie credentials vCenter.</td></tr>' ? '<tr><td colspan="9" class="p-4 text-center text-gray-500">Aucun snapshot dans le scan. Vérifie credentials vCenter.</td></tr>'
: `<tr><td colspan="8" class="p-4 text-center text-cyber-yellow">${total} snapshot(s) trouvé(s) mais tous filtrés. Ajuste les filtres ci-dessus.</td></tr>`; : `<tr><td colspan="9" class="p-4 text-center text-cyber-yellow">${total} snapshot(s) trouvé(s) mais tous filtrés. Ajuste les filtres ci-dessus.</td></tr>`;
updateDeleteBtn(); updateDeleteBtn();
return; return;
} }
@ -195,12 +196,17 @@
const authorCell = s.author const authorCell = s.author
? escapeHTML(s.author) ? escapeHTML(s.author)
: '<i class="text-gray-500">inconnu</i>'; // bypass escape pour ce litteral HTML : '<i class="text-gray-500">inconnu</i>'; // bypass escape pour ce litteral HTML
let originBadge;
if (s.origin === 'patchcenter') originBadge = '<span class="badge badge-blue">PatchCenter</span>';
else if (s.origin === 'slpm') originBadge = '<span class="badge badge-gray">SLPM (.exe)</span>';
else originBadge = '<span class="badge badge-orange">manuel</span>';
return ` return `
<tr class="ss-row border-b border-cyber-border/30" data-idx="${allSnaps.indexOf(s)}"> <tr class="ss-row border-b border-cyber-border/30" data-idx="${allSnaps.indexOf(s)}">
<td class="p-2"><input type="checkbox" class="row-cb"></td> <td class="p-2"><input type="checkbox" class="row-cb"></td>
<td class="p-2 font-mono">${escapeHTML(s.vcenter_name)}</td> <td class="p-2 font-mono">${escapeHTML(s.vcenter_name)}</td>
<td class="p-2 font-mono">${escapeHTML(s.vm_name)}</td> <td class="p-2 font-mono">${escapeHTML(s.vm_name)}</td>
<td class="p-2 font-mono">${escapeHTML(s.snap_name)}</td> <td class="p-2 font-mono">${escapeHTML(s.snap_name)}</td>
<td class="p-2">${originBadge}</td>
<td class="p-2">${authorCell}</td> <td class="p-2">${authorCell}</td>
<td class="p-2 font-mono text-[10px]">${escapeHTML(fmtDateFR(s.created_at))}</td> <td class="p-2 font-mono text-[10px]">${escapeHTML(fmtDateFR(s.created_at))}</td>
<td class="p-2">${ageBadge(s.age_days)}</td> <td class="p-2">${ageBadge(s.age_days)}</td>