[FIX] script todo: réduction — détection de partition robuste (FSTYPE vide)

« Partition à réduire introuvable » sur une vraie VM : _qemu_root_part
parsait lsblk en positionnel et EXIGEAIT 4 colonnes. Juste après le connect
nbd, le FSTYPE n'est pas encore en cache -> colonne VIDE -> 3 tokens ->
toutes les partitions étaient ignorées. (Le test initial passait car le FS
avait eu le temps d'être détecté.)

- lsblk -P (paires clé="valeur") : robuste aux colonnes vides ; on repère
  la partition par TYPE="part" et on sonde le FSTYPE via blkid si absent.
- _qemu_nbd_connect attend l'APPARITION des sous-périphériques nbdNpM
  (jusqu'à ~15 s) avant de rendre la main.
- partprobe silencieux (capture) -> plus de spam « Invalid argument during
  seek » pendant la réparation GPT.

Validé sur image jetable au layout cloud (p1 root + p14 bios + p15 ESP),
détection immédiate après connect : 25G -> 15G, GPT « No problems found »,
partitions préservées, fsck propre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mathieu Benoit 2026-07-30 00:41:54 -04:00
parent 545193d293
commit c9ec5b98b9

View file

@ -1838,7 +1838,9 @@ class TODO:
if rc != 0: if rc != 0:
print(t("Partition rewrite failed; reverting.")) print(t("Partition rewrite failed; reverting."))
return self._qemu_shrink_revert(bak, disk, changed=True) return self._qemu_shrink_revert(bak, disk, changed=True)
subprocess.run(["sudo", "partprobe", dev], check=False) subprocess.run(
["sudo", "partprobe", dev], check=False, capture_output=True
)
# Détache puis tronque le conteneur qcow2. # Détache puis tronque le conteneur qcow2.
self._qemu_nbd_disconnect(dev) self._qemu_nbd_disconnect(dev)
dev = None dev = None
@ -1853,7 +1855,9 @@ class TODO:
dev = self._qemu_nbd_connect(disk) dev = self._qemu_nbd_connect(disk)
if dev: if dev:
subprocess.run(["sudo", "sgdisk", "-e", dev], check=False) subprocess.run(["sudo", "sgdisk", "-e", dev], check=False)
subprocess.run(["sudo", "partprobe", dev], check=False) subprocess.run(
["sudo", "partprobe", dev], check=False, capture_output=True
)
p2 = self._qemu_root_part(dev)[0] p2 = self._qemu_root_part(dev)[0]
if p2: if p2:
subprocess.run( subprocess.run(
@ -1880,7 +1884,9 @@ class TODO:
@staticmethod @staticmethod
def _qemu_nbd_connect(disk): def _qemu_nbd_connect(disk):
"""Attache `disk` à un /dev/nbdN libre et renvoie le chemin, ou None.""" """Attache `disk` à un /dev/nbdN libre et renvoie le chemin, ou None.
Attend que les sous-périphériques de partition (nbdNpM) APPARAISSENT
(sinon lsblk/resize2fs ne voient rien juste après le connect)."""
for i in range(16): for i in range(16):
dev = f"/dev/nbd{i}" dev = f"/dev/nbd{i}"
# /sys/block/nbdN/pid absent => device libre. # /sys/block/nbdN/pid absent => device libre.
@ -1890,11 +1896,20 @@ class TODO:
["sudo", "qemu-nbd", "-c", dev, disk], ["sudo", "qemu-nbd", "-c", dev, disk],
capture_output=True, text=True, capture_output=True, text=True,
).returncode ).returncode
if rc == 0: if rc != 0:
continue
base = f"nbd{i}"
for _ in range(15):
subprocess.run(
["sudo", "partprobe", dev], check=False, capture_output=True
)
time.sleep(1) time.sleep(1)
subprocess.run(["sudo", "partprobe", dev], check=False) if any(
time.sleep(1) os.path.exists(f"/sys/class/block/{base}p{n}")
return dev for n in range(1, 32)
):
break
return dev
return None return None
@staticmethod @staticmethod
@ -1905,25 +1920,29 @@ class TODO:
@staticmethod @staticmethod
def _qemu_root_part(dev): def _qemu_root_part(dev):
"""(partition la plus grosse, secteur de début, type FS) du disque nbd. """(partition la plus grosse, secteur de début, type FS) du disque nbd.
(None, 0, '') si introuvable.""" (None, 0, '') si introuvable. Format lsblk -P (paires) : robuste aux
colonnes VIDES juste après le connect, FSTYPE peut être vide, et un
parsing positionnel décalait/ignorait alors toutes les partitions."""
import re
try: try:
res = subprocess.run( res = subprocess.run(
["lsblk", "-bnro", "NAME,SIZE,FSTYPE,TYPE", dev], ["lsblk", "-Pbno", "NAME,SIZE,TYPE,FSTYPE", dev],
capture_output=True, text=True, timeout=30, capture_output=True, text=True, timeout=30,
) )
except (OSError, subprocess.SubprocessError): except (OSError, subprocess.SubprocessError):
return None, 0, "" return None, 0, ""
best, best_sz, best_fs = None, -1, "" best, best_sz, best_fs = None, -1, ""
for line in res.stdout.splitlines(): for line in res.stdout.splitlines():
cols = line.split() d = dict(re.findall(r'(\w+)="([^"]*)"', line))
if len(cols) < 4 or cols[-1] != "part": if d.get("TYPE") != "part":
continue continue
name, size = cols[0], int(cols[1]) try:
fstype = cols[2] if len(cols) >= 4 and cols[2] != "part" else "" size = int(d.get("SIZE") or 0)
if len(cols) == 3: # pas de FSTYPE -> décalage except ValueError:
fstype = "" size = 0
if size > best_sz: if size > best_sz:
best, best_sz, best_fs = name, size, fstype best, best_sz, best_fs = d.get("NAME"), size, d.get("FSTYPE", "")
if not best: if not best:
return None, 0, "" return None, 0, ""
part = f"/dev/{best}" part = f"/dev/{best}"
@ -1934,6 +1953,7 @@ class TODO:
except OSError: except OSError:
start = 0 start = 0
if not best_fs: if not best_fs:
# FSTYPE pas encore en cache : sonder directement avec blkid.
best_fs = subprocess.run( best_fs = subprocess.run(
["sudo", "blkid", "-o", "value", "-s", "TYPE", part], ["sudo", "blkid", "-o", "value", "-s", "TYPE", part],
capture_output=True, text=True, capture_output=True, text=True,