126 lines
5 KiB
Python
126 lines
5 KiB
Python
|
|
"""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
|