From c9ec5b98b9bfe450632c08ea9537bc97653978b3 Mon Sep 17 00:00:00 2001 From: Mathieu Benoit Date: Thu, 30 Jul 2026 00:41:54 -0400 Subject: [PATCH] =?UTF-8?q?[FIX]=20script=20todo:=20r=C3=A9duction=20?= =?UTF-8?q?=E2=80=94=20d=C3=A9tection=20de=20partition=20robuste=20(FSTYPE?= =?UTF-8?q?=20vide)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit « 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) --- script/todo/todo.py | 52 +++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/script/todo/todo.py b/script/todo/todo.py index 0c5879a..e95cdfc 100755 --- a/script/todo/todo.py +++ b/script/todo/todo.py @@ -1838,7 +1838,9 @@ class TODO: if rc != 0: print(t("Partition rewrite failed; reverting.")) 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. self._qemu_nbd_disconnect(dev) dev = None @@ -1853,7 +1855,9 @@ class TODO: dev = self._qemu_nbd_connect(disk) if dev: 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] if p2: subprocess.run( @@ -1880,7 +1884,9 @@ class TODO: @staticmethod 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): dev = f"/dev/nbd{i}" # /sys/block/nbdN/pid absent => device libre. @@ -1890,11 +1896,20 @@ class TODO: ["sudo", "qemu-nbd", "-c", dev, disk], capture_output=True, text=True, ).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) - subprocess.run(["sudo", "partprobe", dev], check=False) - time.sleep(1) - return dev + if any( + os.path.exists(f"/sys/class/block/{base}p{n}") + for n in range(1, 32) + ): + break + return dev return None @staticmethod @@ -1905,25 +1920,29 @@ class TODO: @staticmethod def _qemu_root_part(dev): """(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: res = subprocess.run( - ["lsblk", "-bnro", "NAME,SIZE,FSTYPE,TYPE", dev], + ["lsblk", "-Pbno", "NAME,SIZE,TYPE,FSTYPE", dev], capture_output=True, text=True, timeout=30, ) except (OSError, subprocess.SubprocessError): return None, 0, "" best, best_sz, best_fs = None, -1, "" for line in res.stdout.splitlines(): - cols = line.split() - if len(cols) < 4 or cols[-1] != "part": + d = dict(re.findall(r'(\w+)="([^"]*)"', line)) + if d.get("TYPE") != "part": continue - name, size = cols[0], int(cols[1]) - fstype = cols[2] if len(cols) >= 4 and cols[2] != "part" else "" - if len(cols) == 3: # pas de FSTYPE -> décalage - fstype = "" + try: + size = int(d.get("SIZE") or 0) + except ValueError: + size = 0 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: return None, 0, "" part = f"/dev/{best}" @@ -1934,6 +1953,7 @@ class TODO: except OSError: start = 0 if not best_fs: + # FSTYPE pas encore en cache : sonder directement avec blkid. best_fs = subprocess.run( ["sudo", "blkid", "-o", "value", "-s", "TYPE", part], capture_output=True, text=True,