317 lines
16 KiB
HTML
317 lines
16 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">Patch</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-patch text-gray-500">·</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="14" 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">
|
||
→ Step 3a (dry-run yum)
|
||
</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)
|
||
</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 btnStep3 = document.getElementById('btn-step3');
|
||
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);
|
||
});
|
||
|
||
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);
|
||
btnStep2.disabled = (ckOk.length === 0);
|
||
btnDryrun.disabled = (snapOk.length === 0);
|
||
btnStep3.disabled = (dryOk.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();
|
||
});
|
||
|
||
btnStep3.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('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 %}
|