feat(patching/iexec B3.4+B3.5): pre-capture services+ports + post-compare avant/apres avec rapport diff (scripts wiki SANEF, push base64) - workflow 3a/3b/3c/3d sequentiel
This commit is contained in:
parent
b2f04c247f
commit
e29ecff949
@ -794,6 +794,68 @@ async def iexec_yum_dryrun(request: Request, row_id: int, db=Depends(get_db)):
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@router.post("/patching/iexec/pre-capture/{row_id}")
|
||||
async def iexec_pre_capture(request: Request, row_id: int, db=Depends(get_db)):
|
||||
"""Step 3b — capture services + ports avant patch (wiki SANEF)."""
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "detail": "Non authentifié"}, status_code=401)
|
||||
perms = get_user_perms(db, user)
|
||||
row, err = _common_iexec_row_check(row_id, db, user, perms)
|
||||
if err:
|
||||
return err
|
||||
hostname = (row.hostname or row.asset_name).strip()
|
||||
|
||||
from ..services.patch_run_service import pre_patch_capture
|
||||
result = pre_patch_capture(hostname)
|
||||
|
||||
try:
|
||||
db.execute(text("""
|
||||
INSERT INTO patch_planning_row_log (row_id, action, details, performed_by)
|
||||
VALUES (:rid, 'pre_patch_capture', :de, :uid)
|
||||
"""), {"rid": row_id,
|
||||
"de": json.dumps({k: v for k, v in result.items() if k != "stdout"},
|
||||
ensure_ascii=False),
|
||||
"uid": user.get("uid")})
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(f"[iexec_pre_capture] audit log failed: {e}")
|
||||
|
||||
result["row_id"] = row_id
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@router.post("/patching/iexec/post-compare/{row_id}")
|
||||
async def iexec_post_compare(request: Request, row_id: int, db=Depends(get_db)):
|
||||
"""Step 3d — compare services+ports avant/après patch + rapport (wiki SANEF)."""
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "detail": "Non authentifié"}, status_code=401)
|
||||
perms = get_user_perms(db, user)
|
||||
row, err = _common_iexec_row_check(row_id, db, user, perms)
|
||||
if err:
|
||||
return err
|
||||
hostname = (row.hostname or row.asset_name).strip()
|
||||
|
||||
from ..services.patch_run_service import post_patch_compare
|
||||
result = post_patch_compare(hostname)
|
||||
|
||||
try:
|
||||
db.execute(text("""
|
||||
INSERT INTO patch_planning_row_log (row_id, action, details, performed_by)
|
||||
VALUES (:rid, 'post_patch_compare', :de, :uid)
|
||||
"""), {"rid": row_id,
|
||||
"de": json.dumps({k: v for k, v in result.items() if k != "stdout"},
|
||||
ensure_ascii=False),
|
||||
"uid": user.get("uid")})
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
print(f"[iexec_post_compare] audit log failed: {e}")
|
||||
|
||||
result["row_id"] = row_id
|
||||
return JSONResponse(result)
|
||||
|
||||
|
||||
@router.post("/patching/iexec/yum-update/{row_id}")
|
||||
async def iexec_yum_update(request: Request, row_id: int, db=Depends(get_db)):
|
||||
"""Step 3 — vrai patch : `sudo -n yum update -y --exclude=...`."""
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
"""Service exécution patch yum :
|
||||
- pre_patch_capture() : capture services + ports avant patch (wiki SANEF)
|
||||
- yum_dryrun() : `sudo -n yum update --assumeno --exclude=...`
|
||||
- yum_update() : `sudo -n yum update -y --exclude=...`
|
||||
Gère la résolution DNS, ouverture SSH (réutilise _resolve/_connect du
|
||||
realtime_audit_service), validation des excludes (anti-injection shell),
|
||||
capture de la sortie + heuristique de résumé du plan yum.
|
||||
- post_patch_compare() : compare services/ports avant/après + rapport
|
||||
Réutilise _resolve/_connect du realtime_audit_service.
|
||||
"""
|
||||
import base64
|
||||
import logging
|
||||
import re
|
||||
from typing import Dict, Any, List
|
||||
@ -136,3 +137,176 @@ def yum_update(hostname: str, excludes_raw) -> Dict[str, Any]:
|
||||
"stdout_tail": r["stdout"][-5000:],
|
||||
"stderr": r["stderr"][:500] if r["stderr"] else "",
|
||||
}
|
||||
|
||||
|
||||
# ─── B3.4 + B3.5 — Pre/Post patch capture (scripts du wiki SANEF) ─────────
|
||||
|
||||
PRE_PATCH_SCRIPT = r"""#!/bin/bash
|
||||
# Généré par PatchCenter — capture pré-patching (wiki SANEF "Patch Linux")
|
||||
HOSTNAME=$(hostname)
|
||||
SNAPSHOT_DIR="/tmp"
|
||||
sudo -n systemctl list-units --type=service --state=running --no-pager \
|
||||
| awk '{print $1}' | grep '\.service$' \
|
||||
> ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt
|
||||
SVC=$(wc -l < ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt)
|
||||
echo "OK services_avant=${SVC} -> ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt"
|
||||
sudo -n ss -tlnup > ${SNAPSHOT_DIR}/secops_ports_avant_${HOSTNAME}.txt
|
||||
sudo -n ss -tlnup | awk 'NR>1 && $7 != "" {
|
||||
match($7, /users:\(\("([^"]+)"/, arr)
|
||||
split($5, addr, ":")
|
||||
port = addr[length(addr)]
|
||||
if (arr[1] != "" && port+0 < 32768) print port, arr[1]
|
||||
}' | sort -u > ${SNAPSHOT_DIR}/secops_ports_detail_avant_${HOSTNAME}.txt
|
||||
PORTS=$(grep -c LISTEN ${SNAPSHOT_DIR}/secops_ports_avant_${HOSTNAME}.txt || echo 0)
|
||||
echo "OK ports_avant=${PORTS} -> ${SNAPSHOT_DIR}/secops_ports_avant_${HOSTNAME}.txt"
|
||||
"""
|
||||
|
||||
POST_PATCH_SCRIPT = r"""#!/bin/bash
|
||||
# Généré par PatchCenter — comparaison post-patching (wiki SANEF "Patch Linux")
|
||||
HOSTNAME=$(hostname)
|
||||
SNAPSHOT_DIR="/tmp"
|
||||
RAPPORT="/tmp/rapport_patching_${HOSTNAME}_$(date +%Y%m%d_%H%M).txt"
|
||||
sudo -n systemctl list-units --type=service --state=running --no-pager \
|
||||
| awk '{print $1}' | grep '\.service$' \
|
||||
> ${SNAPSHOT_DIR}/secops_services_apres_${HOSTNAME}.txt
|
||||
DISPARUS_SVC=$(comm -23 \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt) \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_services_apres_${HOSTNAME}.txt) \
|
||||
| grep -v "user@")
|
||||
APPARUS_SVC=$(comm -13 \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt) \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_services_apres_${HOSTNAME}.txt) \
|
||||
| grep -Ev "setroubleshootd|user@")
|
||||
{
|
||||
echo "=== Rapport patching ${HOSTNAME} - $(date '+%Y-%m-%d %H:%M') ==="
|
||||
echo
|
||||
echo "--- SERVICES ---"
|
||||
echo "Avant : $(wc -l < ${SNAPSHOT_DIR}/secops_services_avant_${HOSTNAME}.txt) | Après : $(wc -l < ${SNAPSHOT_DIR}/secops_services_apres_${HOSTNAME}.txt)"
|
||||
if [ -z "$DISPARUS_SVC" ]; then echo "OK_services_disparus=0"; else echo "KO_services_disparus:"; echo "$DISPARUS_SVC"; fi
|
||||
if [ -z "$APPARUS_SVC" ]; then echo "OK_services_apparus=0"; else echo "WARN_services_apparus:"; echo "$APPARUS_SVC"; fi
|
||||
} | tee ${RAPPORT}
|
||||
sudo -n ss -tlnup > ${SNAPSHOT_DIR}/secops_ports_apres_${HOSTNAME}.txt
|
||||
sudo -n ss -tlnup | awk 'NR>1 && $7 != "" {
|
||||
match($7, /users:\(\("([^"]+)"/, arr)
|
||||
split($5, addr, ":")
|
||||
port = addr[length(addr)]
|
||||
if (arr[1] != "" && port+0 < 32768) print port, arr[1]
|
||||
}' | sort -u > ${SNAPSHOT_DIR}/secops_ports_detail_apres_${HOSTNAME}.txt
|
||||
PORTS_DISPARUS=$(comm -23 \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_ports_detail_avant_${HOSTNAME}.txt) \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_ports_detail_apres_${HOSTNAME}.txt))
|
||||
PORTS_APPARUS=$(comm -13 \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_ports_detail_avant_${HOSTNAME}.txt) \
|
||||
<(sort ${SNAPSHOT_DIR}/secops_ports_detail_apres_${HOSTNAME}.txt))
|
||||
{
|
||||
echo
|
||||
echo "--- PORTS ---"
|
||||
echo "Avant : $(grep -c LISTEN ${SNAPSHOT_DIR}/secops_ports_avant_${HOSTNAME}.txt) | Après : $(grep -c LISTEN ${SNAPSHOT_DIR}/secops_ports_apres_${HOSTNAME}.txt)"
|
||||
if [ -z "$PORTS_DISPARUS" ]; then echo "OK_ports_disparus=0"; else echo "KO_ports_disparus:"; echo "$PORTS_DISPARUS"; fi
|
||||
if [ -z "$PORTS_APPARUS" ]; then echo "OK_ports_apparus=0"; else echo "WARN_ports_apparus:"; echo "$PORTS_APPARUS"; fi
|
||||
echo
|
||||
echo "Rapport : ${RAPPORT}"
|
||||
} | tee -a ${RAPPORT}
|
||||
"""
|
||||
|
||||
|
||||
def _push_and_run(client, remote_path: str, script_content: str, timeout: int) -> Dict[str, Any]:
|
||||
"""Pousse le script (encode base64) puis l'exécute. Retourne {rc, stdout, stderr}."""
|
||||
b64 = base64.b64encode(script_content.encode("utf-8")).decode("ascii")
|
||||
cmd = (f"echo '{b64}' | base64 -d > {remote_path} && "
|
||||
f"chmod +x {remote_path} && bash {remote_path} 2>&1")
|
||||
return _exec(client, cmd, timeout)
|
||||
|
||||
|
||||
def _parse_capture_report(stdout: str) -> Dict[str, Any]:
|
||||
"""Parse les marqueurs OK_/KO_/WARN_ du script post-patch."""
|
||||
parsed = {
|
||||
"services_disparus": [], "services_apparus": [],
|
||||
"ports_disparus": [], "ports_apparus": [],
|
||||
"services_avant": None, "services_apres": None,
|
||||
"ports_avant": None, "ports_apres": None,
|
||||
}
|
||||
section = None # 'svc_dis', 'svc_app', 'port_dis', 'port_app'
|
||||
for ln in stdout.splitlines():
|
||||
s = ln.strip()
|
||||
if s.startswith("KO_services_disparus"): section = "svc_dis"; continue
|
||||
if s.startswith("WARN_services_apparus"): section = "svc_app"; continue
|
||||
if s.startswith("KO_ports_disparus"): section = "port_dis"; continue
|
||||
if s.startswith("WARN_ports_apparus"): section = "port_app"; continue
|
||||
if s.startswith("OK_") or s.startswith("---") or not s:
|
||||
section = None
|
||||
continue
|
||||
if section == "svc_dis": parsed["services_disparus"].append(s)
|
||||
elif section == "svc_app": parsed["services_apparus"].append(s)
|
||||
elif section == "port_dis": parsed["ports_disparus"].append(s)
|
||||
elif section == "port_app": parsed["ports_apparus"].append(s)
|
||||
# Compteurs avant/après depuis lignes "Avant : X | Après : Y"
|
||||
for ln in stdout.splitlines():
|
||||
m = re.match(r"\s*Avant\s*:\s*(\d+).*Apr.s\s*:\s*(\d+)", ln)
|
||||
if not m:
|
||||
continue
|
||||
if parsed["services_avant"] is None:
|
||||
parsed["services_avant"] = int(m.group(1))
|
||||
parsed["services_apres"] = int(m.group(2))
|
||||
elif parsed["ports_avant"] is None:
|
||||
parsed["ports_avant"] = int(m.group(1))
|
||||
parsed["ports_apres"] = int(m.group(2))
|
||||
return parsed
|
||||
|
||||
|
||||
def pre_patch_capture(hostname: str) -> Dict[str, Any]:
|
||||
"""B3.4 — Capture services+ports avant patch (snapshot dans /tmp côté serveur)."""
|
||||
client, target, err = _open_ssh(hostname)
|
||||
if err:
|
||||
return {"ok": False, "detail": err, "target": target}
|
||||
try:
|
||||
r = _push_and_run(client, "/tmp/secops_pre_patching.sh",
|
||||
PRE_PATCH_SCRIPT, timeout=60)
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
ok = (r["rc"] == 0)
|
||||
return {
|
||||
"ok": ok, "rc": r["rc"], "target": target,
|
||||
"stdout": r["stdout"][-2000:],
|
||||
"stderr": r["stderr"][:500] if r["stderr"] else "",
|
||||
}
|
||||
|
||||
|
||||
def post_patch_compare(hostname: str) -> Dict[str, Any]:
|
||||
"""B3.5 — Compare services+ports avant/après patch + génère rapport.
|
||||
À exécuter après le yum update + reboot, le serveur doit avoir
|
||||
ses fichiers /tmp/secops_*_avant_*.txt déjà présents (= pre_patch_capture)."""
|
||||
client, target, err = _open_ssh(hostname)
|
||||
if err:
|
||||
return {"ok": False, "detail": err, "target": target}
|
||||
try:
|
||||
r = _push_and_run(client, "/tmp/secops_post_patching.sh",
|
||||
POST_PATCH_SCRIPT, timeout=60)
|
||||
finally:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
parsed = _parse_capture_report(r["stdout"])
|
||||
has_disparus = bool(parsed["services_disparus"]) or bool(parsed["ports_disparus"])
|
||||
has_apparus = bool(parsed["services_apparus"]) or bool(parsed["ports_apparus"])
|
||||
if r["rc"] != 0:
|
||||
status = "ko"
|
||||
elif has_disparus:
|
||||
status = "ko"
|
||||
elif has_apparus:
|
||||
status = "warn"
|
||||
else:
|
||||
status = "ok"
|
||||
return {
|
||||
"ok": (r["rc"] == 0),
|
||||
"status": status,
|
||||
"rc": r["rc"],
|
||||
"target": target,
|
||||
"report": parsed,
|
||||
"stdout": r["stdout"][-3000:],
|
||||
"stderr": r["stderr"][:500] if r["stderr"] else "",
|
||||
}
|
||||
|
||||
@ -51,7 +51,9 @@
|
||||
<th class="text-left p-1">Verdict</th>
|
||||
<th class="text-left p-1">Snapshot</th>
|
||||
<th class="text-left p-1">Dry-run</th>
|
||||
<th class="text-left p-1">Pre-capt.</th>
|
||||
<th class="text-left p-1">Patch</th>
|
||||
<th class="text-left p-1">Post-cmp.</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="check-tbody">
|
||||
@ -70,10 +72,12 @@
|
||||
<td class="p-1 cell-overall text-gray-500">en attente</td>
|
||||
<td class="p-1 cell-snap text-gray-500">·</td>
|
||||
<td class="p-1 cell-dry text-gray-500">·</td>
|
||||
<td class="p-1 cell-pre text-gray-500">·</td>
|
||||
<td class="p-1 cell-patch text-gray-500">·</td>
|
||||
<td class="p-1 cell-post text-gray-500">·</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="14" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||||
<tr><td colspan="16" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -92,10 +96,16 @@
|
||||
→ Step 2 (snapshot vCenter)
|
||||
</button>
|
||||
<button id="btn-dryrun" class="btn-sm bg-cyber-yellow/20 text-cyber-yellow px-4 py-2 text-xs" disabled title="yum update --assumeno : simule sans appliquer">
|
||||
→ Step 3a (dry-run yum)
|
||||
→ 3a Dry-run
|
||||
</button>
|
||||
<button id="btn-pre" class="btn-sm bg-cyber-yellow/20 text-cyber-yellow px-4 py-2 text-xs" disabled title="Capture services + ports avant patch">
|
||||
→ 3b Pre-capt.
|
||||
</button>
|
||||
<button id="btn-step3" class="btn-sm bg-cyber-blue/20 text-cyber-blue px-4 py-2 text-xs" disabled title="yum update -y : applique réellement les patchs">
|
||||
→ Step 3b (patcher yum)
|
||||
→ 3c Patcher
|
||||
</button>
|
||||
<button id="btn-post" class="btn-sm bg-cyber-blue/20 text-cyber-blue px-4 py-2 text-xs" disabled title="Compare services/ports avant/après patch (à lancer après reboot)">
|
||||
→ 3d Post-cmp.
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +115,9 @@
|
||||
const btnRun = document.getElementById('btn-run-all');
|
||||
const btnStep2 = document.getElementById('btn-step2');
|
||||
const btnDryrun = document.getElementById('btn-dryrun');
|
||||
const btnPre = document.getElementById('btn-pre');
|
||||
const btnStep3 = document.getElementById('btn-step3');
|
||||
const btnPost = document.getElementById('btn-post');
|
||||
const tbody = document.getElementById('check-tbody');
|
||||
const summary = document.getElementById('run-summary');
|
||||
const detailsCard = document.getElementById('details-card');
|
||||
@ -193,9 +205,13 @@
|
||||
const ckOk = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok');
|
||||
const snapOk = trs.filter(tr => tr._snapData && tr._snapData.ok);
|
||||
const dryOk = trs.filter(tr => tr._dryData && tr._dryData.ok);
|
||||
const preOk = trs.filter(tr => tr._preData && tr._preData.ok);
|
||||
const patchOk= trs.filter(tr => tr._patchData && tr._patchData.ok);
|
||||
btnStep2.disabled = (ckOk.length === 0);
|
||||
btnDryrun.disabled = (snapOk.length === 0);
|
||||
btnStep3.disabled = (dryOk.length === 0);
|
||||
btnPre.disabled = (dryOk.length === 0);
|
||||
btnStep3.disabled = (preOk.length === 0);
|
||||
btnPost.disabled = (patchOk.length === 0);
|
||||
}
|
||||
|
||||
btnStep2.addEventListener('click', async () => {
|
||||
@ -265,11 +281,74 @@
|
||||
refreshStepButtons();
|
||||
});
|
||||
|
||||
btnStep3.addEventListener('click', async () => {
|
||||
btnPre.addEventListener('click', async () => {
|
||||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||||
const targets = trs.filter(tr => tr._dryData && tr._dryData.ok);
|
||||
if (!targets.length) { alert('Aucun serveur avec dry-run OK'); return; }
|
||||
if (!confirm('⚠ ATTENTION ⚠\n\nLancer yum update -y (PATCH RÉEL) sur ' + targets.length + ' serveur(s) ?\nCes serveurs vont être modifiés. Snapshot pris en amont.')) return;
|
||||
if (!confirm('Capture services+ports avant patch sur ' + targets.length + ' serveur(s) ?')) return;
|
||||
btnPre.disabled = true; btnStep3.disabled = true;
|
||||
let okCount = 0, koCount = 0;
|
||||
for (const tr of targets) {
|
||||
const cell = tr.querySelector('.cell-pre');
|
||||
cell.innerHTML = '<span class="text-cyber-yellow">… capture</span>';
|
||||
try {
|
||||
const r = await fetch('/patching/iexec/pre-capture/' + tr.dataset.rowId, {method:'POST'});
|
||||
const j = await r.json();
|
||||
tr._preData = j;
|
||||
if (j.ok) {
|
||||
okCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-green" title="' + escapeHTML(j.stdout||'') + '">✓ snapshot</span>';
|
||||
} else {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||||
}
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||||
}
|
||||
}
|
||||
summary.innerHTML += ' · Pre-capt : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||||
refreshStepButtons();
|
||||
});
|
||||
|
||||
btnPost.addEventListener('click', async () => {
|
||||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||||
const targets = trs.filter(tr => tr._patchData && tr._patchData.ok);
|
||||
if (!targets.length) { alert('Aucun serveur avec patch OK'); return; }
|
||||
if (!confirm('Comparer services+ports avant/après patch sur ' + targets.length + ' serveur(s) ?\n(à lancer après le reboot du serveur)')) return;
|
||||
btnPost.disabled = true;
|
||||
let okCount = 0, warnCount = 0, koCount = 0;
|
||||
for (const tr of targets) {
|
||||
const cell = tr.querySelector('.cell-post');
|
||||
cell.innerHTML = '<span class="text-cyber-yellow">… compare</span>';
|
||||
try {
|
||||
const r = await fetch('/patching/iexec/post-compare/' + tr.dataset.rowId, {method:'POST'});
|
||||
const j = await r.json();
|
||||
tr._postData = j;
|
||||
const st = j.status || (j.ok ? 'ok' : 'ko');
|
||||
const rep = j.report || {};
|
||||
const dispSvc = (rep.services_disparus||[]).length;
|
||||
const appSvc = (rep.services_apparus||[]).length;
|
||||
const dispPort = (rep.ports_disparus||[]).length;
|
||||
const appPort = (rep.ports_apparus||[]).length;
|
||||
const summ = 'svc -' + dispSvc + ' +' + appSvc + ' / port -' + dispPort + ' +' + appPort;
|
||||
if (st === 'ok') { okCount++; cell.innerHTML = '<span class="text-cyber-green">✓ ' + escapeHTML(summ) + '</span>'; }
|
||||
else if (st === 'warn') { warnCount++; cell.innerHTML = '<span class="text-cyber-yellow" title="' + escapeHTML(j.stdout||'') + '">⚠ ' + escapeHTML(summ) + '</span>'; }
|
||||
else { koCount++; cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.stdout||j.detail||'') + '">✗ ' + escapeHTML(summ) + '</span>'; }
|
||||
} catch(e) {
|
||||
koCount++;
|
||||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||||
}
|
||||
}
|
||||
summary.innerHTML += ' · Post-cmp : ✓ ' + okCount + ' · ⚠ ' + warnCount + ' · ✗ ' + koCount;
|
||||
refreshStepButtons();
|
||||
});
|
||||
|
||||
btnStep3.addEventListener('click', async () => {
|
||||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||||
const targets = trs.filter(tr => tr._preData && tr._preData.ok);
|
||||
if (!targets.length) { alert('Aucun serveur avec pre-capture OK'); return; }
|
||||
if (!confirm('⚠ ATTENTION ⚠\n\nLancer yum update -y (PATCH RÉEL) sur ' + targets.length + ' serveur(s) ?\nSnapshot + pre-capture déjà faits.')) return;
|
||||
if (!confirm('Confirmer une 2e fois : patcher RÉELLEMENT ' + targets.length + ' serveur(s) ?')) return;
|
||||
btnStep3.disabled = true; btnDryrun.disabled = true; btnStep2.disabled = true;
|
||||
let okCount = 0, koCount = 0;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user