From 65d48c92f8cf00c81e4c4bbc876414ea0b4760fa Mon Sep 17 00:00:00 2001 From: Daniel Maksymilian Syrnicki Date: Tue, 28 Apr 2026 12:09:19 +0200 Subject: [PATCH] feat(installer): filter the boot USB out of the install drive picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On bare-metal installs, `lsblk` reports the USB stick the live ISO booted from as TYPE=disk, so it showed up in the drive picker alongside the real install target — a user could in theory pick the USB they had just booted from. `findmnt /run/archiso/bootmnt` resolves the boot partition and `lsblk -no PKNAME` walks it up to the parent disk; that disk is dropped before scoring. On a normal box neither file nor mountpoint exist and the picker is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_drives.py | 20 ++++++++++++++++++ webinstaller/drives.py | 46 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/tests/test_drives.py b/tests/test_drives.py index 7603c81..3af53c5 100644 --- a/tests/test_drives.py +++ b/tests/test_drives.py @@ -95,3 +95,23 @@ def test_drive_type_label_nvme_ssd_hdd(): def test_parse_lsblk_handles_empty_output(): assert parse_lsblk_output("") == [] + + +def test_parse_lsblk_drops_boot_usb(monkeypatch): + import drives + + monkeypatch.setattr(drives, "_smart_status", lambda _: "passed") + output = "sda 500G disk\nsdb 16G disk\nnvme0n1 1T disk\n" + devices = parse_lsblk_output(output, boot_disk="sdb") + names = [d["name"] for d in devices] + assert "/dev/sdb" not in names + assert names == ["/dev/nvme0n1", "/dev/sda"] + + +def test_parse_lsblk_no_boot_disk_keeps_all(monkeypatch): + import drives + + monkeypatch.setattr(drives, "_smart_status", lambda _: "passed") + output = "sda 500G disk\nsdb 16G disk\n" + names = [d["name"] for d in parse_lsblk_output(output, boot_disk=None)] + assert set(names) == {"/dev/sda", "/dev/sdb"} diff --git a/webinstaller/drives.py b/webinstaller/drives.py index e3c089e..66a5686 100644 --- a/webinstaller/drives.py +++ b/webinstaller/drives.py @@ -1,6 +1,41 @@ import subprocess +def _boot_disk_name(): + """Return the parent disk name of the live-ISO boot media (e.g. "sdb"), or None. + + On a normal box `/run/archiso/bootmnt` does not exist and we return None, + leaving the device list untouched. On bare metal booted from USB this is + the stick we booted from — we want to filter it out so the user can't + accidentally pick it as the install target. + """ + try: + result = subprocess.run( + ["findmnt", "-no", "SOURCE", "/run/archiso/bootmnt"], + capture_output=True, + text=True, + ) + except FileNotFoundError: + return None + if result.returncode != 0: + return None + partition = result.stdout.strip() + if not partition: + return None + try: + parent = subprocess.run( + ["lsblk", "-no", "PKNAME", partition], + capture_output=True, + text=True, + ) + except FileNotFoundError: + return None + if parent.returncode != 0: + return None + name = parent.stdout.strip().splitlines()[0] if parent.stdout.strip() else "" + return name or None + + def _smart_status(device): try: result = subprocess.run( @@ -75,11 +110,14 @@ def score_device(device, size_gb): return get_drive_type_score(device) + get_drive_health(device) + get_size_score(size_gb) -def parse_lsblk_output(output): +def parse_lsblk_output(output, boot_disk=None): """Parse `lsblk -dn -o NAME,SIZE,TYPE` output into scored device dicts. Keeps only TYPE=disk so the live ISO's own squashfs (loop) and the boot - CD-ROM (rom) don't show up as install targets. + CD-ROM (rom) don't show up as install targets. If `boot_disk` is given, + that disk is also dropped — it's the USB stick the live ISO booted from + on bare metal, where it appears as TYPE=disk and would otherwise be a + valid-looking install target. """ devices = [] for line in output.strip().split("\n"): @@ -91,6 +129,8 @@ def parse_lsblk_output(output): name, size, dev_type = parts[0], parts[1], parts[2] if dev_type != "disk": continue + if boot_disk and name == boot_disk: + continue device = f"/dev/{name}" size_gb = parse_size_gb(size) status = _smart_status(device) @@ -120,7 +160,7 @@ def list_scored_devices(): except subprocess.CalledProcessError as e: print(f"Error listing devices: {e}") return [] - return parse_lsblk_output(result.stdout) + return parse_lsblk_output(result.stdout, boot_disk=_boot_disk_name()) def main():