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})
|
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")
|
@router.post("/patching/import/{import_id}/delete")
|
||||||
async def import_delete(request: Request, import_id: int, db=Depends(get_db)):
|
async def import_delete(request: Request, import_id: int, db=Depends(get_db)):
|
||||||
user = get_current_user(request)
|
user = get_current_user(request)
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
<div class="flex items-center mb-4 gap-2 text-xs">
|
<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="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="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="text-gray-500">→</span>
|
||||||
<span class="px-3 py-1 rounded bg-cyber-border text-gray-500">3. Patch yum</span>
|
<span class="px-3 py-1 rounded bg-cyber-border text-gray-500">3. Patch yum</span>
|
||||||
</div>
|
</div>
|
||||||
@ -49,6 +49,7 @@
|
|||||||
<th class="text-left p-1">Disque</th>
|
<th class="text-left p-1">Disque</th>
|
||||||
<th class="text-left p-1">Satellite</th>
|
<th class="text-left p-1">Satellite</th>
|
||||||
<th class="text-left p-1">Verdict</th>
|
<th class="text-left p-1">Verdict</th>
|
||||||
|
<th class="text-left p-1">Snapshot</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="check-tbody">
|
<tbody id="check-tbody">
|
||||||
@ -65,9 +66,10 @@
|
|||||||
<td class="p-1 cell-disk text-gray-500">·</td>
|
<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-sat text-gray-500">·</td>
|
||||||
<td class="p-1 cell-overall text-gray-500">en attente</td>
|
<td class="p-1 cell-overall text-gray-500">en attente</td>
|
||||||
|
<td class="p-1 cell-snap text-gray-500">·</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% else %}
|
{% 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 %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -82,7 +84,7 @@
|
|||||||
<div class="flex justify-between items-center mt-4">
|
<div class="flex justify-between items-center mt-4">
|
||||||
<span id="run-summary" class="text-xs text-gray-400"></span>
|
<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>
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -172,6 +174,42 @@
|
|||||||
btnStep2.disabled = (okCount === 0);
|
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
|
// Click sur une ligne → afficher les détails
|
||||||
tbody.addEventListener('click', (ev) => {
|
tbody.addEventListener('click', (ev) => {
|
||||||
const tr = ev.target.closest('tr[data-row-id]');
|
const tr = ev.target.closest('tr[data-row-id]');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user