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:
parent
d8d803fb48
commit
c918edb093
@ -843,7 +843,8 @@ async def iexec_check(request: Request, row_id: int, db=Depends(get_db)):
|
|||||||
@router.post("/patching/iexec/snapshot/{row_id}")
|
@router.post("/patching/iexec/snapshot/{row_id}")
|
||||||
async def iexec_snapshot(request: Request, row_id: int, db=Depends(get_db)):
|
async def iexec_snapshot(request: Request, row_id: int, db=Depends(get_db)):
|
||||||
"""Step 2 — prend un snapshot vCenter pour 1 row éligible Linux.
|
"""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."""
|
Réutilise quickwin_snapshot_service.snapshot_server."""
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
if not user:
|
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")
|
PROD_PREFIXES = ("vp", "sp", "lp")
|
||||||
branch = "prod" if prefix in PROD_PREFIXES else "hprod"
|
branch = "prod" if prefix in PROD_PREFIXES else "hprod"
|
||||||
|
|
||||||
# Nom snapshot : <intervenant>_<YYYY-MM-DD>_avant_patch
|
# Nom snapshot : <user_connecté>_<YYYY-MM-DD>_<HH-MM>_avant_patch
|
||||||
intervenant = (row.intervenant or "patcheur").strip().replace(" ", "_")
|
# On utilise le username du JWT (sub) — login immutable, traçable par compte AD.
|
||||||
today = datetime.now().strftime("%Y-%m-%d")
|
# Heure ajoutée pour éviter collision si on patche le même serveur 2x dans la journée.
|
||||||
snap_name = f"{intervenant}_{today}_avant_patch"
|
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
|
# 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
|
# qui peut être faux en base). Si plus tard on a un cas où la VM porte un
|
||||||
|
|||||||
@ -26,9 +26,13 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
# Formats gérés par les outils SecOps SANEF :
|
# Formats gérés par les outils SecOps SANEF :
|
||||||
# PatchCenter (web) : `<auteur>_YYYY-MM-DD_avant_patch`
|
# PatchCenter v2 : `<user>_YYYY-MM-DD_HH-MM_avant_patch` (user = login JWT, depuis 2026-05-07)
|
||||||
# Sanef Patch Manager : `SLPM_<auteur>_YYYYMMDD_HHMM`
|
# PatchCenter v1 : `<auteur>_YYYY-MM-DD_avant_patch` (legacy, basé sur intervenant)
|
||||||
SNAP_PATCHCENTER_RE = re.compile(
|
# .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$"
|
r"^(?P<author>[A-Za-z0-9_\-\.]+)_(?P<date>\d{4}-\d{2}-\d{2})_avant_patch$"
|
||||||
)
|
)
|
||||||
SNAP_SLPM_RE = re.compile(
|
SNAP_SLPM_RE = re.compile(
|
||||||
@ -38,11 +42,16 @@ SNAP_SLPM_RE = re.compile(
|
|||||||
|
|
||||||
def _detect_snap_origin(name: str):
|
def _detect_snap_origin(name: str):
|
||||||
"""Renvoie (origin, author) ou (None, None) si format inconnu.
|
"""Renvoie (origin, author) ou (None, None) si format inconnu.
|
||||||
origin in {'patchcenter', 'slpm'} ; author = préfixe utilisateur."""
|
origin in {'patchcenter', 'slpm'} ; author = préfixe utilisateur.
|
||||||
m = SNAP_PATCHCENTER_RE.match(name or "")
|
PatchCenter v2 (avec heure) testé en premier pour ne pas matcher v1."""
|
||||||
|
n = name or ""
|
||||||
|
m = SNAP_PATCHCENTER_V2_RE.match(n)
|
||||||
if m:
|
if m:
|
||||||
return "patchcenter", m.group("author")
|
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:
|
if m:
|
||||||
return "slpm", m.group("author")
|
return "slpm", m.group("author")
|
||||||
return None, None
|
return None, None
|
||||||
|
|||||||
@ -58,8 +58,8 @@
|
|||||||
<div class="col-span-3">
|
<div class="col-span-3">
|
||||||
<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 (login = auteur)</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><auteur>_YYYY-MM-DD_avant_patch</code> ou .exe <code>SLPM_<auteur>_YYYYMMDD_HHMM</code>)</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>
|
||||||
<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>
|
||||||
@ -113,7 +113,7 @@
|
|||||||
const fVc = document.getElementById('f-vcenter');
|
const fVc = document.getElementById('f-vcenter');
|
||||||
const fAuthor = document.getElementById('f-author');
|
const fAuthor = document.getElementById('f-author');
|
||||||
const fOnlyMine = document.getElementById('f-only-mine');
|
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 fMinAge = document.getElementById('f-min-age');
|
||||||
const btnRefresh = document.getElementById('btn-refresh');
|
const btnRefresh = document.getElementById('btn-refresh');
|
||||||
const btnDelete = document.getElementById('btn-delete');
|
const btnDelete = document.getElementById('btn-delete');
|
||||||
@ -139,15 +139,14 @@
|
|||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const author = (fAuthor.value || '').trim().toLowerCase();
|
const author = (fAuthor.value || '').trim().toLowerCase();
|
||||||
const onlyMine = fOnlyMine.checked;
|
const onlyMine = fOnlyMine.checked;
|
||||||
const pcOnly = fPcFormat.checked;
|
const pcOnly = fOnlyPc.checked;
|
||||||
const minAge = parseFloat(fMinAge.value) || 0;
|
const minAge = parseFloat(fMinAge.value) || 0;
|
||||||
return allSnaps.filter(s => {
|
return allSnaps.filter(s => {
|
||||||
// Format géré (PatchCenter OU SLPM .exe)
|
// Snapshots PatchCenter uniquement (origin === 'patchcenter')
|
||||||
if (pcOnly && !s.is_managed_format) return false;
|
if (pcOnly && s.origin !== 'patchcenter') return false;
|
||||||
// Mes snapshots uniquement (auteur doit etre present et matcher)
|
// Mes snapshots uniquement
|
||||||
if (onlyMine) {
|
if (onlyMine) {
|
||||||
if (!author) {
|
if (!author) {
|
||||||
// pas d'auteur indique -> on prend uniquement ceux dont l'auteur est connu
|
|
||||||
if (!s.author) return false;
|
if (!s.author) return false;
|
||||||
} else if (!s.author || s.author.toLowerCase() !== author) {
|
} else if (!s.author || s.author.toLowerCase() !== author) {
|
||||||
return false;
|
return false;
|
||||||
@ -290,8 +289,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
[fAuthor, fMinAge, fOnlyMine, fPcFormat].forEach(el => el.addEventListener('input', render));
|
[fAuthor, fMinAge, fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('input', render));
|
||||||
[fOnlyMine, fPcFormat].forEach(el => el.addEventListener('change', render));
|
[fOnlyMine, fOnlyPc].forEach(el => el.addEventListener('change', render));
|
||||||
|
|
||||||
selAll.addEventListener('change', () => {
|
selAll.addEventListener('change', () => {
|
||||||
tbody.querySelectorAll('.row-cb').forEach(cb => {
|
tbody.querySelectorAll('.row-cb').forEach(cb => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user