192 lines
9.4 KiB
HTML
192 lines
9.4 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-border text-gray-500">2. Snapshot</span>
|
||
<span class="text-gray-500">→</span>
|
||
<span class="px-3 py-1 rounded bg-cyber-border text-gray-500">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>
|
||
</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>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="11" 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">
|
||
<span id="run-summary" class="text-xs text-gray-400"></span>
|
||
<button id="btn-step2" class="btn-sm bg-cyber-green/20 text-cyber-green px-4 py-2 text-xs" disabled>
|
||
→ Step 2 (snapshot) — à brancher
|
||
</button>
|
||
</div>
|
||
|
||
<script>
|
||
(function(){
|
||
const btnRun = document.getElementById('btn-run-all');
|
||
const btnStep2 = document.getElementById('btn-step2');
|
||
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);
|
||
});
|
||
|
||
// 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 %}
|