[IMP] script todo: suffixe d'arch dans le nom + log d'install non vide

1) Nommage : _qemu_infra_name ajoute un suffixe d'architecture quand elle
   diffère de la native de l'hôte -> déployer un s390x sur un hôte amd64
   donne « erplibre-ubuntu-2604-s390x » (au lieu de « erplibre-ubuntu-2604 »).
   Évite les collisions de noms entre archis et rend l'archi visible. Passé
   dans les 3 appelants (VM unique + plan + jobs de l'infra).

2) Log d'installation vide : sur une archi ÉMULÉE (s390x/arm64 sur x86) le
   boot prend plusieurs minutes ; pendant ce temps l'attente n'écrivait rien
   -> le log restait VIDE et paraissait « bloqué ». _launch_one écrit
   désormais un en-tête d'attente immédiat + un battement toutes les ~30 s,
   et l'attente sshd/cloud-init passe de ~12 à ~20 min (boot émulé lent).
   _qemu_wait_ssh (chemin streamé) : timeout porté à 20 min également.

Validé : nommage (amd64/natif sans suffixe, s390x/arm64 suffixés) ; le log
se remplit dès le départ (en-tête + « ... 30s »).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-28 08:15:46 +00:00
parent 88dc1d5409
commit 0930f30cd3
3 changed files with 51 additions and 13 deletions

View file

