[IMP] script todo: add "Clean up QEMU" (orphan files) command
New Manage entry that finds and lists QEMU leftovers with no matching libvirt domain, then asks before deleting: - working disks /var/lib/libvirt/images/<name>.qcow2 - cloud-init seeds .../iso/<name>-seed.iso - interrupted downloads (*.part) - orphan UEFI nvram files Sizes are shown human-readable with a total. Cached base cloud images (reusable) are listed and offered separately, since removing them forces a re-download; this also cleans the stale codename-named duplicates left by the switch to /releases/ version-named images. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d83d244805
commit
c0ceb6bea7
2 changed files with 172 additions and 0 deletions
|
|
@ -884,6 +884,7 @@ class TODO:
|
||||||
{"prompt_description": t("Show a VM IP address")},
|
{"prompt_description": t("Show a VM IP address")},
|
||||||
{"prompt_description": t("Open the console on a VM")},
|
{"prompt_description": t("Open the console on a VM")},
|
||||||
{"prompt_description": t("Delete VM(s)")},
|
{"prompt_description": t("Delete VM(s)")},
|
||||||
|
{"prompt_description": t("Clean up QEMU (orphan files)")},
|
||||||
{"section": t("Catalog")},
|
{"section": t("Catalog")},
|
||||||
{"prompt_description": t("List available images and specs")},
|
{"prompt_description": t("List available images and specs")},
|
||||||
]
|
]
|
||||||
|
|
@ -914,6 +915,8 @@ class TODO:
|
||||||
elif status == "8":
|
elif status == "8":
|
||||||
self._qemu_delete_vm()
|
self._qemu_delete_vm()
|
||||||
elif status == "9":
|
elif status == "9":
|
||||||
|
self._qemu_cleanup()
|
||||||
|
elif status == "10":
|
||||||
self._qemu_list_images()
|
self._qemu_list_images()
|
||||||
else:
|
else:
|
||||||
cmd_no_found = True
|
cmd_no_found = True
|
||||||
|
|
@ -1194,6 +1197,119 @@ class TODO:
|
||||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||||
print(f"\n✅ {t('Deletion done.')}")
|
print(f"\n✅ {t('Deletion done.')}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _human_size(n):
|
||||||
|
"""Octets -> taille lisible (Ko/Mo/Go…)."""
|
||||||
|
size = float(n)
|
||||||
|
for unit in ("o", "Ko", "Mo", "Go", "To"):
|
||||||
|
if size < 1024:
|
||||||
|
return f"{size:.0f} {unit}"
|
||||||
|
size /= 1024
|
||||||
|
return f"{size:.0f} Po"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _qemu_find_files(directory, pattern):
|
||||||
|
"""(taille, chemin) des fichiers du répertoire (via sudo find)."""
|
||||||
|
try:
|
||||||
|
res = subprocess.run(
|
||||||
|
[
|
||||||
|
"sudo",
|
||||||
|
"find",
|
||||||
|
directory,
|
||||||
|
"-maxdepth",
|
||||||
|
"1",
|
||||||
|
"-type",
|
||||||
|
"f",
|
||||||
|
"-name",
|
||||||
|
pattern,
|
||||||
|
"-printf",
|
||||||
|
"%s\t%p\n",
|
||||||
|
],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.SubprocessError):
|
||||||
|
return []
|
||||||
|
out = []
|
||||||
|
for line in res.stdout.splitlines():
|
||||||
|
if "\t" in line:
|
||||||
|
size, path = line.split("\t", 1)
|
||||||
|
out.append((int(size), path))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _qemu_cleanup(self):
|
||||||
|
"""Repère les fichiers QEMU orphelins et propose de les effacer."""
|
||||||
|
print(f"🧹 {t('Scanning for orphan QEMU files...')}")
|
||||||
|
disk_dir = "/var/lib/libvirt/images"
|
||||||
|
seed_dir = "/var/lib/libvirt/images/iso"
|
||||||
|
nvram_dir = "/var/lib/libvirt/qemu/nvram"
|
||||||
|
domains = set(self._qemu_list_domains())
|
||||||
|
|
||||||
|
orphans = [] # (taille, chemin, motif)
|
||||||
|
# Disques de travail sans domaine correspondant.
|
||||||
|
for size, path in self._qemu_find_files(disk_dir, "*.qcow2"):
|
||||||
|
stem = os.path.basename(path)[: -len(".qcow2")]
|
||||||
|
if stem not in domains:
|
||||||
|
orphans.append((size, path, t("orphan disk")))
|
||||||
|
# Seeds cloud-init sans domaine.
|
||||||
|
for size, path in self._qemu_find_files(seed_dir, "*-seed.iso"):
|
||||||
|
stem = os.path.basename(path)[: -len("-seed.iso")]
|
||||||
|
if stem not in domains:
|
||||||
|
orphans.append((size, path, t("orphan seed")))
|
||||||
|
# Téléchargements interrompus.
|
||||||
|
for size, path in self._qemu_find_files(seed_dir, "*.part"):
|
||||||
|
orphans.append((size, path, t("partial download")))
|
||||||
|
# nvram UEFI sans domaine.
|
||||||
|
for size, path in self._qemu_find_files(nvram_dir, "*"):
|
||||||
|
stem = re.sub(r"(_VARS)?\.fd$", "", os.path.basename(path))
|
||||||
|
if stem not in domains:
|
||||||
|
orphans.append((size, path, t("orphan UEFI nvram")))
|
||||||
|
|
||||||
|
if not orphans:
|
||||||
|
print(f"✅ {t('No orphan files found.')}")
|
||||||
|
else:
|
||||||
|
total = sum(o[0] for o in orphans)
|
||||||
|
print(f"\n{t('Orphan files:')}")
|
||||||
|
for size, path, reason in sorted(orphans, key=lambda o: -o[0]):
|
||||||
|
print(f" {self._human_size(size):>9} {path} [{reason}]")
|
||||||
|
print(
|
||||||
|
f"\n {t('Total:')} {self._human_size(total)} "
|
||||||
|
f"({len(orphans)} {t('files')})"
|
||||||
|
)
|
||||||
|
if self._is_yes(input(t("Delete these orphan files? (y/N): "))):
|
||||||
|
paths = " ".join(shlex.quote(o[1]) for o in orphans)
|
||||||
|
cmd = f"sudo rm -f {paths}"
|
||||||
|
print(f"{t('Will execute:')} {cmd}")
|
||||||
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||||
|
print(f"✅ {t('Cleanup done.')}")
|
||||||
|
else:
|
||||||
|
print(t("Cancelled."))
|
||||||
|
|
||||||
|
# Cache d'images de base (réutilisables) : proposé séparément car les
|
||||||
|
# effacer force un re-téléchargement au prochain déploiement.
|
||||||
|
cached = [
|
||||||
|
(s, p)
|
||||||
|
for s, p in self._qemu_find_files(seed_dir, "*")
|
||||||
|
if not p.endswith("-seed.iso") and not p.endswith(".part")
|
||||||
|
]
|
||||||
|
if cached:
|
||||||
|
total = sum(s for s, _ in cached)
|
||||||
|
print(
|
||||||
|
f"\n{t('Cached base images (reusable):')} "
|
||||||
|
f"{self._human_size(total)}"
|
||||||
|
)
|
||||||
|
for size, path in sorted(cached, key=lambda o: -o[0]):
|
||||||
|
print(f" {self._human_size(size):>9} {path}")
|
||||||
|
if self._is_yes(
|
||||||
|
input(t("Also delete the cached base images? (y/N): "))
|
||||||
|
):
|
||||||
|
paths = " ".join(shlex.quote(p) for _, p in cached)
|
||||||
|
cmd = f"sudo rm -f {paths}"
|
||||||
|
print(f"{t('Will execute:')} {cmd}")
|
||||||
|
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||||
|
print(f"✅ {t('Cleanup done.')}")
|
||||||
|
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
# QEMU : déploiement d'un parc « infra ERPLibre »
|
# QEMU : déploiement d'un parc « infra ERPLibre »
|
||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
|
||||||
|
|
@ -1214,6 +1214,62 @@ TRANSLATIONS = {
|
||||||
"fr": "Effacement terminé.",
|
"fr": "Effacement terminé.",
|
||||||
"en": "Deletion done.",
|
"en": "Deletion done.",
|
||||||
},
|
},
|
||||||
|
"Clean up QEMU (orphan files)": {
|
||||||
|
"fr": "Nettoyer QEMU (fichiers orphelins)",
|
||||||
|
"en": "Clean up QEMU (orphan files)",
|
||||||
|
},
|
||||||
|
"Scanning for orphan QEMU files...": {
|
||||||
|
"fr": "Recherche des fichiers QEMU orphelins...",
|
||||||
|
"en": "Scanning for orphan QEMU files...",
|
||||||
|
},
|
||||||
|
"orphan disk": {
|
||||||
|
"fr": "disque orphelin",
|
||||||
|
"en": "orphan disk",
|
||||||
|
},
|
||||||
|
"orphan seed": {
|
||||||
|
"fr": "seed orphelin",
|
||||||
|
"en": "orphan seed",
|
||||||
|
},
|
||||||
|
"partial download": {
|
||||||
|
"fr": "téléchargement interrompu",
|
||||||
|
"en": "partial download",
|
||||||
|
},
|
||||||
|
"orphan UEFI nvram": {
|
||||||
|
"fr": "nvram UEFI orpheline",
|
||||||
|
"en": "orphan UEFI nvram",
|
||||||
|
},
|
||||||
|
"No orphan files found.": {
|
||||||
|
"fr": "Aucun fichier orphelin trouvé.",
|
||||||
|
"en": "No orphan files found.",
|
||||||
|
},
|
||||||
|
"Orphan files:": {
|
||||||
|
"fr": "Fichiers orphelins :",
|
||||||
|
"en": "Orphan files:",
|
||||||
|
},
|
||||||
|
"Total:": {
|
||||||
|
"fr": "Total :",
|
||||||
|
"en": "Total:",
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"fr": "fichiers",
|
||||||
|
"en": "files",
|
||||||
|
},
|
||||||
|
"Delete these orphan files? (y/N): ": {
|
||||||
|
"fr": "Effacer ces fichiers orphelins ? (o/N) : ",
|
||||||
|
"en": "Delete these orphan files? (y/N): ",
|
||||||
|
},
|
||||||
|
"Cleanup done.": {
|
||||||
|
"fr": "Nettoyage terminé.",
|
||||||
|
"en": "Cleanup done.",
|
||||||
|
},
|
||||||
|
"Cached base images (reusable):": {
|
||||||
|
"fr": "Images de base en cache (réutilisables) :",
|
||||||
|
"en": "Cached base images (reusable):",
|
||||||
|
},
|
||||||
|
"Also delete the cached base images? (y/N): ": {
|
||||||
|
"fr": "Effacer aussi les images de base en cache ? (o/N) : ",
|
||||||
|
"en": "Also delete the cached base images? (y/N): ",
|
||||||
|
},
|
||||||
"Deploy ERPLibre infra (one minimal VM per image)": {
|
"Deploy ERPLibre infra (one minimal VM per image)": {
|
||||||
"fr": "Déployer l'infra ERPLibre (une VM minimale par image)",
|
"fr": "Déployer l'infra ERPLibre (une VM minimale par image)",
|
||||||
"en": "Deploy ERPLibre infra (one minimal VM per image)",
|
"en": "Deploy ERPLibre infra (one minimal VM per image)",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue