[IMP] script todo: compteur de complétion, durées par étape + sommaire total

Améliore le suivi du déploiement multi-VM :

- Compteur de COMPLÉTION devant chaque résultat (« [1/26] », « [2/26] »… =
  ordre où les tâches TERMINENT), en plus de l'ID de préparation stable
  (« [7/26] ») — utile car l'exécution parallèle rend les résultats
  désordonnés.
- Temps d'exécution PAR ÉTAPE : durée de chaque VM (déploiement, résolution
  d'IP) + bilan de phase (« Bilan déploiement : 24 OK, 2 en échec, 26 VM,
  3m10s » ; « IP résolues : 25/26 (2m). »).
- SOMMAIRE TOTAL encadré en fin de flux (VM déployées, total avec
  existantes, temps total).

Helper _fmt_dur (« 45s » / « 2m05s »).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-29 08:46:48 +00:00
parent da1206bcb2
commit 1e9e5abd1f
2 changed files with 81 additions and 8 deletions

View file

@ -1642,6 +1642,14 @@ 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
@staticmethod
def _fmt_dur(secs):
"""Durée lisible : « 45s » ou « 2m05s »."""
secs = int(secs)
if secs < 60:
return f"{secs}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):
"""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}.
@ -1653,18 +1661,33 @@ class TODO:
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()
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:
futs = {pool.submit(self._qemu_vm_ip, n): n for n in names} starts = {}
for fut in as_completed(futs): futs = {}
for n in names:
starts[n] = time.time()
futs[pool.submit(self._qemu_vm_ip, n)] = n
# done = ordre de complétion ; prefixe l'ID de suivi + la durée.
for done, fut in enumerate(as_completed(futs), 1):
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
prefix = f"[{labels[n]}] " if n in labels else "" tag = f"[{labels[n]}] " if n in labels else ""
print(f" {prefix}{n}: {ip or t('no IP')}") dur = self._fmt_dur(time.time() - starts[n])
print(
f" [{done}/{len(names)}] {tag}{n}: "
f"{ip or t('no IP')} ({dur})"
)
got = sum(1 for ip in result.values() if ip)
print(
f" {t('IPs resolved:')} {got}/{len(names)} "
f"({self._fmt_dur(time.time() - t0)})"
)
return result return result
def _qemu_vm_arch(self, name): def _qemu_vm_arch(self, name):
@ -2302,6 +2325,8 @@ class TODO:
) )
jobs.append((f"{k}/{n_jobs}", name, parts)) jobs.append((f"{k}/{n_jobs}", name, parts))
deploy_start = time.time()
n_ok = 0
if jobs: if jobs:
from concurrent.futures import ( from concurrent.futures import (
ThreadPoolExecutor, ThreadPoolExecutor,
@ -2316,21 +2341,33 @@ class TODO:
def _run(job): def _run(job):
jid, jname, jparts = job jid, jname, jparts = job
j0 = time.time()
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 jid, jname, res.returncode, out return jid, jname, res.returncode, out, time.time() - j0
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): # done = ordre de COMPLÉTION (les résultats reviennent dans le
jid, jname, rc, out = fut.result() # désordre) ; jid = ordre de préparation (stable). Durée par VM.
for done, fut in enumerate(as_completed(futures), 1):
jid, jname, rc, out, secs = fut.result()
mark = "" if rc == 0 else "" mark = "" if rc == 0 else ""
print(f"\n{mark} [{jid}] {jname} (rc={rc})") print(
f"\n[{done}/{len(jobs)}] {mark} [{jid}] {jname} "
f"(rc={rc}, {self._fmt_dur(secs)})"
)
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}")
if rc == 0: if rc == 0:
deployed.append(jname) deployed.append(jname)
n_ok += 1
print(
f"\n{t('Deploy summary:')} {n_ok} OK, "
f"{len(jobs) - n_ok} {t('failed')}, "
f"{len(jobs)} {t('VMs')}, {self._fmt_dur(time.time() - deploy_start)}"
)
# 6) Résolution des IP EN PARALLÈLE (réutilisée pour ssh_config + # 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 # install) : une boucle EN SÉRIE bloquait plusieurs minutes par VM
@ -2366,6 +2403,14 @@ class TODO:
name, ssh_key, install_branch, ip_map.get(name) name, ssh_key, install_branch, ip_map.get(name)
) )
# Sommaire TOTAL (déploiement + résolution IP + ssh_config + install
# synchrone ; l'install monitorée est détachée, non comptée ici).
print(f"\n{'' * 60}")
print(f" {t('TOTAL summary')}")
print(f" {t('VMs deployed:')} {n_ok}/{len(jobs) if jobs else 0}"
f" ({t('total incl. existing:')} {len(deployed)})")
print(f" {t('Total time:')} {self._fmt_dur(time.time() - deploy_start)}")
print(f"{'' * 60}")
print(f"\n{t('ERPLibre infra deployment done.')}") print(f"\n{t('ERPLibre infra deployment done.')}")
print(f" {t('Default login:')} erplibre / erplibre") print(f" {t('Default login:')} erplibre / erplibre")
print(f" {t('Manage with:')} sudo virsh list --all") print(f" {t('Manage with:')} sudo virsh list --all")

View file

@ -1637,6 +1637,34 @@ TRANSLATIONS = {
"fr": "VM", "fr": "VM",
"en": "VMs", "en": "VMs",
}, },
"Deploy summary:": {
"fr": "Bilan déploiement :",
"en": "Deploy summary:",
},
"failed": {
"fr": "en échec",
"en": "failed",
},
"IPs resolved:": {
"fr": "IP résolues :",
"en": "IPs resolved:",
},
"TOTAL summary": {
"fr": "Sommaire TOTAL",
"en": "TOTAL summary",
},
"VMs deployed:": {
"fr": "VM déployées :",
"en": "VMs deployed:",
},
"total incl. existing:": {
"fr": "total avec existantes :",
"en": "total incl. existing:",
},
"Total time:": {
"fr": "Temps total :",
"en": "Total time:",
},
"Deploying": { "Deploying": {
"fr": "Déploiement de", "fr": "Déploiement de",
"en": "Deploying", "en": "Deploying",