[FIX] script todo: résolution d'IP robuste (ping + multi-source + heartbeat)
Sur un gros parc (30 VM, ~18 émulées), la résolution d'IP « bloquait » 10 min en silence sur les VM lentes. Trois causes traitées : - Joignabilité par PING (ICMP) d'abord, TCP:22 en repli. Le ping répond dès que le réseau de la VM est up, BIEN AVANT sshd : on ne retenait pas une VM qui a déjà son IP juste parce que sshd (lent en émulation) n'était pas prêt. Le ping distingue toujours le bail actif (répond) du bail périmé (non). - IP cherchée sur PLUSIEURS sources : lease (dnsmasq), agent (qemu-guest- agent DANS la VM) et arp (table ARP hôte). Le bail dnsmasq peut être vide sous forte charge alors que la VM a une IP (constaté sur ce parc). - BATTEMENT toutes les 30 s listant les VM encore en attente + timeout PAR VM ramené à 5 min (au lieu de 10) dans la phase de résolution -> plus de silence prolongé, et on n'attend pas indéfiniment une VM sans IP. NB : déployer la matrice complète (30 VM dont ~18 émulées TCG) sature un seul hôte ; certaines VM émulées n'obtiennent pas de bail DHCP à temps (limite de capacité, pas un bug). La résolution dégrade désormais proprement. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1e9e5abd1f
commit
51a9e7b97c
2 changed files with 81 additions and 36 deletions
|
|
@ -1566,26 +1566,50 @@ class TODO:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _qemu_lease_candidates(name):
|
def _qemu_lease_candidates(name):
|
||||||
"""Toutes les IPv4 du bail DHCP de la VM. Il peut y en avoir PLUSIEURS :
|
"""Toutes les IPv4 candidates de la VM, agrégées de PLUSIEURS sources :
|
||||||
au boot, l'image cloud demande d'abord une IP avec son hostname par
|
- lease : base DHCP de dnsmasq (peut manquer sous forte charge, ou
|
||||||
défaut (« ubuntu ») -> 1er bail ; puis cloud-init fixe le vrai hostname
|
contenir plusieurs baux : bail précoce « ubuntu » périmé + bail
|
||||||
et le client redemande -> 2e bail (IP différente). Le 1er devient
|
définitif) ;
|
||||||
périmé (« No route to host »)."""
|
- agent : qemu-guest-agent DANS la VM (voit l'IP réelle même quand le
|
||||||
try:
|
bail dnsmasq est absent) ;
|
||||||
res = subprocess.run(
|
- arp : table ARP de l'hôte (VM active sur le réseau).
|
||||||
["sudo", "virsh", "domifaddr", name, "--source", "lease"],
|
On combine pour ne jamais rater une IP que le bail seul manquerait
|
||||||
capture_output=True,
|
(cas observé : 30 VM émulées, bail dnsmasq vide alors que la VM a une
|
||||||
text=True,
|
IP)."""
|
||||||
timeout=15,
|
ips = []
|
||||||
)
|
for source in ("lease", "agent", "arp"):
|
||||||
except (OSError, subprocess.SubprocessError):
|
try:
|
||||||
return []
|
res = subprocess.run(
|
||||||
return re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
["sudo", "virsh", "domifaddr", name, "--source", source],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
continue
|
||||||
|
for ip in re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout):
|
||||||
|
# Ignore la loopback (remontée par --source agent).
|
||||||
|
if ip != "127.0.0.1" and ip not in ips:
|
||||||
|
ips.append(ip)
|
||||||
|
return ips
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _qemu_ip_reachable(ip, port=22, timeout=3):
|
def _qemu_ip_reachable(ip, port=22, timeout=2):
|
||||||
"""Vrai si le port SSH répond (sshd up) : distingue le bail actif du
|
"""Vrai si la VM répond sur cette IP (bail ACTIF, pas périmé). On teste
|
||||||
bail périmé sans dépendre du ping (souvent filtré)."""
|
le PING d'abord : il répond dès que le réseau de la VM est up, BIEN
|
||||||
|
AVANT sshd — sinon on attendait le sshd (lent en émulation) et la
|
||||||
|
résolution semblait « bloquée » alors que la VM a déjà son IP. Repli
|
||||||
|
TCP:port si l'ICMP est filtré."""
|
||||||
|
try:
|
||||||
|
res = subprocess.run(
|
||||||
|
["ping", "-c", "1", "-W", str(int(timeout)), ip],
|
||||||
|
capture_output=True,
|
||||||
|
timeout=timeout + 1,
|
||||||
|
)
|
||||||
|
if res.returncode == 0:
|
||||||
|
return True
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
pass
|
||||||
import socket
|
import socket
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -1650,39 +1674,56 @@ class TODO:
|
||||||
return f"{secs}s"
|
return f"{secs}s"
|
||||||
return f"{secs // 60}m{secs % 60:02d}s"
|
return f"{secs // 60}m{secs % 60:02d}s"
|
||||||
|
|
||||||
def _qemu_resolve_ips(self, names, labels=None):
|
def _qemu_resolve_ips(self, names, labels=None, timeout=300):
|
||||||
"""Résout les IP de plusieurs VM EN PARALLÈLE (le boot émulé est lent),
|
"""Résout les IP de plusieurs VM EN PARALLÈLE (le boot émulé est lent),
|
||||||
en affichant la progression au fur et à mesure. Renvoie {nom: ip|None}.
|
en affichant la progression au fur et à mesure. Renvoie {nom: ip|None}.
|
||||||
`labels` : {nom: « k/N »} pour préfixer chaque ligne d'un ID de suivi.
|
`labels` : {nom: « k/N »} pour préfixer chaque ligne d'un ID de suivi.
|
||||||
Évite l'attente EN SÉRIE et SANS sortie qui donnait l'impression d'un
|
`timeout` : délai max PAR VM (borne l'attente d'une VM sans IP). Un
|
||||||
blocage (dashboard qui « n'ouvre jamais »)."""
|
BATTEMENT toutes les 30 s liste les VM encore en attente -> jamais de
|
||||||
|
silence prolongé qui donne l'impression d'un blocage."""
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from concurrent.futures import TimeoutError as _FTimeout
|
||||||
|
|
||||||
labels = labels or {}
|
labels = labels or {}
|
||||||
print(f"\n{t('Resolving VM IPs (parallel, emulated boot is slow)...')}")
|
print(f"\n{t('Resolving VM IPs (parallel, emulated boot is slow)...')}")
|
||||||
result = {}
|
result = {}
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
|
starts = {}
|
||||||
workers = min(len(names), (os.cpu_count() or 4)) or 1
|
workers = min(len(names), (os.cpu_count() or 4)) or 1
|
||||||
with ThreadPoolExecutor(max_workers=workers) as pool:
|
with ThreadPoolExecutor(max_workers=workers) as pool:
|
||||||
starts = {}
|
|
||||||
futs = {}
|
futs = {}
|
||||||
for n in names:
|
for n in names:
|
||||||
starts[n] = time.time()
|
starts[n] = time.time()
|
||||||
futs[pool.submit(self._qemu_vm_ip, n)] = n
|
futs[pool.submit(self._qemu_vm_ip, n, timeout)] = n
|
||||||
# done = ordre de complétion ; prefixe l'ID de suivi + la durée.
|
pending = set(futs)
|
||||||
for done, fut in enumerate(as_completed(futs), 1):
|
done = 0
|
||||||
n = futs[fut]
|
while pending:
|
||||||
try:
|
try:
|
||||||
ip = fut.result()
|
for fut in as_completed(list(pending), timeout=30):
|
||||||
except Exception:
|
pending.discard(fut)
|
||||||
ip = None
|
n = futs[fut]
|
||||||
result[n] = ip
|
try:
|
||||||
tag = f"[{labels[n]}] " if n in labels else ""
|
ip = fut.result()
|
||||||
dur = self._fmt_dur(time.time() - starts[n])
|
except Exception:
|
||||||
print(
|
ip = None
|
||||||
f" [{done}/{len(names)}] {tag}{n}: "
|
result[n] = ip
|
||||||
f"{ip or t('no IP')} ({dur})"
|
done += 1
|
||||||
)
|
tag = f"[{labels[n]}] " if n in labels else ""
|
||||||
|
dur = self._fmt_dur(time.time() - starts[n])
|
||||||
|
print(
|
||||||
|
f" [{done}/{len(names)}] {tag}{n}: "
|
||||||
|
f"{ip or t('no IP')} ({dur})"
|
||||||
|
)
|
||||||
|
except _FTimeout:
|
||||||
|
# Battement : VM encore en attente (boot/DHCP lent).
|
||||||
|
waiting = [futs[f] for f in pending]
|
||||||
|
shown = ", ".join(waiting[:5])
|
||||||
|
if len(waiting) > 5:
|
||||||
|
shown += "…"
|
||||||
|
print(
|
||||||
|
f" ⏳ {t('still waiting for')} {len(waiting)} VM "
|
||||||
|
f"({self._fmt_dur(time.time() - t0)}): {shown}"
|
||||||
|
)
|
||||||
got = sum(1 for ip in result.values() if ip)
|
got = sum(1 for ip in result.values() if ip)
|
||||||
print(
|
print(
|
||||||
f" {t('IPs resolved:')} {got}/{len(names)} "
|
f" {t('IPs resolved:')} {got}/{len(names)} "
|
||||||
|
|
|
||||||
|
|
@ -1649,6 +1649,10 @@ TRANSLATIONS = {
|
||||||
"fr": "IP résolues :",
|
"fr": "IP résolues :",
|
||||||
"en": "IPs resolved:",
|
"en": "IPs resolved:",
|
||||||
},
|
},
|
||||||
|
"still waiting for": {
|
||||||
|
"fr": "encore en attente de",
|
||||||
|
"en": "still waiting for",
|
||||||
|
},
|
||||||
"TOTAL summary": {
|
"TOTAL summary": {
|
||||||
"fr": "Sommaire TOTAL",
|
"fr": "Sommaire TOTAL",
|
||||||
"en": "TOTAL summary",
|
"en": "TOTAL summary",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue