- auth.py: flag Secure + path=/ sur le cookie d'authentification - ldap_service.py: logging debug des connexions LDAPS vers logs/ldap_debug.log (jamais les mots de passe) - .gitignore: protege cles/certs TLS (ssl/, *.key, *.crt) + artefacts lourds (db/, sitepkgs.zip, *.bak, dump) - inclut aussi des modifs en cours: planning_import, patch_run_service, patching_iexec Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
908 lines
51 KiB
HTML
908 lines
51 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 ─── #}
|
||
<style>
|
||
.step-pill { padding:.35rem .75rem; border-radius:.5rem; font-weight:bold;
|
||
font-size:.7rem; transition:all .2s; border:1px solid transparent; }
|
||
.step-pill.s-pending { background:rgba(75,85,99,.15); color:#6b7280;
|
||
border-color:rgba(75,85,99,.3); opacity:.6; }
|
||
.step-pill.s-current { background:rgba(245,158,11,.18); color:#f59e0b;
|
||
border-color:#f59e0b; box-shadow:0 0 12px rgba(245,158,11,.35); }
|
||
.step-pill.s-done { background:rgba(34,197,94,.18); color:#22c55e;
|
||
border-color:rgba(34,197,94,.5); }
|
||
.step-pill.s-failed { background:rgba(239,68,68,.2); color:#ef4444;
|
||
border-color:#ef4444; box-shadow:0 0 10px rgba(239,68,68,.35); }
|
||
.step-pill.s-running { background:rgba(245,158,11,.25); color:#f59e0b;
|
||
border-color:#f59e0b; animation: pulseIt 1.2s infinite; }
|
||
@keyframes pulseIt { 0%,100% { opacity:1 } 50% { opacity:.55 } }
|
||
</style>
|
||
{# Banner cumulé des particularités (servers avec patching_notes) #}
|
||
{% set has_notes = rows|selectattr('patching_notes')|list %}
|
||
{% if has_notes %}
|
||
<div class="card p-3 mb-4 border-l-4 border-cyber-orange bg-cyber-orange/10">
|
||
<h3 class="text-sm font-bold text-cyber-orange mb-2">⚠ Particularités de patching pour {{ has_notes|length }} serveur(s)</h3>
|
||
<p class="text-xs text-gray-400">Clique sur le badge <span class="badge badge-orange">⚠ note</span> à côté du nom du serveur pour voir la procédure spécifique.</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="flex items-center mb-4 gap-1 text-xs flex-wrap" id="stepper">
|
||
<span data-step="check" class="step-pill s-current">1 Vérif</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="snap" class="step-pill s-pending">2 Snapshot</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="dry" class="step-pill s-pending">3a Dry-run</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="pre" class="step-pill s-pending">3b Pre-capt</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="patch" class="step-pill s-pending">3c Patch</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="reboot" class="step-pill s-pending">3e Reboot</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="recon" class="step-pill s-pending">3f Reconn.</span>
|
||
<span class="text-gray-600">→</span>
|
||
<span data-step="post" class="step-pill s-pending">3g Post-cmp</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="step-btn s-current" title="Lance les vérifs DNS+SSH+Disque+Satellite">1 Lancer les vérifs</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">Reconnex.</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 '' }}"
|
||
data-pct-required="{{ '1' if r.pct_required else '0' }}"
|
||
data-pct-confirmed="{{ '1' if r.pct_confirmed_at else '0' }}"
|
||
data-skip-first-reboot="{{ '1' if r.skip_first_reboot else '0' }}"
|
||
data-cluster-name="{{ r.cluster_name or '' }}"
|
||
data-reboot-delay-min="{{ r.reboot_delay_min_minutes or 0 }}">
|
||
<td class="p-1 font-mono">{{ r.asset_name }}{% if r.pct_required %}
|
||
<span class="badge {% if r.pct_confirmed_at %}badge-green{% else %}badge-orange{% endif %} ml-1 cell-pct-badge"
|
||
title="{% if r.pct_confirmed_at %}PCT confirmé le {{ r.pct_confirmed_at }}{% else %}Prévenance PCT requise — à confirmer avant le patch{% endif %}">
|
||
{% if r.pct_confirmed_at %}✅ PCT ok{% else %}⚠ Prév PCT à faire{% endif %}
|
||
</span>
|
||
{% endif %}{% if r.skip_first_reboot %}
|
||
<span class="badge badge-blue ml-1" title="Pas de 1er reboot post-patch">⏭ skip 1st reboot</span>
|
||
{% endif %}{% if r.patching_notes %}
|
||
<button type="button" class="badge badge-orange ml-1 cursor-pointer"
|
||
onclick="showPatchingNote(this, '{{ r.asset_name|e }}')"
|
||
data-note="{{ r.patching_notes|e }}"
|
||
title="Particularités de patching — clique pour afficher">⚠ note</button>
|
||
{% endif %}{% if r.cluster_name and r.reboot_delay_min_minutes and r.reboot_delay_min_minutes > 0 %}
|
||
<span class="badge badge-gray ml-1" title="Cluster {{ r.cluster_name }} — délai min {{ r.reboot_delay_min_minutes }} min entre reboots">⏱ {{ r.reboot_delay_min_minutes }}min</span>
|
||
{% endif %}</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-recon text-gray-500">·</td>
|
||
<td class="p-1 cell-post text-gray-500">·</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="17" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{# ─── Détails par serveur (panneau pliable) ─── #}
|
||
<div class="card mb-4" id="details-card" style="display:none;">
|
||
<div class="flex justify-between items-center p-3 border-b border-cyber-border cursor-pointer"
|
||
onclick="toggleDetails()">
|
||
<h3 class="text-sm font-bold text-cyber-accent">
|
||
<span id="details-arrow">▾</span> Détails du dernier check
|
||
</h3>
|
||
<button onclick="event.stopPropagation(); document.getElementById('details-card').style.display='none';"
|
||
class="text-xs text-gray-500 hover:text-cyber-accent">✕ Fermer</button>
|
||
</div>
|
||
<pre id="details-pane" class="bg-cyber-bg p-2 text-[11px] whitespace-pre-wrap overflow-x-auto"
|
||
style="max-height:400px;"></pre>
|
||
</div>
|
||
<script>
|
||
function toggleDetails(){
|
||
const pane = document.getElementById('details-pane');
|
||
const arr = document.getElementById('details-arrow');
|
||
const collapsed = pane.style.display === 'none';
|
||
pane.style.display = collapsed ? '' : 'none';
|
||
arr.textContent = collapsed ? '▾' : '▸';
|
||
}
|
||
</script>
|
||
|
||
{# ─── 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">$ patchcenter iexec — log</h3>
|
||
<div class="flex gap-2">
|
||
<button id="term-clear" class="text-xs text-gray-500 hover:text-cyber-accent">🗑 Vider</button>
|
||
<button id="term-close" class="text-xs text-gray-500 hover:text-cyber-accent">✕ Masquer</button>
|
||
</div>
|
||
</div>
|
||
<pre id="term-pane" class="p-3 text-[11px] whitespace-pre-wrap overflow-auto"
|
||
style="max-height:600px; background:#0a0e14; color:#cdd6f4;
|
||
font-family:'Cascadia Code','Consolas','Courier New',monospace;
|
||
border:1px solid #1f2937; line-height:1.4;"></pre>
|
||
<style>
|
||
#term-pane .t-section { color:#c678dd; font-weight:bold; }
|
||
#term-pane .t-ok { color:#a6e22e; }
|
||
#term-pane .t-warn { color:#e5c07b; }
|
||
#term-pane .t-ko { color:#ff5555; }
|
||
#term-pane .t-info { color:#61afef; }
|
||
#term-pane .t-mute { color:#7f848e; }
|
||
#term-pane .t-cmd { color:#56b6c2; font-weight:bold; }
|
||
#term-pane .t-detail { color:#abb2bf; padding-left:2ch; display:block; }
|
||
</style>
|
||
</div>
|
||
|
||
<style>
|
||
.step-btn { padding:.5rem 1rem; border-radius:.5rem; font-weight:bold;
|
||
font-size:.75rem; transition:all .15s; border:1px solid transparent;
|
||
white-space:nowrap; cursor:pointer; }
|
||
.step-btn:disabled { cursor:not-allowed; }
|
||
.step-btn.s-pending { background:rgba(75,85,99,.15); color:#6b7280;
|
||
border-color:rgba(75,85,99,.3); opacity:.55; }
|
||
.step-btn.s-current { background:rgba(245,158,11,.2); color:#f59e0b;
|
||
border-color:#f59e0b; box-shadow:0 0 14px rgba(245,158,11,.4); }
|
||
.step-btn.s-current:hover { background:rgba(245,158,11,.35); }
|
||
.step-btn.s-done { background:rgba(34,197,94,.2); color:#22c55e;
|
||
border-color:rgba(34,197,94,.5); }
|
||
.step-btn.s-done:hover { background:rgba(34,197,94,.35); }
|
||
.step-btn.s-failed { background:rgba(239,68,68,.2); color:#ef4444;
|
||
border-color:#ef4444; box-shadow:0 0 12px rgba(239,68,68,.4); }
|
||
.step-btn.s-failed:hover { background:rgba(239,68,68,.35); }
|
||
.step-btn.s-running { background:rgba(245,158,11,.3); color:#f59e0b;
|
||
border-color:#f59e0b; animation: pulseIt 1.2s infinite; }
|
||
</style>
|
||
<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="step-btn s-pending" disabled title="Snapshot vCenter">2 Snapshot</button>
|
||
<button id="btn-dryrun" class="step-btn s-pending" disabled title="yum update --assumeno : simule sans appliquer">3a Dry-run</button>
|
||
<button id="btn-pre" class="step-btn s-pending" disabled title="Capture services + ports avant patch">3b Pre-capt.</button>
|
||
<button id="btn-step3" class="step-btn s-pending" disabled title="yum update -y : applique réellement (double confirmation)">3c Patcher</button>
|
||
<button id="btn-reboot" class="step-btn s-pending" disabled title="shutdown -r +1 sur les serveurs patchés (double confirmation)">3e Reboot</button>
|
||
<button id="btn-recon" class="step-btn s-pending" disabled title="Polle TCP/22 + SSH jusqu'à reconnexion">3f Wait reconn.</button>
|
||
<button id="btn-post" class="step-btn s-pending" disabled title="Compare services/ports avant/après patch">3g 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 btnReboot = document.getElementById('btn-reboot');
|
||
const btnRecon = document.getElementById('btn-recon');
|
||
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 || '';
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
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>';
|
||
termSection('check ' + host);
|
||
termLine('⊘', 'Windows non supporté (Linux uniquement)');
|
||
return {overall: 'unsupported'};
|
||
}
|
||
termSection('check ' + host);
|
||
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>';
|
||
termLine('✗', 'erreur backend');
|
||
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>';
|
||
termLine('⊘', j.skipped_reason || 'non supporté');
|
||
return j;
|
||
}
|
||
appendTerm(' # target=' + (j.target || '?') + '\n');
|
||
const byName = {};
|
||
(j.checks || []).forEach(c => {
|
||
byName[c.name] = c;
|
||
const sym = c.status === 'ok' ? '✓' : (c.status === 'warn' ? '⚠' : '✗');
|
||
termLine(sym, c.label + ' : ' + c.message);
|
||
if (c.details && c.status !== 'ok') {
|
||
appendTerm(' │ ' + (c.details || '').split('\n').slice(0,8).join('\n │ ') + '\n');
|
||
}
|
||
});
|
||
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>';
|
||
termLine('→', 'verdict ' + j.overall + ' (' + (j.duration_ms||0) + 'ms)');
|
||
tr._checkData = j;
|
||
return j;
|
||
} catch(e) {
|
||
tr.querySelector('.cell-overall').innerHTML = '<span class="text-cyber-red">err</span>';
|
||
termLine('✗', 'exception : ' + e.message);
|
||
return {overall: 'ko'};
|
||
}
|
||
}
|
||
|
||
btnRun.addEventListener('click', async () => {
|
||
setBtnState(btnRun, 'running'); setStepState('check', 'running');
|
||
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';
|
||
refreshStepButtons();
|
||
});
|
||
|
||
// ─── Terminal global (toutes les étapes) ───
|
||
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';
|
||
});
|
||
document.getElementById('term-clear').addEventListener('click', () => {
|
||
termPane.innerHTML = '';
|
||
});
|
||
function showTerm(){
|
||
if (termCard.style.display === 'none') {
|
||
termCard.style.display = '';
|
||
termCard.scrollIntoView({behavior:'smooth', block:'nearest'});
|
||
}
|
||
}
|
||
function appendTermHTML(html){
|
||
termPane.insertAdjacentHTML('beforeend', html);
|
||
termPane.scrollTop = termPane.scrollHeight;
|
||
}
|
||
function appendTerm(s){
|
||
// Compat : ancien API qui écrit du texte brut
|
||
appendTermHTML(escapeHTML(s));
|
||
}
|
||
function termSection(label){
|
||
showTerm();
|
||
const ts = new Date().toLocaleTimeString('fr-FR', {hour12: false});
|
||
appendTermHTML('\n<span class="t-section">══════ [' + escapeHTML(label) + '] ' + ts + ' ══════</span>\n');
|
||
}
|
||
function termLine(prefix, text){
|
||
let cls = '';
|
||
if (prefix === '✓') cls = 't-ok';
|
||
else if (prefix === '⚠') cls = 't-warn';
|
||
else if (prefix === '✗') cls = 't-ko';
|
||
else if (prefix === '⊘') cls = 't-mute';
|
||
else if (prefix === '⏳') cls = 't-warn';
|
||
else if (prefix === '→') cls = 't-info';
|
||
else if (prefix === '#') cls = 't-mute';
|
||
appendTermHTML(' <span class="' + cls + '">' + escapeHTML(prefix + ' ' + text) + '</span>\n');
|
||
}
|
||
function termDetail(text){
|
||
if (!text) return;
|
||
const lines = String(text).split('\n').slice(0, 12);
|
||
appendTermHTML('<span class="t-detail">' + escapeHTML(lines.join('\n')) + '</span>\n');
|
||
}
|
||
function classifyYumLine(s){
|
||
const ll = String(s).toLowerCase();
|
||
if (ll.startsWith('error') || ll.startsWith('erreur') || ll.includes(' fail') || ll.includes('failed')) return 't-ko';
|
||
if (ll.startsWith('warning') || ll.startsWith('attention')) return 't-warn';
|
||
if (ll.includes('complete!') || ll.includes('terminé !') || ll.includes('nothing to do') || ll.includes('rien à faire')) return 't-ok';
|
||
if (ll.startsWith('==') || ll.startsWith('--') || ll.startsWith('transaction')) return 't-info';
|
||
return '';
|
||
}
|
||
|
||
function streamYum(rowId, mode, hostname, extraExcludes){
|
||
return new Promise((resolve) => {
|
||
const titre = (mode === 'dryrun' ? 'dry-run' : 'PATCH') + ' yum ' + hostname
|
||
+ (extraExcludes ? ' [retry +' + extraExcludes + ']' : '');
|
||
termSection(titre);
|
||
let url = '/patching/iexec/yum-stream/' + rowId + '?mode=' + mode;
|
||
if (extraExcludes) url += '&extra_excludes=' + encodeURIComponent(extraExcludes);
|
||
const ev = new EventSource(url);
|
||
const result = {ok: false, rc: null, lines: 0, summary: [], problems: []};
|
||
ev.onmessage = (m) => {
|
||
let j;
|
||
try { j = JSON.parse(m.data); } catch(e) { return; }
|
||
if (j.type === 'ping') {
|
||
// Heartbeat serveur pendant les phases silencieuses de yum :
|
||
// garde la connexion SSE vivante, rien à afficher.
|
||
return;
|
||
} else if (j.type === 'cmd') {
|
||
appendTerm(' # host : ' + (j.hostname||'') + ' (' + (j.target||'') + ')\n');
|
||
appendTerm(' # cmd : ' + (j.cmd||'') + '\n');
|
||
appendTerm(' # excludes (' + (j.excludes||[]).length + ')\n');
|
||
} else if (j.type === 'line') {
|
||
const cls = classifyYumLine(j.data);
|
||
if (cls) appendTermHTML(' <span class="' + cls + '">' + escapeHTML(j.data) + '</span>\n');
|
||
else appendTerm(j.data + '\n');
|
||
result.lines++;
|
||
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;
|
||
result.problems = j.problems || [];
|
||
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');
|
||
if (!result.ok && result.problems.length) {
|
||
appendTerm('[deps détectées : ' + result.problems.join(', ') + ']\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 buildRetryButton(rowId, mode, hostname, packages){
|
||
// Bouton qui relance streamYum avec extra_excludes
|
||
const btn = document.createElement('button');
|
||
btn.className = 'text-cyber-yellow underline hover:text-cyber-accent text-[10px]';
|
||
btn.textContent = '🔁 Retry sans ' + packages.join(', ');
|
||
btn.title = 'Relance le yum en ajoutant ces paquets aux excludes';
|
||
btn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
const extra = packages.join(' ');
|
||
const result = await streamYum(rowId, mode, hostname, extra);
|
||
// Met à jour la cellule
|
||
updateYumCell(rowId, mode, hostname, result);
|
||
});
|
||
return btn;
|
||
}
|
||
|
||
function updateYumCell(rowId, mode, hostname, result){
|
||
const tr = tbody.querySelector('tr[data-row-id="' + rowId + '"]');
|
||
if (!tr) return;
|
||
const cell = tr.querySelector(mode === 'dryrun' ? '.cell-dry' : '.cell-patch');
|
||
if (mode === 'dryrun') tr._dryData = {ok: result.ok, rc: result.rc, summary: result.summary};
|
||
else tr._patchData = {ok: result.ok, rc: result.rc, summary: result.summary};
|
||
cell.innerHTML = '';
|
||
if (result.ok) {
|
||
const sumLine = (result.summary || []).slice(-2).join(' / ') || ('OK (' + result.lines + ' lignes)');
|
||
cell.innerHTML = '<span class="text-cyber-green">✓ </span><span class="text-[10px] text-gray-300">' + escapeHTML(sumLine.slice(0,60)) + '</span>';
|
||
} else {
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ KO (rc=' + result.rc + ')</span>';
|
||
if ((result.problems || []).length) {
|
||
cell.appendChild(document.createElement('br'));
|
||
cell.appendChild(buildRetryButton(rowId, mode, hostname, result.problems));
|
||
}
|
||
}
|
||
refreshStepButtons();
|
||
}
|
||
|
||
// ─── États visuels boutons + stepper ───
|
||
function setBtnState(btn, state){
|
||
if (!btn) return;
|
||
btn.classList.remove('s-pending','s-current','s-done','s-failed','s-running');
|
||
btn.classList.add('s-' + state);
|
||
btn.disabled = (state === 'pending' || state === 'running');
|
||
}
|
||
function setStepState(name, state){
|
||
const el = document.querySelector('#stepper [data-step="' + name + '"]');
|
||
if (!el) return;
|
||
el.classList.remove('s-pending','s-current','s-done','s-failed','s-running');
|
||
el.classList.add('s-' + state);
|
||
}
|
||
|
||
// Calcul de l'état de chaque étape selon les données accumulées
|
||
function deriveState(prevDoneCount, ownAttempted, ownOkCount){
|
||
// pending : étape précédente pas encore validée
|
||
if (prevDoneCount === 0) return 'pending';
|
||
// pas encore tenté : étape actuelle (à faire)
|
||
if (!ownAttempted) return 'current';
|
||
// tenté : ok si au moins 1 OK, failed sinon
|
||
return ownOkCount > 0 ? 'done' : 'failed';
|
||
}
|
||
|
||
function refreshStepButtons(){
|
||
const trs = Array.from(tbody.querySelectorAll('tr[data-row-id]'));
|
||
const checkAttempted = trs.some(tr => tr._checkData);
|
||
const ckOk = trs.filter(tr => tr._checkData && tr._checkData.overall === 'ok').length;
|
||
const snapAttempted = trs.some(tr => tr._snapData);
|
||
const snapOk = trs.filter(tr => tr._snapData && tr._snapData.ok).length;
|
||
// snapEff : snapshot OK *ou* échec forcé (override) — autorise la suite sans snapshot
|
||
const snapEff = trs.filter(tr => tr._snapData && (tr._snapData.ok || tr._snapData.override)).length;
|
||
const dryAttempted = trs.some(tr => tr._dryData);
|
||
const dryOk = trs.filter(tr => tr._dryData && tr._dryData.ok).length;
|
||
const preAttempted = trs.some(tr => tr._preData);
|
||
const preOk = trs.filter(tr => tr._preData && tr._preData.ok).length;
|
||
const patchAttempted = trs.some(tr => tr._patchData);
|
||
const patchOk = trs.filter(tr => tr._patchData && tr._patchData.ok).length;
|
||
const rebootAttempted = trs.some(tr => tr._rebootData);
|
||
const rebootOk = trs.filter(tr => tr._rebootData && tr._rebootData.ok).length;
|
||
const reconAttempted = trs.some(tr => tr._reconData);
|
||
const reconOk = trs.filter(tr => tr._reconData && tr._reconData.ok).length;
|
||
const postAttempted = trs.some(tr => tr._postData);
|
||
const postOk = trs.filter(tr => tr._postData && tr._postData.ok).length;
|
||
|
||
// Step 1 (check) : actif uniquement si on n'a pas encore lancé une étape ultérieure.
|
||
// Dès qu'on est passé à snap/dry/pre/patch/reboot/recon/post, on lock le bouton "Vérifs".
|
||
const anyLaterStarted = snapAttempted || dryAttempted || preAttempted
|
||
|| patchAttempted || rebootAttempted || reconAttempted || postAttempted;
|
||
const checkState = !checkAttempted ? 'current' : (ckOk > 0 ? 'done' : 'failed');
|
||
setBtnState(btnRun, checkState);
|
||
setStepState('check', checkState);
|
||
// Lock explicite si on est passé à la phase suivante
|
||
if (btnRun && anyLaterStarted) {
|
||
btnRun.disabled = true;
|
||
btnRun.title = 'Phase 1 verrouillée — étape suivante déjà démarrée';
|
||
}
|
||
|
||
// Cascade : chaque étape dépend du `done` de la précédente
|
||
const snapState = deriveState(ckOk, snapAttempted, snapOk);
|
||
setBtnState(btnStep2, snapState); setStepState('snap', snapState);
|
||
|
||
const dryState = deriveState(snapEff, dryAttempted, dryOk);
|
||
setBtnState(btnDryrun, dryState); setStepState('dry', dryState);
|
||
|
||
const preState = deriveState(dryOk, preAttempted, preOk);
|
||
setBtnState(btnPre, preState); setStepState('pre', preState);
|
||
|
||
const patchState = deriveState(preOk, patchAttempted, patchOk);
|
||
setBtnState(btnStep3, patchState); setStepState('patch', patchState);
|
||
|
||
const rebootState = deriveState(patchOk, rebootAttempted, rebootOk);
|
||
setBtnState(btnReboot, rebootState); setStepState('reboot', rebootState);
|
||
|
||
const reconState = deriveState(patchOk, reconAttempted, reconOk);
|
||
setBtnState(btnRecon, reconState); setStepState('recon', reconState);
|
||
|
||
// Post-cmp dispo dès qu'on a soit recon OK (idéal après reboot) soit patch OK
|
||
const postPrevDone = Math.max(reconOk, patchOk);
|
||
const postState = deriveState(postPrevDone, postAttempted, postOk);
|
||
setBtnState(btnPost, postState); setStepState('post', postState);
|
||
}
|
||
|
||
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;
|
||
setBtnState(btnStep2, 'running'); setStepState('snap', 'running');
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of okTrs) {
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
termSection('snapshot ' + host);
|
||
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>';
|
||
termLine('✓', 'snap_name=' + (j.snap_name||'') + ' on vCenter ' + (j.vcenter||''));
|
||
if (j.branch) appendTerm(' # branch=' + j.branch + ' vm=' + (j.vm_name||'') + '\n');
|
||
} else if (j.skipped) {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-yellow">⚠ ' + escapeHTML(j.detail||'skip') + '</span>';
|
||
termLine('⚠', j.detail || 'skipped');
|
||
} else {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||
termLine('✗', j.detail || 'KO');
|
||
}
|
||
tr._snapData = j;
|
||
} catch(e) {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur réseau</span>';
|
||
termLine('✗', 'exception : ' + e.message);
|
||
}
|
||
}
|
||
summary.innerHTML += ' · Snapshot : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
// Échec snapshot : déblocage direct de l'étape suivante (sans confirmation)
|
||
if (koCount > 0) {
|
||
const failed = okTrs.filter(tr => tr._snapData && !tr._snapData.ok);
|
||
for (const tr of failed) {
|
||
tr._snapData.override = true;
|
||
const cell = tr.querySelector('.cell-snap');
|
||
if (cell) cell.innerHTML += ' <span class="text-cyber-yellow text-[10px]">(forcé)</span>';
|
||
const c3 = tr.querySelector('td:nth-child(3)');
|
||
termLine('⚠', 'snapshot en échec — poursuite forcée sans snapshot : ' + (c3 ? c3.textContent.trim() : tr.dataset.rowId));
|
||
}
|
||
}
|
||
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 || tr._snapData.override));
|
||
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;
|
||
setBtnState(btnDryrun, 'running'); setStepState('dry', 'running');
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const host = tr.querySelector('td:nth-child(2)').textContent.trim()
|
||
|| tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
tr.querySelector('.cell-dry').innerHTML = '<span class="text-cyber-yellow">… dry-run (live)</span>';
|
||
const result = await streamYum(tr.dataset.rowId, 'dryrun', host);
|
||
updateYumCell(tr.dataset.rowId, 'dryrun', host, result);
|
||
if (result.ok) okCount++; else koCount++;
|
||
}
|
||
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;
|
||
setBtnState(btnPre, 'running'); setStepState('pre', 'running');
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
termSection('pre-capture ' + host);
|
||
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>';
|
||
termLine('✓', 'services + ports capturés dans /tmp/secops_*_avant_*.txt');
|
||
termDetail(j.stdout);
|
||
} else {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||
termLine('✗', j.detail || 'KO');
|
||
if (j.stderr) termDetail(j.stderr);
|
||
}
|
||
} catch(e) {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||
termLine('✗', 'exception : ' + e.message);
|
||
}
|
||
}
|
||
summary.innerHTML += ' · Pre-capt : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
btnReboot.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('⚠ REBOOT ⚠\n\nDéclencher `shutdown -r +1` sur ' + targets.length + ' serveur(s) ?\n(le reboot effectif a lieu dans 1 minute)')) return;
|
||
if (!confirm('Vraiment ? Liste des hôtes :\n' + targets.map(tr => tr.querySelector('td:nth-child(3)').textContent.trim()).join('\n') + '\n\nConfirmer le reboot ?')) return;
|
||
setBtnState(btnReboot, 'running'); setStepState('reboot', 'running');
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
termSection('reboot ' + host);
|
||
const cell = tr.querySelector('.cell-recon');
|
||
cell.innerHTML = '<span class="text-cyber-yellow">… reboot demandé</span>';
|
||
try {
|
||
const r = await fetch('/patching/iexec/reboot/' + tr.dataset.rowId, {method:'POST'});
|
||
const j = await r.json();
|
||
tr._rebootData = j;
|
||
if (j.ok) {
|
||
okCount++;
|
||
cell.innerHTML = '<span class="text-cyber-yellow">⏳ reboot dans 1min · ' + escapeHTML(j.started_at||'') + '</span>';
|
||
termLine('⏳', 'shutdown -r +1 envoyé · effectif dans 1 min · ' + (j.started_at||''));
|
||
} else {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.detail||j.stderr||'') + '">✗ ' + escapeHTML((j.detail||'KO').slice(0,80)) + '</span>';
|
||
termLine('✗', j.detail || 'KO');
|
||
if (j.stderr) termDetail(j.stderr);
|
||
}
|
||
} catch(e) {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||
termLine('✗', 'exception : ' + e.message);
|
||
}
|
||
}
|
||
summary.innerHTML += ' · Reboot : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
btnRecon.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('Attendre la reconnexion (TCP/22 + SSH) sur ' + targets.length + ' serveur(s) ?\nPoll toutes les 10s, timeout 10 min par serveur.')) return;
|
||
setBtnState(btnRecon, 'running'); setStepState('recon', 'running');
|
||
const startTs = Date.now();
|
||
// Pour chaque target, polling indépendant
|
||
await Promise.all(targets.map(async (tr) => {
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
termSection('wait reconnexion ' + host);
|
||
const cell = tr.querySelector('.cell-recon');
|
||
const t0 = Date.now();
|
||
const TIMEOUT_MS = 10 * 60 * 1000;
|
||
const POLL_MS = 10 * 1000;
|
||
cell.innerHTML = '<span class="text-cyber-yellow">⏳ poll TCP/22…</span>';
|
||
let last = '';
|
||
while (Date.now() - t0 < TIMEOUT_MS) {
|
||
await new Promise(r => setTimeout(r, POLL_MS));
|
||
try {
|
||
const resp = await fetch('/patching/iexec/reboot-status/' + tr.dataset.rowId);
|
||
const j = await resp.json();
|
||
if (j.tcp22 && j.ssh) {
|
||
const dur = Math.round((Date.now() - t0) / 1000);
|
||
tr._reconData = {ok: true, downtime_s: dur, uptime: j.uptime};
|
||
cell.innerHTML = '<span class="text-cyber-green">✓ revenu en ' + dur + 's</span>'
|
||
+ '<br><span class="text-[10px] text-gray-400" title="' + escapeHTML(j.uptime||'') + '">' + escapeHTML((j.uptime||'').slice(0,60)) + '</span>';
|
||
termLine('✓', host + ' revenu en ' + dur + 's · ' + (j.uptime||''));
|
||
return;
|
||
}
|
||
const status = j.tcp22 ? 'TCP/22 OK · SSH KO' : 'pas joignable';
|
||
const elapsed = Math.round((Date.now()-t0)/1000);
|
||
cell.innerHTML = '<span class="text-cyber-yellow">⏳ ' + status + ' · ' + elapsed + 's</span>';
|
||
if (status !== last) {
|
||
termLine('⏳', host + ' : ' + status + ' (t+' + elapsed + 's)');
|
||
last = status;
|
||
}
|
||
} catch(e) { /* retry */ }
|
||
}
|
||
tr._reconData = {ok: false};
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ timeout 10 min</span>';
|
||
termLine('✗', host + ' : timeout 10 min sans reconnexion');
|
||
}));
|
||
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;
|
||
setBtnState(btnPost, 'running'); setStepState('post', 'running');
|
||
let okCount = 0, warnCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const host = tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
termSection('post-compare ' + host);
|
||
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>'; termLine('✓', summ); }
|
||
else if (st === 'warn') { warnCount++; cell.innerHTML = '<span class="text-cyber-yellow" title="' + escapeHTML(j.stdout||'') + '">⚠ ' + escapeHTML(summ) + '</span>'; termLine('⚠', summ); }
|
||
else { koCount++; cell.innerHTML = '<span class="text-cyber-red" title="' + escapeHTML(j.stdout||j.detail||'') + '">✗ ' + escapeHTML(summ) + '</span>'; termLine('✗', summ); }
|
||
if (rep.services_disparus && rep.services_disparus.length)
|
||
appendTermHTML(' <span class="t-ko">services disparus :</span> ' + escapeHTML(rep.services_disparus.join(', ')) + '\n');
|
||
if (rep.services_apparus && rep.services_apparus.length)
|
||
appendTermHTML(' <span class="t-warn">services apparus :</span> ' + escapeHTML(rep.services_apparus.join(', ')) + '\n');
|
||
if (rep.ports_disparus && rep.ports_disparus.length)
|
||
appendTermHTML(' <span class="t-ko">ports disparus :</span> ' + escapeHTML(rep.ports_disparus.join(', ')) + '\n');
|
||
if (rep.ports_apparus && rep.ports_apparus.length)
|
||
appendTermHTML(' <span class="t-warn">ports apparus :</span> ' + escapeHTML(rep.ports_apparus.join(', ')) + '\n');
|
||
} catch(e) {
|
||
koCount++;
|
||
cell.innerHTML = '<span class="text-cyber-red">✗ erreur</span>';
|
||
termLine('✗', 'exception : ' + e.message);
|
||
}
|
||
}
|
||
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; }
|
||
|
||
// ─── Gate Prévenance PCT ─────────────────────────────────────
|
||
// Pour chaque serveur cible avec pct_required=1 et pct_confirmed=0,
|
||
// demander la confirmation que la PCT a été prévenue.
|
||
const pctPending = targets.filter(tr =>
|
||
tr.dataset.pctRequired === '1' && tr.dataset.pctConfirmed !== '1'
|
||
);
|
||
if (pctPending.length) {
|
||
const list = pctPending.map(tr =>
|
||
' • ' + (tr.querySelector('td:nth-child(2)').textContent.trim()
|
||
|| tr.querySelector('td:nth-child(1)').textContent.trim())
|
||
).join('\n');
|
||
const msg = '⚠ PRÉVENANCE PCT requise pour ' + pctPending.length + ' serveur(s) :\n\n'
|
||
+ list + '\n\n'
|
||
+ 'La PCT doit avoir été prévenue EN AMONT de cette intervention.\n'
|
||
+ 'Confirmer que la prévenance PCT a bien été faite pour ces serveurs ?';
|
||
if (!confirm(msg)) {
|
||
alert('Patch annulé. Préviens la PCT puis relance le step 3.');
|
||
return;
|
||
}
|
||
// Re-confirm pour éviter le clic réflexe
|
||
if (!confirm('Tu confirmes une 2e fois que la PCT est prévenue pour ces ' + pctPending.length + ' serveur(s) ? (cette confirmation est tracée en BDD)')) {
|
||
alert('Patch annulé.');
|
||
return;
|
||
}
|
||
// Marquer en BDD
|
||
const rowIds = pctPending.map(tr => tr.dataset.rowId).join(',');
|
||
try {
|
||
const fd = new FormData(); fd.append('row_ids', rowIds);
|
||
const r = await fetch('/patching/iexec/confirm-pct', {
|
||
method: 'POST', credentials: 'same-origin', body: fd,
|
||
});
|
||
const j = await r.json();
|
||
if (!j.ok) { alert('Échec enregistrement confirmation PCT: ' + (j.msg || '')); return; }
|
||
// Mettre à jour DOM : badges en vert, attribut pct_confirmed=1
|
||
pctPending.forEach(tr => {
|
||
tr.dataset.pctConfirmed = '1';
|
||
const badge = tr.querySelector('.cell-pct-badge');
|
||
if (badge) {
|
||
badge.classList.remove('badge-orange');
|
||
badge.classList.add('badge-green');
|
||
badge.textContent = '✅ PCT ok';
|
||
}
|
||
});
|
||
} catch (e) {
|
||
alert('Erreur réseau confirm-pct: ' + e); return;
|
||
}
|
||
}
|
||
// ─── Fin gate PCT ────────────────────────────────────────────
|
||
|
||
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;
|
||
setBtnState(btnStep3, 'running'); setStepState('patch', 'running');
|
||
let okCount = 0, koCount = 0;
|
||
for (const tr of targets) {
|
||
const host = tr.querySelector('td:nth-child(2)').textContent.trim()
|
||
|| tr.querySelector('td:nth-child(3)').textContent.trim();
|
||
tr.querySelector('.cell-patch').innerHTML = '<span class="text-cyber-yellow">… patch (live)</span>';
|
||
const result = await streamYum(tr.dataset.rowId, 'update', host);
|
||
updateYumCell(tr.dataset.rowId, 'update', host, result);
|
||
if (result.ok) okCount++; else koCount++;
|
||
}
|
||
summary.innerHTML += ' · Patch : ✓ ' + okCount + ' / ✗ ' + koCount;
|
||
refreshStepButtons();
|
||
});
|
||
|
||
// Init états visuels au chargement
|
||
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>
|
||
|
||
{# Modal de patching_notes (affiché au clic sur badge ⚠ note) #}
|
||
<div id="patching-note-modal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50" onclick="if(event.target===this)closePatchingNote()">
|
||
<div class="card p-5 max-w-3xl w-full max-h-[80vh] overflow-y-auto m-4">
|
||
<div class="flex justify-between items-center mb-3 border-b border-cyber-border pb-2">
|
||
<h3 class="text-cyber-orange font-bold text-lg">⚠ Particularités de patching — <span id="patching-note-title"></span></h3>
|
||
<button type="button" onclick="closePatchingNote()" class="text-gray-400 hover:text-white text-xl">✕</button>
|
||
</div>
|
||
<pre id="patching-note-body" class="text-xs whitespace-pre-wrap text-gray-200 font-mono"></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function showPatchingNote(btn, asset) {
|
||
document.getElementById('patching-note-title').textContent = asset;
|
||
document.getElementById('patching-note-body').textContent = btn.dataset.note || '';
|
||
const m = document.getElementById('patching-note-modal');
|
||
m.classList.remove('hidden'); m.classList.add('flex');
|
||
}
|
||
function closePatchingNote() {
|
||
const m = document.getElementById('patching-note-modal');
|
||
m.classList.add('hidden'); m.classList.remove('flex');
|
||
}
|
||
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePatchingNote(); });
|
||
</script>
|
||
{% endblock %}
|