Integrate claude, improve selenium and support qemu #1

Open
mathben wants to merge 224 commits from develop into master
2 changed files with 81 additions and 36 deletions
Showing only changes of commit 51a9e7b97c - Show all commits

View file

@ -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
bail dnsmasq est absent) ;
- arp : table ARP de l'hôte (VM active sur le réseau).
On combine pour ne jamais rater une IP que le bail seul manquerait
(cas observé : 30 VM émulées, bail dnsmasq vide alors que la VM a une
IP)."""
ips = []
for source in ("lease", "agent", "arp"):
try: try:
res = subprocess.run( res = subprocess.run(
["sudo", "virsh", "domifaddr", name, "--source", "lease"], ["sudo", "virsh", "domifaddr", name, "--source", source],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=15, timeout=15,
) )
except (OSError, subprocess.SubprocessError): except (OSError, subprocess.SubprocessError):
return [] continue
return re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout) 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
while pending:
try:
for fut in as_completed(list(pending), timeout=30):
pending.discard(fut)
n = futs[fut] n = futs[fut]
try: try:
ip = fut.result() ip = fut.result()
except Exception: except Exception:
ip = None ip = None
result[n] = ip result[n] = ip
done += 1
tag = f"[{labels[n]}] " if n in labels else "" tag = f"[{labels[n]}] " if n in labels else ""
dur = self._fmt_dur(time.time() - starts[n]) dur = self._fmt_dur(time.time() - starts[n])
print( print(
f" [{done}/{len(names)}] {tag}{n}: " f" [{done}/{len(names)}] {tag}{n}: "
f"{ip or t('no IP')} ({dur})" 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)} "

View file

@ -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",