From 19e3801d2c386338d87d08600ab071c812dbd974 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 19 Jul 2026 08:15:41 +0000 Subject: [PATCH] [IMP] script todo: deploy the ERPLibre infra in parallel The infra fleet was deployed one VM at a time, each blocking up to 90s waiting for its DHCP lease. Deploys now run concurrently through a ThreadPoolExecutor (default min(count, 4), promptable), with each job's output captured and printed per VM as it finishes (no interleaving). VMs are created with --no-wait-ip so workers return quickly; IPs are then collected once for the ~/.ssh/config step. Preferred over shelling out to GNU parallel: no external dependency, grouped output, and it stays integrated with the ssh-config and ERPLibre-clone steps. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/todo.py | 65 +++++++++++++++++++++++++++++++++------- script/todo/todo_i18n.py | 12 ++++++++ 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 92d8a1a..d5406fc 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1433,7 +1433,17 @@ class TODO: input(t("Add each VM to ~/.ssh/config? (y/N): ")) ) - # 5) Confirmation puis déploiement séquentiel. + # Nombre de déploiements en parallèle (défaut : min(nb VM, 4)). + default_par = min(len(selected), 4) + raw = input( + f"{t('Parallel deployments (default:')} {default_par}): " + ).strip() + try: + parallelism = max(1, int(raw)) if raw else default_par + except ValueError: + parallelism = default_par + + # 5) Confirmation puis déploiement (en parallèle). ans = input(f"\n{t('Deploy these VMs now? (y/N): ')}") if not self._is_yes(ans): print(t("Cancelled.")) @@ -1441,10 +1451,11 @@ class TODO: script_path = self._qemu_script_path() deployed = [] + jobs = [] # (name, parts) des VM à créer for d, v, _ram, _disk in selected: name = self._qemu_infra_name(d, v) if self._qemu_domain_exists(name): - print(f"\n⏭ {name}: {t('already exists, skipped.')}") + print(f"⏭ {name}: {t('already exists, skipped.')}") deployed.append(name) continue parts = [ @@ -1456,25 +1467,57 @@ class TODO: v, "--name", name, + # Mot de passe console « erplibre » (+ clé SSH). --no-wait-ip : + # ne bloque pas 90s par VM, l'IP est collectée après coup. + "--password", + "erplibre", + "--no-wait-ip", ] if ssh_key: parts += ["--ssh-key", ssh_key] - # Mot de passe console par défaut « erplibre » (login virsh - # console + SSH par mot de passe), en plus de la clé. - parts += ["--password", "erplibre"] if install_branch: parts += ["--package", "git"] parts.append("-y") - cmd = " ".join(shlex.quote(p) for p in parts) - print(f"\n▶ {name}: {cmd}") - self.execute.exec_command_live(cmd, source_erplibre=False) - deployed.append(name) - if add_ssh_config: + jobs.append((name, parts)) + + if jobs: + from concurrent.futures import ( + ThreadPoolExecutor, + as_completed, + ) + + workers = min(parallelism, len(jobs)) + print( + f"\n{t('Deploying')} {len(jobs)} VM " + f"({t('parallel jobs:')} {workers})…" + ) + + def _run(job): + jname, jparts = job + res = subprocess.run(jparts, capture_output=True, text=True) + out = (res.stdout or "") + (res.stderr or "") + return jname, res.returncode, out + + with ThreadPoolExecutor(max_workers=workers) as pool: + futures = [pool.submit(_run, j) for j in jobs] + for fut in as_completed(futures): + jname, rc, out = fut.result() + mark = "✅" if rc == 0 else "❌" + print(f"\n{mark} {jname} (rc={rc})") + tail = [ln for ln in out.strip().splitlines() if ln][-4:] + for ln in tail: + print(f" {ln}") + if rc == 0: + deployed.append(jname) + + # 6) ~/.ssh/config (une fois les VM démarrées et l'IP attribuée). + if add_ssh_config: + for name in deployed: ip = self._qemu_vm_ip(name) if ip: self._write_ssh_config_entry(name, "erplibre", ip) - # 6) Installation ERPLibre (clone) si demandée. + # 7) Installation ERPLibre (clone) si demandée. if install_branch: print(f"\n{t('Cloning ERPLibre on each VM')} ({install_branch})…") for name in deployed: diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index f634c97..62977b4 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1287,6 +1287,18 @@ TRANSLATIONS = { "fr": "Clonage d'ERPLibre sur chaque VM", "en": "Cloning ERPLibre on each VM", }, + "Parallel deployments (default:": { + "fr": "Déploiements en parallèle (défaut :", + "en": "Parallel deployments (default:", + }, + "Deploying": { + "fr": "Déploiement de", + "en": "Deploying", + }, + "parallel jobs:": { + "fr": "tâches parallèles :", + "en": "parallel jobs:", + }, "ERPLibre infra deployment done.": { "fr": "Déploiement de l'infra ERPLibre terminé.", "en": "ERPLibre infra deployment done.",