465 lines
25 KiB
HTML
465 lines
25 KiB
HTML
{% extends 'base.html' %}
|
||
{% block title %}Pré-patching — iexec{% endblock %}
|
||
{% block content %}
|
||
<div class="flex justify-between items-center mb-4">
|
||
<div>
|
||
<h2 class="text-xl font-bold text-cyber-accent">Pré-patching — workflow iexec</h2>
|
||
<p class="text-xs text-gray-500 mt-1">
|
||
{{ rows|length }} serveur(s) éligible(s).
|
||
<span class="text-cyber-yellow">Note : seuls les Linux sont concernés (Windows non géré).</span>
|
||
</p>
|
||
</div>
|
||
<a href="javascript:history.back()" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">← Retour</a>
|
||
</div>
|
||
|
||
{# ─── Stepper ─── #}
|
||
<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-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-blue/20 text-cyber-blue font-bold">3. Patch yum</span>
|
||
</div>
|
||
|
||
<div class="card p-3 mb-4">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<h3 class="text-sm font-bold text-cyber-accent">Step 1 — Vérifications pré-patching</h3>
|
||
<div class="flex gap-2">
|
||
<button id="btn-run-all" class="btn-primary px-3 py-1 text-xs">Lancer les checks</button>
|
||
</div>
|
||
</div>
|
||
<p class="text-xs text-gray-500 mb-3">
|
||
Pour chaque serveur Linux : résolution DNS · connexion SSH ·
|
||
joignabilité Satellite (LAN <code>vpdsiasat2</code> / DMZ <code>vpdsiasat1</code>) +
|
||
<code>subscription-manager identity</code> + <code>yum repolist enabled</code>.
|
||
Toutes les commandes utilisent <code>sudo -n</code>.
|
||
</p>
|
||
|
||
<table class="w-full text-xs">
|
||
<thead class="text-cyber-accent border-b border-cyber-border">
|
||
<tr>
|
||
<th class="text-left p-1">Asset</th>
|
||
<th class="text-left p-1">Hostname BDD</th>
|
||
<th class="text-left p-1">Env</th>
|
||
<th class="text-left p-1">Domaine</th>
|
||
<th class="text-left p-1">OS</th>
|
||
<th class="text-left p-1">Excludes</th>
|
||
<th class="text-left p-1">DNS</th>
|
||
<th class="text-left p-1">SSH</th>
|
||
<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>
|
||
<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">
|
||
{% for r in rows %}
|
||
<tr class="border-b border-cyber-border/30" data-row-id="{{ r.id }}" data-os="{{ r.os or '' }}">
|
||
<td class="p-1 font-mono">{{ r.asset_name }}</td>
|
||
<td class="p-1 font-mono">{{ r.hostname or '–' }}</td>
|
||
<td class="p-1">{{ r.environnement or '' }}</td>
|
||
<td class="p-1">{{ r.domaine if r.domaine is defined else '' }}</td>
|
||
<td class="p-1">{{ r.os or '' }}</td>
|
||
<td class="p-1 text-cyber-yellow">{{ r.effective_excludes or '(aucun)' }}</td>
|
||
<td class="p-1 cell-dns text-gray-500">·</td>
|
||
<td class="p-1 cell-ssh 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-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="16" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{# ─── Détails par serveur (panneau pliable) ─── #}
|
||
<div class="card p-3 mb-4" id="details-card" style="display:none;">
|
||
<h3 class="text-sm font-bold text-cyber-accent mb-2">Détails du dernier check</h3>
|
||
<pre id="details-pane" class="bg-cyber-bg p-2 text-[11px] whitespace-pre-wrap overflow-x-auto" style="max-height:400px;"></pre>
|
||
</div>
|
||
|
||
{# ─── Terminal style log dry-run / patch ─── #}
|
||
<div class="card p-2 mb-4" id="term-card" style="display:none;">
|
||
<div class="flex justify-between items-center mb-1">
|
||
<h3 id="term-title" class="text-xs text-cyber-accent font-bold font-mono">$ terminal</h3>
|
||
<button id="term-close" class="text-xs text-gray-500 hover:text-cyber-accent">✕ Fermer</button>
|
||
</div>
|
||
<pre id="term-pane" class="p-3 text-[11px] whitespace-pre-wrap overflow-auto"
|
||
style="max-height:500px; background:#0a0e14; color:#a6e22e;
|
||
font-family:'Cascadia Code','Consolas','Courier New',monospace;
|
||
border:1px solid #1f2937; line-height:1.4;"></pre>
|
||
</div>
|
||
|
||
<div class="flex justify-between items-center mt-4 flex-wrap gap-2">
|
||
<span id="run-summary" class="text-xs text-gray-400"></span>
|
||
<div class="flex gap-2 flex-wrap">
|
||
<button id="btn-step2" class="btn-sm bg-cyber-green/20 text-cyber-green px-4 py-2 text-xs" disabled>
|
||
→ 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">
|
||
→ 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">
|
||
→ 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>
|
||
|
||
<script>
|
||
(function(){
|
||
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');
|
||
const detailsPane = document.getElementById('details-pane');
|
||
|
||
function escapeHTML(s){
|
||
if (s === null || s === undefined) return '';
|
||
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||
}
|
||
|
||
function statusBadge(st){
|
||
if (st === 'ok') return '<span class="text-cyber-green">✓ OK</span>';
|
||
if (st === 'warn')return '<span class="text-cyber-yellow">⚠ WARN</span>';
|
||
if (st === 'ko') return '<span class="text-cyber-red">✗ KO</span>';
|
||
if (st === 'unsupported') return '<span class="text-gray-400">⊘ N/A</span>';
|
||
return '<span class="text-gray-500">' + st + '</span>';
|
||
}
|
||
|
||
function isLinux(osStr){
|
||
const s = (osStr || '').toLowerCase();
|
||
return s && !s.includes('windows') && s !== 'win';
|
||
}
|
||
|
||
async function checkOne(tr){
|
||
const rowId = tr.dataset.rowId;
|
||
const osStr = tr.dataset.os || '';
|
||
if (!isLinux(osStr)) {
|
||
tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-ssh').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-sat').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="Workflow Linux uniquement">⊘ Windows</span>';
|
||
return {overall: 'unsupported'};
|
||
}
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-yellow">…</span>';
|
||
try {
|
||
const r = await fetch('/patching/iexec/check/' + rowId, {method:'POST'});
|
||
const j = await r.json();
|
||
if (!j.ok) {
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
|
||
return {overall: 'ko'};
|
||
}
|
||
if (j.overall === 'unsupported') {
|
||
tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-ssh').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-disk').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-sat').innerHTML = statusBadge('unsupported');
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="' + (j.skipped_reason||'') + '">⊘ N/A</span>';
|
||
return j;
|
||
}
|
||
const byName = {};
|
||
(j.checks || []).forEach(c => byName[c.name] = c);
|
||
tr.querySelector('.cell-dns').innerHTML = byName.dns ? statusBadge(byName.dns.status) : '–';
|
||
tr.querySelector('.cell-ssh').innerHTML = byName.ssh ? statusBadge(byName.ssh.status) : '–';
|
||
tr.querySelector('.cell-disk').innerHTML = byName.disk ? statusBadge(byName.disk.status) + ' <span class="text-[10px] text-gray-400">' + escapeHTML(byName.disk.message) + '</span>' : '–';
|
||
tr.querySelector('.cell-sat').innerHTML = byName.satellite ? statusBadge(byName.satellite.status) : '–';
|
||
tr.querySelector('.cell-overall').innerHTML = statusBadge(j.overall) + ' <span class="text-[10px] text-gray-500">(' + (j.duration_ms||0) + 'ms)</span>';
|
||
tr._checkData = j;
|
||
return j;
|
||
} catch(e) {
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
|
||
return {overall: 'ko'};
|
||
}
|
||
}
|
||
|
||
btnRun.addEventListener('click', async () => {
|
||
btnRun.disabled = true; btnStep2.disabled = true;
|
||
summary.textContent = 'Lancement…';
|
||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||
let okCount = 0, warnCount = 0, koCount = 0, naCount = 0;
|
||
for (const tr of trs) {
|
||
const r = await checkOne(tr);
|
||
if (r.overall === 'ok') okCount++;
|
||
else if (r.overall === 'warn') warnCount++;
|
||
else if (r.overall === 'unsupported') naCount++;
|
||
else koCount++;
|
||
}
|
||
summary.innerHTML = '✓ ' + okCount + ' OK · ⚠ ' + warnCount + ' warn · ✗ ' + koCount + ' KO · ⊘ ' + naCount + ' N/A';
|
||
btnRun.disabled = false;
|
||
btnStep2.disabled = (okCount === 0);
|
||
});
|
||
|
||
// ─── Terminal ───
|
||
const termCard = document.getElementById('term-card');
|
||
const termTitle = document.getElementById('term-title');
|
||
const termPane = document.getElementById('term-pane');
|
||
document.getElementById('term-close').addEventListener('click', () => {
|
||
termCard.style.display = 'none';
|
||
});
|
||
function openTerm(title){
|
||
termCard.style.display = '';
|
||
termTitle.textContent = '$ ' + title;
|
||
termPane.textContent = '';
|
||
termCard.scrollIntoView({behavior:'smooth', block:'nearest'});
|
||
}
|
||
function appendTerm(s){
|
||
termPane.textContent += s;
|
||
termPane.scrollTop = termPane.scrollHeight;
|
||
}
|
||
|
||
function streamYum(rowId, mode, hostname){
|
||
return new Promise((resolve) => {
|
||
openTerm((mode === 'dryrun' ? 'dry-run' : 'PATCH') + ' yum @ ' + hostname);
|
||
const url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode;
|
||
const ev = new EventSource(url);
|
||
const result = {ok: false, rc: null, lines: 0, summary: []};
|
||
ev.onmessage = (m) => {
|
||
let j;
|
||
try { j = JSON.parse(m.data); } catch(e) { return; }
|
||
if (j.type === 'cmd') {
|
||
appendTerm('# host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n');
|
||
appendTerm('# cmd : ' + (j.cmd||'') + '\n');
|
||
appendTerm('# excludes (' + (j.excludes||[]).length + ')\n\n');
|
||
} else if (j.type === 'line') {
|
||
appendTerm(j.data + '\n');
|
||
result.lines++;
|
||
// Capture quelques lignes-clé pour le badge cellule
|
||
const ll = j.data.toLowerCase();
|
||
if (ll.includes('package') || ll.includes('paquet')
|
||
|| ll.includes('nothing to do') || ll.includes('rien à faire')
|
||
|| ll.includes('complete!') || ll.includes('terminé !')) {
|
||
result.summary.push(j.data);
|
||
}
|
||
} else if (j.type === 'end') {
|
||
result.rc = j.rc;
|
||
// dryrun rc=0 (rien) ou rc=1 (updates dispo) = OK
|
||
// update rc=0 = OK
|
||
if (mode === 'dryrun') result.ok = (j.rc === 0 || j.rc === 1);
|
||
else result.ok = (j.rc === 0);
|
||
appendTerm('\n[exit code: ' + j.rc + ' — ' + (result.ok ? 'OK' : 'KO') + ']\n');
|
||
ev.close();
|
||
resolve(result);
|
||
} else if (j.type === 'error') {
|
||
appendTerm('\n[ERROR] ' + (j.msg||'') + '\n');
|
||
result.error = j.msg;
|
||
ev.close();
|
||
resolve(result);
|
||
}
|
||
};
|
||
ev.onerror = () => {
|
||
appendTerm('\n[connection lost]\n');
|
||
ev.close();
|
||
resolve(result);
|
||
};
|
||
});
|
||
}
|
||
|
||
function refreshStepButtons(){
|
||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||
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);
|
||
btnPre.disabled = (dryOk.length === 0);
|
||
btnStep3.disabled = (preOk.length === 0);
|
||
btnPost.disabled = (patchOk.length === 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>';
|
||
}
|
||
}
|
||
btnRun.disabled = false;
|
||
summary.innerHTML += ' · Snapshot : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
btnDryrun.addEventListener('click', async () => {
|
||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||
const targets = trs.filter(tr => tr._snapData && tr._snapData.ok);
|
||
if (!targets.length) { alert('Aucun serveur avec snapshot OK'); return; }
|
||
if (!confirm('Lancer dry-run yum (simulation) sur ' + targets.length + ' serveur(s) ?\nLog en temps réel dans le terminal.')) return;
|
||
btnDryrun.disabled = true; btnStep3.disabled = true;
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const cell = tr.querySelector('.cell-dry');
|
||
const host = tr.querySelector('td:nth-child(2)').textContent.trim()
|
||
|| tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
cell.innerHTML = '<span class="text-cyber-yellow">… dry-run (live)</span>';
|
||
const result = await streamYum(tr.dataset.rowId, 'dryrun', host);
|
||
tr._dryData = {ok: result.ok, rc: result.rc, summary: result.summary};
|
||
if (result.ok) {
|
||
okCount++;
|
||
const sumLine = (result.summary || []).slice(-2).join(' / ') || ('plan OK (' + result.lines + ' lignes)');
|
||
cell.innerHTML = '<span class="text-cyber-green">✓ </span><span class="text-[10px] text-gray-300" title="' + escapeHTML(sumLine) + '">' + escapeHTML(sumLine.slice(0,80)) + '</span>';
|
||
} else {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red" title="rc=' + result.rc + '">✗ KO (rc=' + result.rc + ')</span>';
|
||
}
|
||
}
|
||
summary.innerHTML += ' · Dry-run : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
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('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.\nLog en temps réel dans le terminal.')) 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;
|
||
for (const tr of targets) {
|
||
const cell = tr.querySelector('.cell-patch');
|
||
const host = tr.querySelector('td:nth-child(2)').textContent.trim()
|
||
|| tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
cell.innerHTML = '<span class="text-cyber-yellow">… patch (live)</span>';
|
||
const result = await streamYum(tr.dataset.rowId, 'update', host);
|
||
tr._patchData = {ok: result.ok, rc: result.rc, summary: result.summary};
|
||
if (result.ok) {
|
||
okCount++;
|
||
const sumLine = (result.summary || []).slice(-2).join(' / ') || 'patch OK';
|
||
cell.innerHTML = '<span class="text-cyber-green">✓ </span><span class="text-[10px] text-gray-300" title="' + escapeHTML(sumLine) + '">' + escapeHTML(sumLine.slice(0,80)) + '</span>';
|
||
} else {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red" title="rc=' + result.rc + '">✗ KO (rc=' + result.rc + ')</span>';
|
||
}
|
||
}
|
||
summary.innerHTML += ' · Patch : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
// Click sur une ligne → afficher les détails
|
||
tbody.addEventListener('click', (ev) => {
|
||
const tr = ev.target.closest('tr[data-row-id]');
|
||
if (!tr || !tr._checkData) return;
|
||
detailsCard.style.display = '';
|
||
const j = tr._checkData;
|
||
let txt = `Hostname: ${j.hostname || '–'}\nTarget: ${j.target || '–'}\nVerdict: ${j.overall}\n\n`;
|
||
(j.checks || []).forEach(c => {
|
||
txt += `[${c.status.toUpperCase()}] ${c.label} : ${c.message}\n`;
|
||
if (c.details) txt += c.details + '\n';
|
||
txt += '---\n';
|
||
});
|
||
detailsPane.textContent = txt;
|
||
});
|
||
})();
|
||
</script>
|
||
{% endblock %}
|