From 3f204979125e5f2107f502a05a830115f3511d0a Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Wed, 29 Jul 2026 11:29:53 +0000 Subject: [PATCH] =?UTF-8?q?[IMP]=20script=20todo:=20FS=20invit=C3=A9=20rob?= =?UTF-8?q?uste=20(heartbeat+console),=20temps/version,=20i18n=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois améliorations QEMU : - Extension du FS invité robuste : résolution d'IP avec BATTEMENT (_qemu_resolve_ips, parallèle, boot émulé lent) au lieu d'un timeout court ; en cas d'absence d'IP ou d'échec SSH, repli sur la CONSOLE SÉRIE avec la commande growpart/resize prête à coller (login erplibre/erplibre). Commande factorisée dans _GROW_FS_REMOTE. - Temps estimés PAR VERSION : nouveau mon.avg_by_version(distro, version) et _qemu_stat_avg("version", v, distro) -> suffixe « · ~46s moy (1) » dans les listes de versions (prompt simple + granulaire). - i18n : « Versions for » n'avait AUCUNE traduction (restait en anglais). Ajout FR « Versions des » (+ « Version des ») et capitalisation de la distro -> « Versions des Ubuntu : ». Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/qemu_install_monitor.py | 10 +++ script/todo/todo.py | 100 +++++++++++++++++++--------- script/todo/todo_i18n.py | 22 +++++- 3 files changed, 99 insertions(+), 33 deletions(-) diff --git a/script/todo/qemu_install_monitor.py b/script/todo/qemu_install_monitor.py index 02ada77..67b939d 100644 --- a/script/todo/qemu_install_monitor.py +++ b/script/todo/qemu_install_monitor.py @@ -265,6 +265,16 @@ def avg_by_distro(distro, stats=None): return (sum(secs) / len(secs), len(secs)) if secs else (None, 0) +def avg_by_version(distro, version, stats=None): + """(moyenne_secondes, nb_runs) pour cette (distro, version), ou (None, 0).""" + secs = [ + r["seconds"] + for r in _runs(stats) + if r.get("distro") == distro and r.get("version") == version + ] + return (sum(secs) / len(secs), len(secs)) if secs else (None, 0) + + def last_run(stats=None): """Dernier run enregistré (dict) ou None.""" runs = _runs(stats) diff --git a/script/todo/todo.py b/script/todo/todo.py index 84524cf..585fbd8 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -911,10 +911,11 @@ class TODO: def _qemu_prompt_version(self, distro): """Demande la version pour la distro (défaut = version par défaut).""" versions, default = self._QEMU_DISTROS.get(distro, ([], "")) - print(f"\n{t('Version for')} {distro} :") + print(f"\n{t('Version for')} {distro.capitalize()} :") for i, v in enumerate(versions, 1): suffix = " *" if v == default else "" - print(f" [{i}] {v}{suffix}") + stat = self._qemu_stat_avg("version", v, distro) + print(f" [{i}] {v}{suffix}{stat}") sel = input( f"{t('Choice (number or version, blank = default):')} " ).strip() @@ -978,14 +979,19 @@ class TODO: pass return "" - def _qemu_stat_avg(self, field, value): + def _qemu_stat_avg(self, field, value, distro=None): """Suffixe « · ~5m moy (3) » : durée d'install MOYENNE historique pour - cette archi/distro (fichier .venv.erplibre), ou '' si aucune donnée.""" + cette archi/distro/version (fichier .venv.erplibre), ou '' si aucune + donnée. Pour field='version', `distro` est requis.""" try: from script.todo import qemu_install_monitor as mon - fn = mon.avg_by_arch if field == "arch" else mon.avg_by_distro - secs, n = fn(value) + if field == "arch": + secs, n = mon.avg_by_arch(value) + elif field == "version": + secs, n = mon.avg_by_version(distro, value) + else: + secs, n = mon.avg_by_distro(value) if secs: return f" · ~{mon._fmt_secs(secs)} {t('avg')} ({n})" except Exception: @@ -1582,32 +1588,61 @@ class TODO: print(f"{t('Will execute:')} {cmd}") self.execute.exec_command_live(cmd, source_erplibre=False) + # Commande d'extension du FS racine (partition + FS) réutilisée par SSH + # et par le repli console série. + _GROW_FS_REMOTE = ( + "set -e; " + "root=$(findmnt -no SOURCE /); " + "dev=$(lsblk -no PKNAME \"$root\" | head -1); " + "part=$(echo \"$root\" | grep -oE '[0-9]+$'); " + "sudo growpart /dev/$dev $part || true; " + "fstype=$(findmnt -no FSTYPE /); " + 'case "$fstype" in ' + "ext*) sudo resize2fs \"$root\";; " + "xfs) sudo xfs_growfs /;; " + "btrfs) sudo btrfs filesystem resize max /;; " + 'esac; ' + "df -h /" + ) + def _qemu_grow_guest_fs(self, name): - """Étend la partition racine + le FS invité via SSH (growpart + - resize2fs/xfs_growfs/btrfs). Détecte le device et le type de FS.""" - ip = self._qemu_vm_ip(name, timeout=120) - if not ip: - print(t("No IP; grow the guest FS manually once booted.")) + """Étend la partition racine + le FS invité. Essaie SSH (IP résolue + avec BATTEMENT, le boot émulé étant lent) ; en cas d'absence d'IP ou + d'échec SSH, propose le repli par CONSOLE SÉRIE (commande à coller).""" + remote = self._GROW_FS_REMOTE + real = self._qemu_domname(name) + # Résolution d'IP robuste (parallèle + battement toutes les 30 s) + # plutôt qu'un simple timeout court qui abandonnait trop tôt. + ip = self._qemu_resolve_ips([real], timeout=300).get(real) + if ip: + opts = ( + "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " + "-o ConnectTimeout=15" + ) + cmd = f"ssh {opts} erplibre@{ip} {shlex.quote(remote)}" + print(f"{t('Will execute:')} {cmd}") + if self.execute.exec_command_live(cmd, source_erplibre=False) == 0: + return + print(f"⚠ {t('SSH grow failed; falling back to serial console.')}") + else: + print(t("No IP; falling back to serial console.")) + self._qemu_grow_via_console(real, remote) + + def _qemu_grow_via_console(self, name, remote): + """Repli console série : affiche la commande prête à coller puis ouvre + la console (login interactif erplibre/erplibre — pas d'automatisation + fiable de la saisie).""" + print(f"\n{t('Serial console fallback. Log in, then paste:')}") + print(f"\n {remote}\n") + print(f"💡 {t('To leave the console, press Ctrl+] (then Enter).')}") + print( + f"👤 {t('Default login (if set at deploy): erplibre / erplibre')}" + ) + if not self._is_yes( + input(t("Open the serial console now? (y/N): ")) + ): return - remote = ( - "set -e; " - "root=$(findmnt -no SOURCE /); " - "dev=$(lsblk -no PKNAME \"$root\" | head -1); " - "part=$(echo \"$root\" | grep -oE '[0-9]+$'); " - "sudo growpart /dev/$dev $part || true; " - "fstype=$(findmnt -no FSTYPE /); " - 'case "$fstype" in ' - "ext*) sudo resize2fs \"$root\";; " - "xfs) sudo xfs_growfs /;; " - "btrfs) sudo btrfs filesystem resize max /;; " - 'esac; ' - "df -h /" - ) - opts = ( - "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null " - "-o ConnectTimeout=15" - ) - cmd = f"ssh {opts} erplibre@{ip} {shlex.quote(remote)}" + cmd = f"sudo virsh console {shlex.quote(name)}" print(f"{t('Will execute:')} {cmd}") self.execute.exec_command_live(cmd, source_erplibre=False) @@ -2804,10 +2839,11 @@ class TODO: if catalog_all: chosen = vlist else: - print(f"\n{t('Versions for')} {d} :") + print(f"\n{t('Versions for')} {d.capitalize()} :") for i, v in enumerate(vlist, 1): _c, _o, ram, disk = versions_map[v] - print(f" [{i}] {v} (RAM≥{ram}Mo, {disk})") + stat = self._qemu_stat_avg("version", v, d) + print(f" [{i}] {v} (RAM≥{ram}Mo, {disk}){stat}") print(f" [all] {t('select all')}") r = input( t("Selection (numbers, or 'all', default: all): ") diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index e1dded0..487b3ed 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1171,9 +1171,13 @@ TRANSLATIONS = { "en": "Invalid selection, using ubuntu", }, "Version for": { - "fr": "Version pour", + "fr": "Version des", "en": "Version for", }, + "Versions for": { + "fr": "Versions des", + "en": "Versions of", + }, "Architecture:": { "fr": "Architecture :", "en": "Architecture:", @@ -1420,6 +1424,22 @@ TRANSLATIONS = { "fr": "Impossible de lire la taille actuelle du disque ; abandon.", "en": "Could not read current disk size; aborting.", }, + "SSH grow failed; falling back to serial console.": { + "fr": "Échec de l'extension via SSH ; repli sur la console série.", + "en": "SSH grow failed; falling back to serial console.", + }, + "No IP; falling back to serial console.": { + "fr": "Pas d'IP ; repli sur la console série.", + "en": "No IP; falling back to serial console.", + }, + "Serial console fallback. Log in, then paste:": { + "fr": "Repli console série. Connectez-vous, puis collez :", + "en": "Serial console fallback. Log in, then paste:", + }, + "Open the serial console now? (y/N): ": { + "fr": "Ouvrir la console série maintenant ? (o/N) : ", + "en": "Open the serial console now? (y/N): ", + }, "The VM must be off. Shut it down and retry? (y/N): ": { "fr": "La VM doit être éteinte. L'éteindre et réessayer ? (o/N) : ", "en": "The VM must be off. Shut it down and retry? (y/N): ",