@ -20,6 +20,14 @@ import subprocess
import time import time
from pathlib import Path from pathlib import Path
try:
from script.todo.todo_i18n import t
except Exception: # pragma: no cover - repli si i18n indisponible
def t(key: str) -> str:
return key
EXIT_MARKER = "__ERPLIBRE_EXIT__" EXIT_MARKER = "__ERPLIBRE_EXIT__"
SSH_OPTS = ( SSH_OPTS = (
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
@ -38,7 +46,8 @@ def _launch_one(ip: str, remote_cmd: str, log_path: str) -> None:
"""Lance une install SSH DÉTACHÉE : attend le sshd, exécute, journalise """Lance une install SSH DÉTACHÉE : attend le sshd, exécute, journalise
la sortie puis écrit le marqueur de fin avec le code de sortie.""" la sortie puis écrit le marqueur de fin avec le code de sortie."""
# Sonde de disponibilité : on attend que sshd réponde ET que cloud-init # Sonde de disponibilité : on attend que sshd réponde ET que cloud-init
# soit TERMINÉ, via des connexions COURTES successives (jusqu'à ~12 min). # soit TERMINÉ, via des connexions COURTES successives (jusqu'à ~20 min :
# une architecture ÉMULÉE, s390x/arm64 sur hôte x86, boote lentement).
# Au 1er boot, cloud-init régénère les clés d'hôte et REDÉMARRE sshd : une # Au 1er boot, cloud-init régénère les clés d'hôte et REDÉMARRE sshd : une
# session SSH longue (ex. « cloud-init status --wait ») serait alors tuée # session SSH longue (ex. « cloud-init status --wait ») serait alors tuée
# (« Connection closed by remote host », exit 255 — cas Fedora). Chaque # (« Connection closed by remote host », exit 255 — cas Fedora). Chaque
@ -49,16 +58,27 @@ def _launch_one(ip: str, remote_cmd: str, log_path: str) -> None:
"if command -v cloud-init >/dev/null 2>&1; then " "if command -v cloud-init >/dev/null 2>&1; then "
"cloud-init status 2>/dev/null || true; else echo nocloudinit; fi" "cloud-init status 2>/dev/null || true; else echo nocloudinit; fi"
) )
log_q = shlex.quote(log_path)
# On écrit un message d'attente + un battement toutes les ~30 s : sinon le
# log reste VIDE pendant tout le boot émulé et paraît « bloqué ».
msg_wait = t("Waiting for the VM to start (boot + cloud-init)")
msg_slow = t("(an emulated architecture can be slow; this is normal)")
msg_ready = t("VM ready - starting the ERPLibre install")
wrapper = ( wrapper = (
f"for i in $(seq 1 144); do " f"echo {shlex.quote('== ' + msg_wait + ' ==')} >> {log_q}; "
f"echo {shlex.quote(' ' + msg_slow)} >> {log_q}; "
f"for i in $(seq 1 240); do "
f"st=$(ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} " f"st=$(ssh {SSH_OPTS} -o BatchMode=yes erplibre@{ip} "
f"{shlex.quote(ci_probe)} 2>/dev/null); " f"{shlex.quote(ci_probe)} 2>/dev/null); "
f'case "$st" in ' f'case "$st" in '
f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; " f"*done*|*disabled*|*error*|*degraded*|*nocloudinit*) break;; "
f"esac; sleep 5; done; " f"esac; "
f'if [ $((i % 6)) -eq 0 ]; then echo " ... $((i*5))s" >> {log_q}; fi; '
f"sleep 5; done; "
f"echo {shlex.quote('== ' + msg_ready + ' ==')} >> {log_q}; "
f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} " f"ssh {SSH_OPTS} erplibre@{ip} {shlex.quote(remote_cmd)} "
f"> {shlex.quote(log_path)} 2>&1; " f">> {log_q} 2>&1; "
f'echo "{EXIT_MARKER} $?" >> {shlex.quote(log_path)}' f'echo "{EXIT_MARKER} $?" >> {log_q}'
) )
# setsid -f : le process survit à la fermeture du menu / du dashboard. # setsid -f : le process survit à la fermeture du menu / du dashboard.
subprocess.Popen( subprocess.Popen(

View file

@ -1041,7 +1041,7 @@ class TODO:
distro = self._qemu_prompt_distro() distro = self._qemu_prompt_distro()
version = self._qemu_prompt_version(distro) version = self._qemu_prompt_version(distro)
arch = self._qemu_prompt_arch(distro) arch = self._qemu_prompt_arch(distro)
default_name = self._qemu_infra_name(distro, version) default_name = self._qemu_infra_name(distro, version, arch)
name = ( name = (
input(f"{t('VM name (default: ')}{default_name}): ").strip() input(f"{t('VM name (default: ')}{default_name}): ").strip()
or default_name or default_name
@ -1608,10 +1608,16 @@ class TODO:
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
return mod return mod
@staticmethod @classmethod
def _qemu_infra_name(distro, version): def _qemu_infra_name(cls, distro, version, arch=None):
"""Nom de VM stable pour le parc, ex. erplibre-ubuntu-2404.""" """Nom de VM stable pour le parc, ex. erplibre-ubuntu-2404. Ajoute un
return f"erplibre-{distro}-{version.replace('.', '')}" suffixe d'architecture quand elle diffère de la native de l'hôte (ex.
erplibre-ubuntu-2604-s390x sur un hôte amd64) pour éviter les collisions
de noms entre archis et rendre l'archi visible."""
base = f"erplibre-{distro}-{version.replace('.', '')}"
if arch and arch != cls._native_arch():
base += f"-{arch}"
return base
@staticmethod @staticmethod
def _parse_disk_gb(size): def _parse_disk_gb(size):
@ -1743,7 +1749,7 @@ class TODO:
ERPLIBRE_EXTRA_DISK_GB = 5 ERPLIBRE_EXTRA_DISK_GB = 5
@staticmethod @staticmethod
def _qemu_wait_ssh(ip, user="erplibre", timeout=720): def _qemu_wait_ssh(ip, user="erplibre", timeout=1200):
"""Attend que sshd réponde ET que cloud-init soit TERMINÉ, via des """Attend que sshd réponde ET que cloud-init soit TERMINÉ, via des
connexions COURTES successives. Au 1er boot, cloud-init régénère les connexions COURTES successives. Au 1er boot, cloud-init régénère les
clés d'hôte et REDÉMARRE sshd : attendre la fin de cloud-init AVANT de clés d'hôte et REDÉMARRE sshd : attendre la fin de cloud-init AVANT de
@ -2049,7 +2055,7 @@ class TODO:
free_ram = self._host_free_ram_mb() free_ram = self._host_free_ram_mb()
print(f"\n{t('Deployment plan')} ({len(selected)} VM) :") print(f"\n{t('Deployment plan')} ({len(selected)} VM) :")
for d, v, ram, disk in selected: for d, v, ram, disk in selected:
name = self._qemu_infra_name(d, v) name = self._qemu_infra_name(d, v, arch)
print(f" - {name:<26} {d} {v:<7} RAM {ram}Mo {t('disk')} {disk}") print(f" - {name:<26} {d} {v:<7} RAM {ram}Mo {t('disk')} {disk}")
print(f"\n {t('Total RAM (all running):')} {total_ram} Mo") print(f"\n {t('Total RAM (all running):')} {total_ram} Mo")
print(f" {t('Total virtual disk (thin qcow2):')} ~{total_disk} G") print(f" {t('Total virtual disk (thin qcow2):')} ~{total_disk} G")
@ -2108,7 +2114,7 @@ class TODO:
deployed = [] deployed = []
jobs = [] # (name, parts) des VM à créer jobs = [] # (name, parts) des VM à créer
for d, v, _ram, disk in selected: for d, v, _ram, disk in selected:
name = self._qemu_infra_name(d, v) name = self._qemu_infra_name(d, v, arch)
if self._qemu_domain_exists(name): if self._qemu_domain_exists(name):
print(f"{name}: {t('already exists, skipped.')}") print(f"{name}: {t('already exists, skipped.')}")
deployed.append(name) deployed.append(name)

View file

@ -1197,6 +1197,18 @@ TRANSLATIONS = {
"fr": "images pour cette architecture disponibles seulement pour :", "fr": "images pour cette architecture disponibles seulement pour :",
"en": "images for this arch only exist for:", "en": "images for this arch only exist for:",
}, },
"Waiting for the VM to start (boot + cloud-init)": {
"fr": "En attente du démarrage de la VM (boot + cloud-init)",
"en": "Waiting for the VM to start (boot + cloud-init)",
},
"(an emulated architecture can be slow; this is normal)": {
"fr": "(une architecture émulée peut être lente ; c'est normal)",
"en": "(an emulated architecture can be slow; this is normal)",
},
"VM ready - starting the ERPLibre install": {
"fr": "VM prête — installation ERPLibre en cours",
"en": "VM ready - starting the ERPLibre install",
},
"Choice (number or name, blank = native):": { "Choice (number or name, blank = native):": {
"fr": "Choix (numéro ou nom, vide = native) :", "fr": "Choix (numéro ou nom, vide = native) :",
"en": "Choice (number or name, blank = native):", "en": "Choice (number or name, blank = native):",