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 from __future__ import annotations
import logging import logging
import re
import socket import socket
import time import time
from typing import Callable, Dict, List, Any from typing import Callable, Dict, List, Any
@ -30,6 +31,10 @@ log = logging.getLogger("patchcenter.prepatch")
# Timeout par commande SSH # Timeout par commande SSH
EXEC_TIMEOUT = 15 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 # Satellites SANEF par zone réseau
SATELLITE_LAN = "vpdsiasat2.sanef.groupe" SATELLITE_LAN = "vpdsiasat2.sanef.groupe"
SATELLITE_DMZ = "vpdsiasat1.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]: def check_satellite(ctx: Dict[str, Any]) -> Dict[str, Any]:
"""Vérifie : """Vérifie :
1. la joignabilité du Satellite cible (LAN ou DMZ selon Domaine) 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") sat_reachable = http_code in ("200", "301", "302", "403")
# 2) subscription-manager identity # 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") 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 # 3) yum repolist enabled --quiet
r2 = _exec(client, "sudo -n yum repolist enabled --quiet 2>&1 | head -50") 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]]] = { CHECKS: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {
"dns": check_dns, "dns": check_dns,
"ssh": check_ssh, "ssh": check_ssh,
"disk": check_disk,
"satellite": check_satellite, "satellite": check_satellite,
} }

View File

@ -46,6 +46,7 @@
<th class="text-left p-1">Excludes</th> <th class="text-left p-1">Excludes</th>
<th class="text-left p-1">DNS</th> <th class="text-left p-1">DNS</th>
<th class="text-left p-1">SSH</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">Satellite</th>
<th class="text-left p-1">Verdict</th> <th class="text-left p-1">Verdict</th>
</tr> </tr>
@ -61,11 +62,12 @@
<td class="p-1 text-cyber-yellow">{{ r.effective_excludes or '(aucun)' }}</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-dns text-gray-500">·</td>
<td class="p-1 cell-ssh 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-sat text-gray-500">·</td>
<td class="p-1 cell-overall text-gray-500">en attente</td> <td class="p-1 cell-overall text-gray-500">en attente</td>
</tr> </tr>
{% else %} {% 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 %} {% endfor %}
</tbody> </tbody>
</table> </table>
@ -93,6 +95,11 @@
const detailsCard = document.getElementById('details-card'); const detailsCard = document.getElementById('details-card');
const detailsPane = document.getElementById('details-pane'); 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){ function statusBadge(st){
if (st === 'ok') return '<span class="text-cyber-green">✓ OK</span>'; if (st === 'ok') return '<span class="text-cyber-green">✓ OK</span>';
if (st === 'warn')return '<span class="text-cyber-yellow">⚠ WARN</span>'; if (st === 'warn')return '<span class="text-cyber-yellow">⚠ WARN</span>';
@ -112,6 +119,7 @@
if (!isLinux(osStr)) { if (!isLinux(osStr)) {
tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-ssh').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-sat').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="Workflow Linux uniquement">⊘ Windows</span>'; tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="Workflow Linux uniquement">⊘ Windows</span>';
return {overall: 'unsupported'}; return {overall: 'unsupported'};
@ -127,6 +135,7 @@
if (j.overall === 'unsupported') { if (j.overall === 'unsupported') {
tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported'); tr.querySelector('.cell-dns').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-ssh').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-sat').innerHTML = statusBadge('unsupported');
tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="' + (j.skipped_reason||'') + '">⊘ N/A</span>'; tr.querySelector('.cell-overall').innerHTML = '<span class="text-gray-400" title="' + (j.skipped_reason||'') + '">⊘ N/A</span>';
return j; return j;
@ -135,6 +144,7 @@
(j.checks || []).forEach(c => byName[c.name] = c); (j.checks || []).forEach(c => byName[c.name] = c);
tr.querySelector('.cell-dns').innerHTML = byName.dns ? statusBadge(byName.dns.status) : ''; 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-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-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.querySelector('.cell-overall').innerHTML = statusBadge(j.overall) + ' <span class="text-[10px] text-gray-500">(' + (j.duration_ms||0) + 'ms)</span>';
tr._checkData = j; tr._checkData = j;