[IMP] script todo: commande QEMU « Redimensionner le disque d'une VM »
Nouvelle entrée dans le menu QEMU/KVM (section Gérer). Elle : - affiche le disque principal (qcow2 via domblklist) + « qemu-img info » (taille virtuelle + réelle) et l'état de la VM ; - demande +NG (agrandir), -NG (réduire) ou NG (taille cible) ; - applique : agrandissement À CHAUD si la VM tourne (virsh blockresize), sinon qemu-img resize ; réduction via qemu-img resize --shrink (VM éteinte obligatoire + avertissement fort : le FS invité n'est PAS réduit, risque de perte de données) ; - propose ensuite d'étendre le FS invité via SSH (growpart + resize2fs / xfs_growfs / btrfs, device et type de FS détectés). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6f70da98d5
commit
1fd98e6ab6
2 changed files with 280 additions and 2 deletions
|
|
@ -984,6 +984,7 @@ class TODO:
|
|||
{"prompt_description": t("List VMs (virsh list --all)")},
|
||||
{"prompt_description": t("Show a VM IP address")},
|
||||
{"prompt_description": t("Open the console on a VM")},
|
||||
{"prompt_description": t("Resize a VM disk")},
|
||||
{"prompt_description": t("Delete VM(s)")},
|
||||
{"prompt_description": t("Clean up QEMU (orphan files)")},
|
||||
{"section": t("Catalog")},
|
||||
|
|
@ -1012,10 +1013,12 @@ class TODO:
|
|||
elif status == "6":
|
||||
self._qemu_console()
|
||||
elif status == "7":
|
||||
self._qemu_delete_vm()
|
||||
self._qemu_resize_disk()
|
||||
elif status == "8":
|
||||
self._qemu_cleanup()
|
||||
self._qemu_delete_vm()
|
||||
elif status == "9":
|
||||
self._qemu_cleanup()
|
||||
elif status == "10":
|
||||
self._qemu_list_images()
|
||||
else:
|
||||
cmd_no_found = True
|
||||
|
|
@ -1095,6 +1098,194 @@ class TODO:
|
|||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Redimensionnement du disque d'une VM
|
||||
# ------------------------------------------------------------------ #
|
||||
@staticmethod
|
||||
def _qemu_domstate(name):
|
||||
"""État libvirt de la VM (« running », « shut off », …) ou ''."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "virsh", "domstate", name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return ""
|
||||
return res.stdout.strip() if res.returncode == 0 else ""
|
||||
|
||||
@staticmethod
|
||||
def _qemu_main_disk(name):
|
||||
"""Chemin du disque PRINCIPAL (qcow2) de la VM via domblklist. On
|
||||
ignore le seed cloud-init (…-seed.iso, en lecture seule)."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "virsh", "domblklist", name, "--details"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
disks = []
|
||||
for line in res.stdout.splitlines():
|
||||
parts = line.split()
|
||||
# Colonnes : Type Device Target Source
|
||||
if len(parts) >= 4 and parts[1] == "disk" and parts[3] != "-":
|
||||
disks.append(parts[3])
|
||||
# Le disque de travail est le .qcow2 (le seed est un .iso).
|
||||
for d in disks:
|
||||
if d.endswith(".qcow2"):
|
||||
return d
|
||||
return disks[0] if disks else None
|
||||
|
||||
@staticmethod
|
||||
def _qemu_disk_virtual_bytes(disk):
|
||||
"""Taille VIRTUELLE (octets) du disque via « qemu-img info --json »."""
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["sudo", "qemu-img", "info", "--output=json", disk],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=20,
|
||||
)
|
||||
data = json.loads(res.stdout)
|
||||
return int(data.get("virtual-size", 0))
|
||||
except (OSError, subprocess.SubprocessError, ValueError):
|
||||
return 0
|
||||
|
||||
def _qemu_resize_disk(self):
|
||||
"""Redimensionne le disque d'une VM : affiche l'espace actuel, demande
|
||||
+NG / -NG / taille cible, applique (à chaud si possible pour agrandir),
|
||||
puis propose d'étendre le système de fichiers invité."""
|
||||
self._qemu_list_vms()
|
||||
print()
|
||||
name = input(t("VM name to resize: ")).strip()
|
||||
if not name:
|
||||
print(t("VM name is required!"))
|
||||
return
|
||||
if not self._qemu_domain_exists(name):
|
||||
print(f"{name}: {t('VM not found.')}")
|
||||
return
|
||||
disk = self._qemu_main_disk(name)
|
||||
if not disk:
|
||||
print(t("Main disk not found for this VM."))
|
||||
return
|
||||
|
||||
# 1) Espace actuel (virtuel + réel) + df invité si joignable.
|
||||
print(f"\n{t('Current disk:')} {disk}")
|
||||
self.execute.exec_command_live(
|
||||
f"sudo qemu-img info {shlex.quote(disk)}", source_erplibre=False
|
||||
)
|
||||
cur_bytes = self._qemu_disk_virtual_bytes(disk)
|
||||
cur_gb = cur_bytes / (1 << 30)
|
||||
print(f"{t('Current virtual size:')} {cur_gb:.1f} G")
|
||||
state = self._qemu_domstate(name)
|
||||
print(f"{t('VM state:')} {state or '?'}")
|
||||
|
||||
# 2) Nouvelle taille : +NG (agrandir), -NG (réduire) ou NG (cible).
|
||||
guide = t(
|
||||
"Enter +NG to grow, -NG to shrink, or NG for a target size "
|
||||
"(e.g. +20G, -10G, 60G)."
|
||||
)
|
||||
print(f"\n{guide}")
|
||||
raw = input(t("Resize: ")).strip().upper().replace("G", "")
|
||||
try:
|
||||
if raw.startswith("+"):
|
||||
new_gb = cur_gb + float(raw[1:])
|
||||
elif raw.startswith("-"):
|
||||
new_gb = cur_gb - float(raw[1:])
|
||||
else:
|
||||
new_gb = float(raw)
|
||||
except ValueError:
|
||||
print(t("Invalid size."))
|
||||
return
|
||||
if new_gb <= 0:
|
||||
print(t("Invalid size."))
|
||||
return
|
||||
new_gb = round(new_gb, 1)
|
||||
if abs(new_gb - cur_gb) < 0.05:
|
||||
print(t("No change."))
|
||||
return
|
||||
shrink = new_gb < cur_gb
|
||||
print(
|
||||
f"\n{t('New virtual size:')} {cur_gb:.1f} G -> {new_gb:.1f} G"
|
||||
)
|
||||
|
||||
# 3) Application selon agrandir/réduire et l'état de la VM.
|
||||
if shrink:
|
||||
# DANGER : qcow2 --shrink ne réduit PAS le FS invité -> perte de
|
||||
# données si le FS dépasse la cible. VM éteinte obligatoire.
|
||||
danger = t(
|
||||
"SHRINKING is DANGEROUS: the guest filesystem is NOT shrunk. "
|
||||
"Data beyond the new size is LOST. Shrink the guest FS FIRST, "
|
||||
"and only then shrink here."
|
||||
)
|
||||
print(f"⚠ {danger}")
|
||||
if state != "shut off":
|
||||
print(t("Shut the VM off before shrinking (virsh shutdown)."))
|
||||
return
|
||||
if not self._is_yes(
|
||||
input(t("Type y to confirm you understand the risk (y/N): "))
|
||||
):
|
||||
print(t("Cancelled."))
|
||||
return
|
||||
cmd = (
|
||||
f"sudo qemu-img resize --shrink {shlex.quote(disk)} "
|
||||
f"{new_gb:g}G"
|
||||
)
|
||||
elif state == "running":
|
||||
# Agrandissement À CHAUD : le disque virtuel grossit, le FS invité
|
||||
# devra être étendu ensuite.
|
||||
cmd = (
|
||||
f"sudo virsh blockresize {shlex.quote(name)} "
|
||||
f"{shlex.quote(disk)} {new_gb:g}G"
|
||||
)
|
||||
else:
|
||||
cmd = f"sudo qemu-img resize {shlex.quote(disk)} {new_gb:g}G"
|
||||
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
if self.execute.exec_command_live(cmd, source_erplibre=False) != 0:
|
||||
print(f"❌ {t('Resize failed (see error above).')}")
|
||||
return
|
||||
print(f"✅ {t('Virtual disk resized.')}")
|
||||
|
||||
# 4) Étendre le FS invité (agrandissement seulement, VM joignable).
|
||||
if not shrink and self._is_yes(
|
||||
input(t("Grow the guest filesystem now (over SSH)? (y/N): "))
|
||||
):
|
||||
self._qemu_grow_guest_fs(name)
|
||||
|
||||
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."))
|
||||
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)}"
|
||||
print(f"{t('Will execute:')} {cmd}")
|
||||
self.execute.exec_command_live(cmd, source_erplibre=False)
|
||||
|
||||
def _write_ssh_config_entry(self, host, user, ip):
|
||||
"""Écrit/remplace un bloc « Host <host> » dans ~/.ssh/config."""
|
||||
cfg = os.path.expanduser("~/.ssh/config")
|
||||
|
|
|
|||
|
|
@ -1282,6 +1282,93 @@ TRANSLATIONS = {
|
|||
"fr": "suivi d'installation",
|
||||
"en": "install monitoring",
|
||||
},
|
||||
"Resize a VM disk": {
|
||||
"fr": "Redimensionner le disque d'une VM",
|
||||
"en": "Resize a VM disk",
|
||||
},
|
||||
"VM name to resize: ": {
|
||||
"fr": "Nom de la VM à redimensionner : ",
|
||||
"en": "VM name to resize: ",
|
||||
},
|
||||
"VM not found.": {
|
||||
"fr": "VM introuvable.",
|
||||
"en": "VM not found.",
|
||||
},
|
||||
"Main disk not found for this VM.": {
|
||||
"fr": "Disque principal introuvable pour cette VM.",
|
||||
"en": "Main disk not found for this VM.",
|
||||
},
|
||||
"Current disk:": {
|
||||
"fr": "Disque actuel :",
|
||||
"en": "Current disk:",
|
||||
},
|
||||
"Current virtual size:": {
|
||||
"fr": "Taille virtuelle actuelle :",
|
||||
"en": "Current virtual size:",
|
||||
},
|
||||
"VM state:": {
|
||||
"fr": "État de la VM :",
|
||||
"en": "VM state:",
|
||||
},
|
||||
"Enter +NG to grow, -NG to shrink, or NG for a target size "
|
||||
"(e.g. +20G, -10G, 60G).": {
|
||||
"fr": "Entrez +NG pour agrandir, -NG pour réduire, ou NG pour une "
|
||||
"taille cible (ex. +20G, -10G, 60G).",
|
||||
"en": "Enter +NG to grow, -NG to shrink, or NG for a target size "
|
||||
"(e.g. +20G, -10G, 60G).",
|
||||
},
|
||||
"Resize: ": {
|
||||
"fr": "Redimensionner : ",
|
||||
"en": "Resize: ",
|
||||
},
|
||||
"Invalid size.": {
|
||||
"fr": "Taille invalide.",
|
||||
"en": "Invalid size.",
|
||||
},
|
||||
"No change.": {
|
||||
"fr": "Aucun changement.",
|
||||
"en": "No change.",
|
||||
},
|
||||
"New virtual size:": {
|
||||
"fr": "Nouvelle taille virtuelle :",
|
||||
"en": "New virtual size:",
|
||||
},
|
||||
"SHRINKING is DANGEROUS: the guest filesystem is NOT shrunk. "
|
||||
"Data beyond the new size is LOST. Shrink the guest FS FIRST, "
|
||||
"and only then shrink here.": {
|
||||
"fr": "RÉDUIRE est DANGEREUX : le système de fichiers invité n'est "
|
||||
"PAS réduit. Les données au-delà de la nouvelle taille sont PERDUES. "
|
||||
"Réduisez d'ABORD le FS invité, puis seulement ici.",
|
||||
"en": "SHRINKING is DANGEROUS: the guest filesystem is NOT shrunk. "
|
||||
"Data beyond the new size is LOST. Shrink the guest FS FIRST, "
|
||||
"and only then shrink here.",
|
||||
},
|
||||
"Shut the VM off before shrinking (virsh shutdown).": {
|
||||
"fr": "Éteignez la VM avant de réduire (virsh shutdown).",
|
||||
"en": "Shut the VM off before shrinking (virsh shutdown).",
|
||||
},
|
||||
"Type y to confirm you understand the risk (y/N): ": {
|
||||
"fr": "Tapez o pour confirmer que vous comprenez le risque "
|
||||
"(o/N, défaut : non) : ",
|
||||
"en": "Type y to confirm you understand the risk (y/N): ",
|
||||
},
|
||||
"Resize failed (see error above).": {
|
||||
"fr": "Échec du redimensionnement (voir l'erreur ci-dessus).",
|
||||
"en": "Resize failed (see error above).",
|
||||
},
|
||||
"Virtual disk resized.": {
|
||||
"fr": "Disque virtuel redimensionné.",
|
||||
"en": "Virtual disk resized.",
|
||||
},
|
||||
"Grow the guest filesystem now (over SSH)? (y/N): ": {
|
||||
"fr": "Étendre le système de fichiers invité maintenant (via SSH) ? "
|
||||
"(o/N, défaut : non) : ",
|
||||
"en": "Grow the guest filesystem now (over SSH)? (y/N): ",
|
||||
},
|
||||
"No IP; grow the guest FS manually once booted.": {
|
||||
"fr": "Pas d'IP ; étendez le FS invité manuellement après le boot.",
|
||||
"en": "No IP; grow the guest FS manually once booted.",
|
||||
},
|
||||
"completed": {
|
||||
"fr": "terminées",
|
||||
"en": "completed",
|
||||
|
|
|
|||
Loading…
Reference in a new issue