patchcenter/app/templates/patching_iexec.html

396 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% 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>
<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 => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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);
});
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) ?')) return;
btnDryrun.disabled = true; btnStep3.disabled = true;
let okCount = 0, koCount = 0;
for (const tr of targets) {
const cell = tr.querySelector('.cell-dry');
cell.innerHTML = '<span class="text-cyber-yellow">… dry-run</span>';
try {
const r = await fetch('/patching/iexec/yum-dryrun/' + tr.dataset.rowId, {method:'POST'});
const j = await r.json();
tr._dryData = j;
if (j.ok) {
okCount++;
const sumLine = (j.summary || []).slice(-2).join(' / ') || 'plan 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="' + 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 += ' · 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.')) 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');
cell.innerHTML = '<span class="text-cyber-yellow">… patch en cours</span>';
try {
const r = await fetch('/patching/iexec/yum-update/' + tr.dataset.rowId, {method:'POST'});
const j = await r.json();
tr._patchData = j;
if (j.ok) {
okCount++;
const sumLine = (j.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="' + 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 += ' · 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 %}