feat(patching/iexec B2): branchement snapshot vCenter - bouton Step 2 lance snapshot pour rows verdict OK, nom intervenant_YYYY-MM-DD_avant_patch, log audit dans patch_planning_row_log
This commit is contained in:
parent
b07a6816d4
commit
a6b98568f1
@ -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 : <intervenant>_<YYYY-MM-DD>_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 : <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"
|
||||
|
||||
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)
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<div class="flex items-center mb-4 gap-2 text-xs">
|
||||
<span class="px-3 py-1 rounded bg-cyber-yellow/20 text-cyber-yellow font-bold">1. Vérifications</span>
|
||||
<span class="text-gray-500">→</span>
|
||||
<span class="px-3 py-1 rounded bg-cyber-border text-gray-500">2. Snapshot</span>
|
||||
<span class="px-3 py-1 rounded bg-cyber-green/20 text-cyber-green font-bold">2. Snapshot</span>
|
||||
<span class="text-gray-500">→</span>
|
||||
<span class="px-3 py-1 rounded bg-cyber-border text-gray-500">3. Patch yum</span>
|
||||
</div>
|
||||
@ -49,6 +49,7 @@
|
||||
<th class="text-left p-1">Disque</th>
|
||||
<th class="text-left p-1">Satellite</th>
|
||||
<th class="text-left p-1">Verdict</th>
|
||||
<th class="text-left p-1">Snapshot</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="check-tbody">
|
||||
@ -65,9 +66,10 @@
|
||||
<td class="p-1 cell-disk text-gray-500">·</td>
|
||||
<td class="p-1 cell-sat text-gray-500">·</td>
|
||||
<td class="p-1 cell-overall text-gray-500">en attente</td>
|
||||
<td class="p-1 cell-snap text-gray-500">·</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="11" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||||
<tr><td colspan="12" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -82,7 +84,7 @@
|
||||
<div class="flex justify-between items-center mt-4">
|
||||
<span id="run-summary" class="text-xs text-gray-400"></span>
|
||||
<button id="btn-step2" class="btn-sm bg-cyber-green/20 text-cyber-green px-4 py-2 text-xs" disabled>
|
||||
→ Step 2 (snapshot) — à brancher
|
||||
→ Step 2 (snapshot vCenter)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -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 = <intervenant>_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 = '<span class="text-cyber-yellow">… snapshot</span>';
|
||||
try {
|
||||
const r = await fetch('/patching/iexec/snapshot/' + tr.dataset.rowId, {method:'POST'});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
okCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.detail||'') + '">✓ ' + escapeHTML(j.snap_name||'OK') + '</span>'
|
||||
+ ' <span class="text-[10px] text-gray-400">(' + escapeHTML(j.vcenter||'') + ')</span>';
|
||||
} else if (j.skipped) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-yellow">⚠ ' + escapeHTML(j.detail||'skip') + '</span>';
|
||||
} else {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||||
}
|
||||
tr._snapData = j;
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur réseau</span>';
|
||||
}
|
||||
}
|
||||
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]');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user