[IMP] script todo: nb de VM dans le prompt parallèle + IDs de suivi (k/N)

Sur un gros parc (ex. 26 VM), on ne voyait ni combien de VM allaient être
déployées ni lesquelles étaient en cours (résultats parallèles dans le
désordre). Deux améliorations :

- Le prompt de parallélisme affiche le NOMBRE de VM à déployer :
  « Déploiements en parallèle (défaut : 24, 26 VM) : ». Pour cela, la
  séparation à-créer / déjà-existantes est faite AVANT le prompt (on connaît
  le vrai compte).
- Chaque VM reçoit un ID « k/N » suivant l'ordre de préparation, affiché
  dans les résultats de déploiement («  [3/26] nom ») et dans la résolution
  d'IP (« [5/30] nom : ip »). L'ID reste stable même si les résultats
  reviennent dans le désordre -> on suit lesquelles sont traitées.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-29 08:33:50 +00:00
parent 372aedb0eb
commit a74b0c0c4c
2 changed files with 43 additions and 21 deletions

View file

@ -1642,13 +1642,15 @@ 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): def _qemu_resolve_ips(self, names, labels=None):
"""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.
Évite l'attente EN SÉRIE et SANS sortie qui donnait l'impression d'un Évite l'attente EN SÉRIE et SANS sortie qui donnait l'impression d'un
blocage (dashboard qui « n'ouvre jamais »).""" blocage (dashboard qui « n'ouvre jamais »)."""
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
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 = {}
workers = min(len(names), (os.cpu_count() or 4)) or 1 workers = min(len(names), (os.cpu_count() or 4)) or 1
@ -1661,7 +1663,8 @@ class TODO:
except Exception: except Exception:
ip = None ip = None
result[n] = ip result[n] = ip
print(f" {n}: {ip or t('no IP')}") prefix = f"[{labels[n]}] " if n in labels else ""
print(f" {prefix}{n}: {ip or t('no IP')}")
return result return result
def _qemu_vm_arch(self, name): def _qemu_vm_arch(self, name):
@ -2256,37 +2259,48 @@ class TODO:
input(t("Add each VM to ~/.ssh/config? (y/N): ")) input(t("Add each VM to ~/.ssh/config? (y/N): "))
) )
# Nombre de déploiements en parallèle. Défaut = nombre de CPU de # 5) Sépare les VM à CRÉER des déjà existantes AVANT de proposer le
# l'hôte (borné par le nombre de VM ; repli sur 4 si indéterminé). # parallélisme : on connaît alors le vrai nombre à déployer (affiché
default_par = min(len(selected), os.cpu_count() or 4) # dans le prompt) et on peut numéroter chaque tâche.
pending = [] # (name, d, v, ram, disk, a)
deployed = []
for i, (d, v, ram, disk, a) in enumerate(selected):
name = names[i]
if self._qemu_domain_exists(name):
print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name)
else:
pending.append((name, d, v, ram, disk, a))
n_jobs = len(pending)
# Nombre de déploiements en parallèle. Défaut = nombre de CPU de l'hôte
# (borné par le nombre de VM). On affiche aussi le NB DE VM à déployer.
default_par = min(n_jobs, os.cpu_count() or 4) or 1
raw = input( raw = input(
f"{t('Parallel deployments (default:')} {default_par}): " f"{t('Parallel deployments (default:')} {default_par}, "
f"{n_jobs} {t('VMs')}): "
).strip() ).strip()
try: try:
parallelism = max(1, int(raw)) if raw else default_par parallelism = max(1, int(raw)) if raw else default_par
except ValueError: except ValueError:
parallelism = default_par parallelism = default_par
# 5) Confirmation puis déploiement (en parallèle). # Confirmation puis déploiement (en parallèle).
ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}") ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}")
if not self._is_yes(ans): if not self._is_yes(ans):
print(t("Cancelled.")) print(t("Cancelled."))
return return
deployed = [] # Jobs numérotés (k/N) : l'ID suit l'ORDRE de préparation, stable même
jobs = [] # (name, parts) des VM à créer # si les résultats reviennent dans le désordre (exécution parallèle).
for i, (d, v, ram, disk, a) in enumerate(selected): jobs = [] # (id, name, parts)
name = names[i] for k, (name, d, v, ram, disk, a) in enumerate(pending, 1):
if self._qemu_domain_exists(name):
print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name)
continue
eram, evcpus = eff_res(ram) eram, evcpus = eff_res(ram)
parts = self._qemu_build_deploy_parts( parts = self._qemu_build_deploy_parts(
d, v, a, name, eram, evcpus, disk, d, v, a, name, eram, evcpus, disk,
ssh_key, install_branch, dry_run=False, ssh_key, install_branch, dry_run=False,
) )
jobs.append((name, parts)) jobs.append((f"{k}/{n_jobs}", name, parts))
if jobs: if jobs:
from concurrent.futures import ( from concurrent.futures import (
@ -2301,17 +2315,17 @@ class TODO:
) )
def _run(job): def _run(job):
jname, jparts = job jid, jname, jparts = job
res = subprocess.run(jparts, capture_output=True, text=True) res = subprocess.run(jparts, capture_output=True, text=True)
out = (res.stdout or "") + (res.stderr or "") out = (res.stdout or "") + (res.stderr or "")
return jname, res.returncode, out return jid, jname, res.returncode, out
with ThreadPoolExecutor(max_workers=workers) as pool: with ThreadPoolExecutor(max_workers=workers) as pool:
futures = [pool.submit(_run, j) for j in jobs] futures = [pool.submit(_run, j) for j in jobs]
for fut in as_completed(futures): for fut in as_completed(futures):
jname, rc, out = fut.result() jid, jname, rc, out = fut.result()
mark = "" if rc == 0 else "" mark = "" if rc == 0 else ""
print(f"\n{mark} {jname} (rc={rc})") print(f"\n{mark} [{jid}] {jname} (rc={rc})")
tail = [ln for ln in out.strip().splitlines() if ln][-4:] tail = [ln for ln in out.strip().splitlines() if ln][-4:]
for ln in tail: for ln in tail:
print(f" {ln}") print(f" {ln}")
@ -2323,7 +2337,11 @@ class TODO:
# émulée SANS sortie -> le dashboard « n'ouvrait jamais ». # émulée SANS sortie -> le dashboard « n'ouvrait jamais ».
ip_map = {} ip_map = {}
if deployed and (add_ssh_config or install_branch): if deployed and (add_ssh_config or install_branch):
ip_map = self._qemu_resolve_ips(deployed) labels = {
nm: f"{k}/{len(deployed)}"
for k, nm in enumerate(deployed, 1)
}
ip_map = self._qemu_resolve_ips(deployed, labels)
if add_ssh_config: if add_ssh_config:
for name in deployed: for name in deployed:

View file

@ -1633,6 +1633,10 @@ TRANSLATIONS = {
"fr": "Déploiements en parallèle (défaut :", "fr": "Déploiements en parallèle (défaut :",
"en": "Parallel deployments (default:", "en": "Parallel deployments (default:",
}, },
"VMs": {
"fr": "VM",
"en": "VMs",
},
"Deploying": { "Deploying": {
"fr": "Déploiement de", "fr": "Déploiement de",
"en": "Deploying", "en": "Deploying",