[ADD] tui qemu: resume an installation already running
Installs run detached (setsid -f): closing the terminal does not stop them, but it lost the only view onto them. With no way to resume, the only way out was deleting the VMs and starting over. Picking "Deploy" now looks at the latest run: if VMs there still lack an exit marker, it offers to reopen its monitoring instead of starting another. Time since the last write is shown, a dead run being otherwise indistinguishable from a live one. Only the latest run is examined: an old one left without a marker would flag a phantom install forever. Dry-run creates nothing, so it does not ask. "Reopen monitoring" moves to the Deployment section, where one looks for it. --- FR --- Les installs partent détachées (setsid -f) : fermer le terminal ne les arrête pas, mais faisait perdre la seule vue dessus. Sans moyen de reprendre, la seule issue était d'effacer les VM et de recommencer. Choisir « Déployer » regarde donc le dernier run : s'il lui reste des VM sans marqueur de sortie, il propose de rouvrir son suivi plutôt que d'en lancer un autre. Le silence depuis la dernière écriture est affiché, un run mort n'étant pas distinguable autrement d'un run vivant. Seul le dernier run est examiné : un run ancien laissé sans marqueur signalerait éternellement une install fantôme. L'aperçu ne crée rien, il ne pose pas la question. « Rouvrir le suivi » rejoint la section Déploiement, où on le cherche. Assisted-by: Claude Opus 5
This commit is contained in:
parent
7650710295
commit
b6de9b5506
3 changed files with 134 additions and 21 deletions
|
|
@ -261,6 +261,46 @@ def read_status(log_path: str) -> tuple[str, int | None]:
|
|||
return "running", None
|
||||
|
||||
|
||||
def run_progress(run: dict) -> dict:
|
||||
"""Avancement d'un run : combien de VM tournent encore, et depuis quand
|
||||
plus rien n'a été écrit. `idle` sert à distinguer une install vivante d'un
|
||||
run laissé pour mort — le marqueur de sortie manque dans les deux cas."""
|
||||
active = final = 0
|
||||
latest = 0.0
|
||||
for vm in run.get("vms", []):
|
||||
log = vm.get("log") or ""
|
||||
state, _code = read_status(log)
|
||||
if state in ("done", "failed"):
|
||||
final += 1
|
||||
else:
|
||||
active += 1
|
||||
try:
|
||||
latest = max(latest, os.path.getmtime(log))
|
||||
except OSError:
|
||||
pass
|
||||
return {
|
||||
"active": active,
|
||||
"final": final,
|
||||
"total": active + final,
|
||||
"idle": (time.time() - latest) if latest else None,
|
||||
}
|
||||
|
||||
|
||||
def active_run():
|
||||
"""Le run le PLUS RÉCENT s'il a encore des VM en cours, sinon None.
|
||||
|
||||
Les installs tournent détachées (`setsid -f`) : fermer le terminal laisse
|
||||
le travail se poursuivre mais fait perdre la seule vue dessus. On ne
|
||||
regarde que le dernier run — un run ancien resté sans marqueur de sortie
|
||||
signalerait éternellement une install fantôme."""
|
||||
runs = list_install_runs()
|
||||
if not runs:
|
||||
return None
|
||||
run = dict(runs[0])
|
||||
run.update(run_progress(run))
|
||||
return run if run["total"] and run["active"] else None
|
||||
|
||||
|
||||
# Listes d'ignore reprises de script/test/run_parallel_test.py (erreurs/
|
||||
# avertissements connus et bénins) : on réutilise la MÊME logique de détection
|
||||
# que la suite de tests ERPLibre pour analyser les logs d'installation.
|
||||
|
|
|
|||
|
|
@ -1243,6 +1243,11 @@ class TODO:
|
|||
)
|
||||
},
|
||||
{"prompt_description": t("Download a cloud image only")},
|
||||
{
|
||||
"prompt_description": t(
|
||||
"Reopen install monitoring (last run / history)"
|
||||
)
|
||||
},
|
||||
{"section": t("Manage")},
|
||||
{"prompt_description": t("List VMs (virsh list --all)")},
|
||||
{"prompt_description": t("Show a VM IP address")},
|
||||
|
|
@ -1255,11 +1260,6 @@ class TODO:
|
|||
"Test a VM (open Odoo in a CLI browser)"
|
||||
)
|
||||
},
|
||||
{
|
||||
"prompt_description": t(
|
||||
"Reopen install monitoring (last run / history)"
|
||||
)
|
||||
},
|
||||
{"prompt_description": t("Statistics (installs, durations, VMs)")},
|
||||
{
|
||||
"prompt_description": t(
|
||||
|
|
@ -1286,21 +1286,21 @@ class TODO:
|
|||
elif status == "3":
|
||||
self._qemu_download_image()
|
||||
elif status == "4":
|
||||
self._qemu_list_vms(ask_advanced=True)
|
||||
elif status == "5":
|
||||
self._qemu_show_ip()
|
||||
elif status == "6":
|
||||
self._qemu_console()
|
||||
elif status == "7":
|
||||
self._qemu_resize_disk()
|
||||
elif status == "8":
|
||||
self._qemu_delete_vm()
|
||||
elif status == "9":
|
||||
self._qemu_cleanup()
|
||||
elif status == "10":
|
||||
self._qemu_test_vm()
|
||||
elif status == "11":
|
||||
self._qemu_reopen_monitor()
|
||||
elif status == "5":
|
||||
self._qemu_list_vms(ask_advanced=True)
|
||||
elif status == "6":
|
||||
self._qemu_show_ip()
|
||||
elif status == "7":
|
||||
self._qemu_console()
|
||||
elif status == "8":
|
||||
self._qemu_resize_disk()
|
||||
elif status == "9":
|
||||
self._qemu_delete_vm()
|
||||
elif status == "10":
|
||||
self._qemu_cleanup()
|
||||
elif status == "11":
|
||||
self._qemu_test_vm()
|
||||
elif status == "12":
|
||||
self._qemu_stats()
|
||||
elif status == "13":
|
||||
|
|
@ -2367,16 +2367,62 @@ class TODO:
|
|||
except ValueError:
|
||||
print(t("Invalid selection."))
|
||||
return
|
||||
self._qemu_open_monitor(run["manifest"])
|
||||
|
||||
def _qemu_open_monitor(self, manifest):
|
||||
"""Ouvre le dashboard sur un manifeste, en installant Textual au
|
||||
besoin. Deux entrées y mènent — l'historique et la reprise proposée
|
||||
avant un déploiement — d'où une seule définition."""
|
||||
from script.todo import qemu_install_monitor as mon
|
||||
|
||||
try:
|
||||
mon.run_monitor(run["manifest"])
|
||||
mon.run_monitor(manifest)
|
||||
except ImportError:
|
||||
from script.todo import textual_setup
|
||||
|
||||
if textual_setup.ensure():
|
||||
mon.run_monitor(run["manifest"])
|
||||
mon.run_monitor(manifest)
|
||||
except Exception as exc:
|
||||
print(f"{t('Command failed: ')}{exc}")
|
||||
|
||||
def _qemu_active_install(self):
|
||||
"""Propose de reprendre le suivi quand une installation tourne encore.
|
||||
|
||||
Les installs partent détachées (`setsid -f`) : fermer le terminal ne
|
||||
les arrête pas, mais faisait perdre la seule vue dessus, et la seule
|
||||
issue connue était de tout effacer pour recommencer.
|
||||
|
||||
True si l'on ne doit PAS enchaîner sur un déploiement."""
|
||||
try:
|
||||
from script.todo import qemu_install_monitor as mon
|
||||
|
||||
run = mon.active_run()
|
||||
except Exception:
|
||||
return False
|
||||
if not run:
|
||||
return False
|
||||
names = ", ".join(v.get("name", "?") for v in run["vms"])
|
||||
print(
|
||||
f"\n⏳ {t('An install is still running:')} {run['label']} — "
|
||||
f"{run['active']}/{run['total']} {t('VM(s) in progress')}"
|
||||
)
|
||||
print(f" {names}")
|
||||
if run.get("idle") is not None:
|
||||
# Un silence prolongé trahit un run mort dont le marqueur de sortie
|
||||
# ne viendra jamais : l'utilisateur tranche mieux que nous.
|
||||
print(
|
||||
f" {t('Last activity:')} {mon._fmt_secs(int(run['idle']))}"
|
||||
)
|
||||
print(f"\n [1] {t('Reopen that monitoring')} *")
|
||||
print(f" [2] {t('Deploy anyway (new run)')}")
|
||||
print(f" [0] {t('Back')}")
|
||||
sel = input(t("Choice (number, blank = reopen): ")).strip()
|
||||
if sel == "2":
|
||||
return False
|
||||
if sel != "0":
|
||||
self._qemu_open_monitor(run["manifest"])
|
||||
return True
|
||||
|
||||
def _qemu_choose_cli_browser(self):
|
||||
"""Offre la LISTE des navigateurs CLI installés, plus une option pour
|
||||
en INSTALLER un autre, et renvoie celui choisi, sinon None."""
|
||||
|
|
@ -5170,6 +5216,9 @@ class TODO:
|
|||
last = self._qemu_last_run_line()
|
||||
if last:
|
||||
print(last)
|
||||
# Un aperçu ne crée rien : il n'a pas à interroger sur un run en cours.
|
||||
if not dry_run and self._qemu_active_install():
|
||||
return
|
||||
|
||||
self._qemu_check_libvirt_group()
|
||||
self._qemu_check_kvm()
|
||||
|
|
|
|||
|
|
@ -1973,6 +1973,30 @@ TRANSLATIONS = {
|
|||
"fr": "RAM (Mo)",
|
||||
"en": "RAM (MB)",
|
||||
},
|
||||
"An install is still running:": {
|
||||
"fr": "Une installation est encore en cours :",
|
||||
"en": "An install is still running:",
|
||||
},
|
||||
"VM(s) in progress": {
|
||||
"fr": "VM en cours",
|
||||
"en": "VM(s) in progress",
|
||||
},
|
||||
"Last activity:": {
|
||||
"fr": "Dernière activité il y a :",
|
||||
"en": "Last activity:",
|
||||
},
|
||||
"Reopen that monitoring": {
|
||||
"fr": "Rouvrir ce suivi",
|
||||
"en": "Reopen that monitoring",
|
||||
},
|
||||
"Deploy anyway (new run)": {
|
||||
"fr": "Déployer quand même (nouveau run)",
|
||||
"en": "Deploy anyway (new run)",
|
||||
},
|
||||
"Choice (number, blank = reopen): ": {
|
||||
"fr": "Choix (numéro, vide = rouvrir) : ",
|
||||
"en": "Choice (number, blank = reopen): ",
|
||||
},
|
||||
"free value…": {
|
||||
"fr": "valeur libre…",
|
||||
"en": "free value…",
|
||||
|
|
|
|||
Loading…
Reference in a new issue