"""Release a target disk before handing it to archinstall. The live ISO ships lvm2, mdadm, dmraid and cryptsetup (releng defaults), and udev happily auto-activates whatever it finds on attached disks at boot. A drive that used to live in another machine — the first hardware bench had an old Proxmox VE install on its SSD — therefore comes up with device-mapper volumes (or swap, or md arrays) stacked on top of its partitions. archinstall then writes the new partition table fine but `BLKRRPART` fails with Partition(s) 2, 3 on /dev/sda have been written, but we have been unable to inform the kernel of the change, probably because it/they are in use. and the install dies at 12 %. Nothing on a fresh VM ever triggers this, which is why it survived every smoke run. `release_disk()` tears that stack down leaves-first (swapoff, umount, dmsetup remove / mdadm --stop), wipes signatures and asks the kernel to re-read the partition table. Every step is logged so the install log shows what was done. All commands are best-effort: the point is to leave archinstall a disk the kernel can re-partition, not to be a full disk-management tool. """ import re import subprocess _PAIR_RE = re.compile(r'(\w+)="([^"]*)"') def _run(cmd, run): try: result = run(cmd, capture_output=True, text=True) except FileNotFoundError: return 127, f"{cmd[0]}: command not found" out = ((result.stdout or "") + (result.stderr or "")).strip() return result.returncode, out def _parse_pairs(output): """Parse `lsblk -P` key="value" lines into dicts.""" rows = [] for line in output.splitlines(): row = {} for match in _PAIR_RE.finditer(line): row[match.group(1)] = match.group(2) if row.get("NAME"): rows.append(row) return rows def _children(disk, run): """Everything stacked on `disk`, in lsblk tree order, disk itself excluded.""" rc, out = _run(["lsblk", "-lnP", "-o", "NAME,TYPE,MOUNTPOINTS", disk], run) if rc != 0: return [] rows = _parse_pairs(out) seen = set() children = [] for row in rows: if row.get("TYPE") == "disk" or row["NAME"] in seen: continue seen.add(row["NAME"]) children.append(row) return children def release_disk(disk, run=subprocess.run): """Free `disk` (e.g. "/dev/sda") of anything that would block a partition-table reload. Returns the log lines describing what happened.""" if not disk.startswith("/dev/"): disk = f"/dev/{disk}" lines = [f"Releasing {disk} before install…"] children = _children(disk, run) if not children: lines.append(" nothing stacked on the disk — good.") else: lines.append(" found: " + ", ".join(f"{c['NAME']} ({c['TYPE']})" for c in children)) # Leaves first: mountpoints and swap sit on the outermost devices. for child in reversed(children): dev = f"/dev/{child['NAME']}" mounts = child.get("MOUNTPOINTS", "") if "[SWAP]" in mounts: rc, out = _run(["swapoff", dev], run) lines.append(f" swapoff {dev}: {'ok' if rc == 0 else out}") elif mounts: rc, out = _run(["umount", "--all-targets", dev], run) lines.append(f" umount {dev}: {'ok' if rc == 0 else out}") # Device-mapper (LVM, thin pools, LUKS) and md stacks. Thin pools need # their thin LVs gone before the pool goes, and lsblk lists shared nodes # more than once, so go leaves-first and retry a couple of passes until # nothing dm/md-shaped is left. for _ in range(3): stacked = [c for c in reversed(_children(disk, run)) if c["TYPE"] != "part"] if not stacked: break for child in stacked: name, typ = child["NAME"], child["TYPE"] if typ.startswith("raid") or name.startswith("md"): cmd = ["mdadm", "--stop", f"/dev/{name}"] else: cmd = ["dmsetup", "remove", name] rc, out = _run(cmd, run) lines.append(f" {' '.join(cmd)}: {'ok' if rc == 0 else out}") leftovers = [c["NAME"] for c in _children(disk, run) if c["TYPE"] != "part"] if leftovers: lines.append( " WARNING: still in use after cleanup: " + ", ".join(leftovers) + " — the install may fail to re-read the partition table." ) # Old signatures (LVM PV headers, RAID superblocks) would otherwise be # re-activated by udev the moment the kernel re-reads the table. for child in reversed(_children(disk, run)): if child["TYPE"] == "part": rc, out = _run(["wipefs", "-a", f"/dev/{child['NAME']}"], run) lines.append(f" wipefs /dev/{child['NAME']}: {'ok' if rc == 0 else out}") rc, out = _run(["wipefs", "-a", disk], run) lines.append(f" wipefs {disk}: {'ok' if rc == 0 else out}") rc, out = _run(["blockdev", "--rereadpt", disk], run) lines.append(f" re-read partition table: {'ok' if rc == 0 else out}") return lines