[FIX] script todo: résolution d'IP en parallèle (dashboard ne s'ouvrait jamais)

Après un déploiement infra, la résolution d'IP tournait EN SÉRIE (boucle
ssh_config puis install), jusqu'à 10 min par VM émulée, SANS aucune sortie :
le dashboard « n'ouvrait jamais » (impression de blocage), d'autant qu'une
VM qui ne boote pas (IP jamais attribuée) bloquait tout le reste.

Fix : _qemu_resolve_ips résout les IP de toutes les VM EN PARALLÈLE, avec
progression affichée par VM, une seule fois, réutilisée pour ~/.ssh/config
ET l'installation (plus de double résolution). _qemu_install_erplibre_*
acceptent une IP/ip_map déjà résolue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-28 09:22:46 +00:00
parent 7bd44d21a9
commit b49e107b83

View file

@ -1763,6 +1763,28 @@ class TODO:
# Meilleur effort : le dernier bail (le plus récent) plutôt que le 1er. # Meilleur effort : le dernier bail (le plus récent) plutôt que le 1er.
return cands[-1] if cands else None return cands[-1] if cands else None
def _qemu_resolve_ips(self, names):
"""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}.
Évite l'attente EN SÉRIE et SANS sortie qui donnait l'impression d'un
blocage (dashboard qui « n'ouvre jamais »)."""
from concurrent.futures import ThreadPoolExecutor, as_completed
print(f"\n{t('Resolving VM IPs (parallel, emulated boot is slow)...')}")
result = {}
workers = min(len(names), (os.cpu_count() or 4)) or 1
with ThreadPoolExecutor(max_workers=workers) as pool:
futs = {pool.submit(self._qemu_vm_ip, n): n for n in names}
for fut in as_completed(futs):
n = futs[fut]
try:
ip = fut.result()
except Exception:
ip = None
result[n] = ip
print(f" {n}: {ip or t('no IP')}")
return result
def _qemu_vm_arch(self, name): def _qemu_vm_arch(self, name):
"""Architecture d'une VM (jeton amd64/arm64/s390x) via virsh dumpxml.""" """Architecture d'une VM (jeton amd64/arm64/s390x) via virsh dumpxml."""
try: try:
@ -1952,10 +1974,10 @@ class TODO:
f"make {self.ERPLIBRE_ODOO_TARGET}" f"make {self.ERPLIBRE_ODOO_TARGET}"
) )
def _qemu_install_erplibre_monitored(self, names, branch): def _qemu_install_erplibre_monitored(self, names, branch, ip_map=None):
"""Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre """Lance l'install ERPLibre en parallèle DÉTACHÉE sur les VM et ouvre
le dashboard Textual. Quitter le dashboard n'arrête pas les installs. le dashboard Textual. Quitter le dashboard n'arrête pas les installs.
""" `ip_map` : IP déjà résolues (sinon on résout ici, EN PARALLÈLE)."""
from script.todo.qemu_install_monitor import ( from script.todo.qemu_install_monitor import (
launch_installs, launch_installs,
run_monitor, run_monitor,
@ -1966,10 +1988,11 @@ class TODO:
mod = self._qemu_import_module() mod = self._qemu_import_module()
except Exception: except Exception:
mod = None mod = None
if ip_map is None:
ip_map = self._qemu_resolve_ips(names)
vms = [] vms = []
for name in names: for name in names:
print(f" {name}: {t('resolving IP...')}") ip = ip_map.get(name)
ip = self._qemu_vm_ip(name)
if ip: if ip:
d, v, a = ( d, v, a = (
self._qemu_vm_meta(name, mod) self._qemu_vm_meta(name, mod)
@ -2012,10 +2035,12 @@ class TODO:
# Commande prête à copier pour relire/partager tous les logs. # Commande prête à copier pour relire/partager tous les logs.
print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log") print(f" {t('Read the logs:')} tail -n +1 {logdir}/*.log")
def _qemu_install_erplibre_vm(self, name, ssh_key, branch): def _qemu_install_erplibre_vm(self, name, ssh_key, branch, ip=None):
"""Clone ERPLibre (branche donnée) dans ~/git/erplibre de la VM puis """Clone ERPLibre (branche donnée) dans ~/git/erplibre de la VM puis
exécute « make install_os » et « make install_odoo_18 » (streamé).""" exécute « make install_os » et « make install_odoo_18 » (streamé).
ip = self._qemu_vm_ip(name) `ip` : IP déjà résolue (sinon on la résout ici)."""
if ip is None:
ip = self._qemu_vm_ip(name)
if not ip: if not ip:
print( print(
f" {name}: {t('no IP obtained, ERPLibre install skipped.')}" f" {name}: {t('no IP obtained, ERPLibre install skipped.')}"
@ -2316,10 +2341,16 @@ class TODO:
if rc == 0: if rc == 0:
deployed.append(jname) deployed.append(jname)
# 6) ~/.ssh/config (une fois les VM démarrées et l'IP attribuée). # 6) Résolution des IP EN PARALLÈLE (réutilisée pour ssh_config +
# install) : une boucle EN SÉRIE bloquait plusieurs minutes par VM
# émulée SANS sortie -> le dashboard « n'ouvrait jamais ».
ip_map = {}
if deployed and (add_ssh_config or install_branch):
ip_map = self._qemu_resolve_ips(deployed)
if add_ssh_config: if add_ssh_config:
for name in deployed: for name in deployed:
ip = self._qemu_vm_ip(name) ip = ip_map.get(name)
if ip: if ip:
self._write_ssh_config_entry(name, "erplibre", ip) self._write_ssh_config_entry(name, "erplibre", ip)
@ -2327,7 +2358,9 @@ class TODO:
if install_branch: if install_branch:
if install_monitor: if install_monitor:
# Installs détachées en parallèle + dashboard Textual. # Installs détachées en parallèle + dashboard Textual.
self._qemu_install_erplibre_monitored(deployed, install_branch) self._qemu_install_erplibre_monitored(
deployed, install_branch, ip_map
)
else: else:
print( print(
f"\n{t('Installing ERPLibre on each VM')} " f"\n{t('Installing ERPLibre on each VM')} "
@ -2335,7 +2368,7 @@ class TODO:
) )
for name in deployed: for name in deployed:
self._qemu_install_erplibre_vm( self._qemu_install_erplibre_vm(
name, ssh_key, install_branch name, ssh_key, install_branch, ip_map.get(name)
) )
print(f"\n{t('ERPLibre infra deployment done.')}") print(f"\n{t('ERPLibre infra deployment done.')}")