From ef78b9d3155bd4305567e2ea44a8f7de310f886d Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Sun, 19 Jul 2026 07:26:28 +0000 Subject: [PATCH] [IMP] script todo: add "Delete VM(s)" QEMU command New QEMU/KVM menu entry to remove one, several or all VMs. It lists the defined domains, lets you pick by number or "all", asks whether to also delete the disk images (the qcow2 working disk + the seed ISO), shows what will be removed and asks for confirmation. Each VM is powered off (destroy) then undefined (--nvram, with a fallback for older virsh); disks are only deleted when explicitly requested. Co-Authored-By: Claude Opus 4.8 (1M context) --- script/todo/todo.py | 72 ++++++++++++++++++++++++++++++++++++++++ script/todo/todo_i18n.py | 40 ++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/script/todo/todo.py b/script/todo/todo.py index d1ecfa9..88d69b7 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -857,6 +857,7 @@ class TODO: "Deploy ERPLibre infra (one minimal VM per image)" ) }, + {"prompt_description": t("Delete VM(s)")}, ] config_entries = self.config_file.get_config("qemu_from_makefile") if config_entries: @@ -882,6 +883,8 @@ class TODO: self._qemu_list_images() elif status == "7": self._qemu_deploy_infra() + elif status == "8": + self._qemu_delete_vm() else: cmd_no_found = True try: @@ -994,6 +997,75 @@ class TODO: print(f"{t('Will execute:')} {cmd}") self.execute.exec_command_live(cmd, source_erplibre=False) + def _qemu_list_domains(self): + """Noms des VM libvirt définies (via virsh).""" + try: + res = subprocess.run( + ["sudo", "virsh", "list", "--all", "--name"], + capture_output=True, + text=True, + timeout=15, + ) + except (OSError, subprocess.SubprocessError): + return [] + return [n for n in res.stdout.split() if n.strip()] + + def _qemu_delete_vm(self): + """Efface une ou plusieurs VM (arrêt + undefine), disques en option.""" + self._qemu_list_vms() + print() + names = self._qemu_list_domains() + if not names: + print(t("No VM found.")) + return + print(f"\n{t('Select VMs to delete:')}") + for i, n in enumerate(names, 1): + print(f" [{i}] {n}") + print(f" [all] {t('select all')}") + raw = input(t("Selection (numbers, or 'all'): ")).strip() + if not raw: + print(t("Nothing selected.")) + return + if raw.lower() in ("all", "*"): + chosen = list(names) + else: + chosen = self._parse_index_selection(raw.lower(), names) + if not chosen: + print(t("Nothing selected.")) + return + + del_disks = self._is_yes( + input(t("Also delete disk images (qcow2 + seed ISO)? (y/N): ")) + ) + + print(f"\n{t('Will delete:')} {', '.join(chosen)}") + if del_disks: + print(f" + {t('disk images and seed ISOs')}") + else: + print(f" ({t('disks kept')})") + if not self._is_yes(input(t("Confirm deletion? (y/N): "))): + print(t("Cancelled.")) + return + + disk_dir = "/var/lib/libvirt/images" + seed_dir = "/var/lib/libvirt/images/iso" + for name in chosen: + q = shlex.quote(name) + # Éteindre si en cours, puis retirer la définition (+ nvram si + # UEFI ; repli sans l'option pour les vieilles versions de virsh). + cmd = ( + f"sudo virsh destroy {q} 2>/dev/null; " + f"sudo virsh undefine {q} --nvram 2>/dev/null " + f"|| sudo virsh undefine {q}" + ) + if del_disks: + disk = shlex.quote(f"{disk_dir}/{name}.qcow2") + seed = shlex.quote(f"{seed_dir}/{name}-seed.iso") + cmd += f"; sudo rm -f {disk} {seed}" + print(f"\n▶ {name}: {cmd}") + self.execute.exec_command_live(cmd, source_erplibre=False) + print(f"\n✅ {t('Deletion done.')}") + # ------------------------------------------------------------------ # # QEMU : déploiement d'un parc « infra ERPLibre » # ------------------------------------------------------------------ # diff --git a/script/todo/todo_i18n.py b/script/todo/todo_i18n.py index 4ffbee1..c7de6d5 100644 --- a/script/todo/todo_i18n.py +++ b/script/todo/todo_i18n.py @@ -1049,6 +1049,46 @@ TRANSLATIONS = { "fr": "tout sélectionner", "en": "select all", }, + "Delete VM(s)": { + "fr": "Effacer une ou plusieurs VM", + "en": "Delete VM(s)", + }, + "No VM found.": { + "fr": "Aucune VM trouvée.", + "en": "No VM found.", + }, + "Select VMs to delete:": { + "fr": "Sélectionner les VM à effacer :", + "en": "Select VMs to delete:", + }, + "Selection (numbers, or 'all'): ": { + "fr": "Sélection (numéros, ou « all ») : ", + "en": "Selection (numbers, or 'all'): ", + }, + "Also delete disk images (qcow2 + seed ISO)? (y/N): ": { + "fr": "Effacer aussi les disques (qcow2 + seed ISO) ? (o/N) : ", + "en": "Also delete disk images (qcow2 + seed ISO)? (y/N): ", + }, + "Will delete:": { + "fr": "Sera effacé :", + "en": "Will delete:", + }, + "disk images and seed ISOs": { + "fr": "les disques et les seed ISO", + "en": "disk images and seed ISOs", + }, + "disks kept": { + "fr": "disques conservés", + "en": "disks kept", + }, + "Confirm deletion? (y/N): ": { + "fr": "Confirmer l'effacement ? (o/N) : ", + "en": "Confirm deletion? (y/N): ", + }, + "Deletion done.": { + "fr": "Effacement terminé.", + "en": "Deletion done.", + }, "Deploy ERPLibre infra (one minimal VM per image)": { "fr": "Déployer l'infra ERPLibre (une VM minimale par image)", "en": "Deploy ERPLibre infra (one minimal VM per image)",