feat(patching/iexec): check espace disque (/ >= 1.5Go, /var/log >= 1Go) + fix detection subscription-manager identity FR/EN via UUID regex

This commit is contained in:
Pierre & Lumière 2026-05-04 15:37:12 +02:00
parent 11bbda5a27
commit b07a6816d4
2 changed files with 80 additions and 2 deletions

View File

@ -19,6 +19,7 @@ calcule un verdict global.
from __future__ import annotations
import logging
import re
import socket
import time
from typing import Callable, Dict, List, Any
@ -30,6 +31,10 @@ log = logging.getLogger("patchcenter.prepatch")
# Timeout par commande SSH
EXEC_TIMEOUT = 15
# Seuils espace disque (en Mo)
DISK_MIN_ROOT_MB = 1500 # 1.5 Go sur /
DISK_MIN_VARLOG_MB = 1000 # 1 Go sur /var/log
# Satellites SANEF par zone réseau
SATELLITE_LAN = "vpdsiasat2.sanef.groupe"
SATELLITE_DMZ = "vpdsiasat1.sanef.groupe"
@ -127,6 +132,65 @@ def check_ssh(ctx: Dict[str, Any]) -> Dict[str, Any]:
}
def _disk_avail_mb(client, path: str):
"""Renvoie l'espace dispo en Mo sur le FS contenant `path`, ou None si KO."""
r = _exec(client, f"sudo -n df -BM --output=avail {path} 2>&1 | tail -n +2")
out = (r["stdout"] or "").strip()
m = re.search(r"(\d+)\s*M", out)
if m:
return int(m.group(1))
return None
def check_disk(ctx: Dict[str, Any]) -> Dict[str, Any]:
"""Vérifie l'espace disque dispo :
- / >= 1.5 Go
- /var/log >= 1 Go
KO si insuffisant pas éligible au snapshot.
"""
client = ctx.get("client")
if client is None:
return {
"name": "disk",
"label": f"Espace disque (/ ≥ {DISK_MIN_ROOT_MB}M, /var/log ≥ {DISK_MIN_VARLOG_MB}M)",
"status": "ko",
"message": "SSH KO en amont",
"details": "",
}
root_mb = _disk_avail_mb(client, "/")
var_mb = _disk_avail_mb(client, "/var/log")
issues = []
parts = []
if root_mb is None:
issues.append("/ : mesure impossible")
else:
parts.append(f"/ {root_mb}M")
if root_mb < DISK_MIN_ROOT_MB:
issues.append(f"/ {root_mb}M < min {DISK_MIN_ROOT_MB}M")
if var_mb is None:
issues.append("/var/log : mesure impossible")
else:
parts.append(f"/var/log {var_mb}M")
if var_mb < DISK_MIN_VARLOG_MB:
issues.append(f"/var/log {var_mb}M < min {DISK_MIN_VARLOG_MB}M")
label = f"Espace disque (/ ≥ {DISK_MIN_ROOT_MB}M, /var/log ≥ {DISK_MIN_VARLOG_MB}M)"
details = (
f"$ sudo df -BM --output=avail / → {root_mb if root_mb is not None else 'N/A'}M\n"
f"$ sudo df -BM --output=avail /var/log → {var_mb if var_mb is not None else 'N/A'}M"
)
if issues:
return {
"name": "disk", "label": label, "status": "ko",
"message": " · ".join(issues),
"details": details,
}
return {
"name": "disk", "label": label, "status": "ok",
"message": " · ".join(parts) + " (au-dessus seuils)",
"details": details,
}
def check_satellite(ctx: Dict[str, Any]) -> Dict[str, Any]:
"""Vérifie :
1. la joignabilité du Satellite cible (LAN ou DMZ selon Domaine)
@ -154,8 +218,11 @@ def check_satellite(ctx: Dict[str, Any]) -> Dict[str, Any]:
sat_reachable = http_code in ("200", "301", "302", "403")
# 2) subscription-manager identity
# Locale-indépendant : on cherche un UUID dans la sortie (présent en EN comme en FR).
r1 = _exec(client, "sudo -n subscription-manager identity 2>&1")
sub_ok = (r1["rc"] == 0 and "system identity" in r1["stdout"].lower())
sub_ok = (r1["rc"] == 0 and bool(re.search(
r"\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b",
r1["stdout"], re.IGNORECASE)))
# 3) yum repolist enabled --quiet
r2 = _exec(client, "sudo -n yum repolist enabled --quiet 2>&1 | head -50")
@ -205,6 +272,7 @@ def check_satellite(ctx: Dict[str, Any]) -> Dict[str, Any]:
CHECKS: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {
"dns": check_dns,
"ssh": check_ssh,
"disk": check_disk,
"satellite": check_satellite,
}

View File

@ -46,6 +46,7 @@
<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>
@ -61,11 +62,12 @@
<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="10" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
<tr><td colspan="11" class="p-2 text-gray-500">Aucune ligne éligible.</td></tr>
{% endfor %}
</tbody>
</table>
@ -93,6 +95,11 @@
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>';
@ -112,6 +119,7 @@
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'};
@ -127,6 +135,7 @@
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;
@ -135,6 +144,7 @@
(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;