[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.
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):
"""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}.
@ -1653,18 +1661,33 @@ class TODO:
labels = labels or {}
print(f"\n{t('Resolving VM IPs (parallel, emulated boot is slow)...')}")
result = {}
t0 = time.time()
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):
starts = {}
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]
try:
ip = fut.result()
except Exception:
ip = None
result[n] = ip
prefix = f"[{labels[n]}] " if n in labels else ""
print(f" {prefix}{n}: {ip or t('no IP')}")
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})"
)
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
def _qemu_vm_arch(self, name):
@ -2302,6 +2325,8 @@ class TODO:
)
jobs.append((f"{k}/{n_jobs}", name, parts))
deploy_start = time.time()
n_ok = 0
if jobs:
from concurrent.futures import (
ThreadPoolExecutor,
@ -2316,21 +2341,33 @@ class TODO:
def _run(job):
jid, jname, jparts = job
j0 = time.time()
res = subprocess.run(jparts, capture_output=True, text=True)
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:
futures = [pool.submit(_run, j) for j in jobs]
for fut in as_completed(futures):
jid, jname, rc, out = fut.result()
# done = ordre de COMPLÉTION (les résultats reviennent dans le
# 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 ""
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:]
for ln in tail:
print(f" {ln}")
if rc == 0:
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 +
# 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)
)
# 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" {t('Default login:')} erplibre / erplibre")
print(f" {t('Manage with:')} sudo virsh list --all")

View file

@ -1637,6 +1637,34 @@ TRANSLATIONS = {
"fr": "VM",
"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": {
"fr": "Déploiement de",
"en": "Deploying",