From 7e359adfbbf04ae688f598e1ac0c4fb3bf48ad1a Mon Sep 17 00:00:00 2001 From: themylogin Date: Fri, 19 Jul 2024 15:19:42 +0200 Subject: [PATCH] Wipe unused boot-pool disks --- truenas_installer/disks.py | 16 ++++++++--- truenas_installer/install.py | 35 +++++++++++++++++++------ truenas_installer/installer_menu.py | 33 +++++++++++++++++++++-- truenas_installer/server/api/info.py | 10 +++++++ truenas_installer/server/api/install.py | 17 ++++++++++-- 5 files changed, 96 insertions(+), 15 deletions(-) diff --git a/truenas_installer/disks.py b/truenas_installer/disks.py index 5389bb4..8e6fbcf 100644 --- a/truenas_installer/disks.py +++ b/truenas_installer/disks.py @@ -9,12 +9,19 @@ MIN_DISK_SIZE = 8_000_000_000 +@dataclass +class ZFSMember: + name: str + pool: str + + @dataclass class Disk: name: str size: int model: str label: str + zfs_members: list[ZFSMember] removable: bool @@ -44,12 +51,15 @@ async def list_disks(): if m := re.search("Model: (.+)", (await run(["sgdisk", "-p", device], check=False)).stdout): model = m.group(1) + zfs_members = [] if disk["fstype"] is not None: label = disk["fstype"] else: children = disk.get("children", []) - if zfs_members := [child for child in children if child["fstype"] == "zfs_member"]: - label = f"zfs-\"{zfs_members[0]['label']}\"" + if zfs_members := [ZFSMember(child["name"], child["label"]) + for child in children + if child["fstype"] == "zfs_member"]: + label = ", ".join([f"zfs-\"{zfs_member.pool}\"" for zfs_member in zfs_members]) else: for fstype in ["ext4", "xfs"]: if labels := [child for child in children if child["fstype"] == fstype]: @@ -61,6 +71,6 @@ async def list_disks(): else: label = "" - disks.append(Disk(disk["name"], disk["size"], model, label, disk["rm"])) + disks.append(Disk(disk["name"], disk["size"], model, label, zfs_members, disk["rm"])) return disks diff --git a/truenas_installer/install.py b/truenas_installer/install.py index b8d8a67..b300d08 100644 --- a/truenas_installer/install.py +++ b/truenas_installer/install.py @@ -3,7 +3,9 @@ import os import subprocess import tempfile +from typing import Callable +from .disks import Disk from .exception import InstallError from .lock import installation_lock from .utils import get_partitions, run @@ -13,19 +15,24 @@ BOOT_POOL = "boot-pool" -async def install(disks, set_pmbr, authentication, post_install, sql, callback): +async def install(disks: list[Disk], destination_disks: list[str], wipe_disks: list[str], set_pmbr: bool, + authentication: dict | None, post_install: dict | None, sql: str | None, callback: Callable): with installation_lock: try: if not os.path.exists("/etc/hostid"): await run(["zgenhostid"]) - for disk in disks: + for disk in destination_disks: callback(0, f"Formatting disk {disk}") - await format_disk(f"/dev/{disk}", set_pmbr, callback) + await format_disk(disks, f"/dev/{disk}", set_pmbr, callback) + + for disk in wipe_disks: + callback(0, f"Wiping disk {disk}") + await wipe_disk(disks, f"/dev/{disk}", callback) disk_parts = list() part_num = 3 - for disk in disks: + for disk in destination_disks: found = (await get_partitions(disk, [part_num]))[part_num] if found is None: raise InstallError(f"Failed to find data partition on {disk!r}") @@ -35,21 +42,33 @@ async def install(disks, set_pmbr, authentication, post_install, sql, callback): callback(0, "Creating boot pool") await create_boot_pool(disk_parts) try: - await run_installer(disks, authentication, post_install, sql, callback) + await run_installer(destination_disks, authentication, post_install, sql, callback) finally: await run(["zpool", "export", "-f", BOOT_POOL]) except subprocess.CalledProcessError as e: raise InstallError(f"Command {' '.join(e.cmd)} failed:\n{e.stderr.rstrip()}") -async def format_disk(device, set_pmbr, callback): +async def wipe_disk(disks: list[Disk], device: str, callback: Callable): + for disk in disks: + if f"/dev/{disk.name}" == device: + for zfs_member in disk.zfs_members: + if (result := await run(["zpool", "labelclear", "-f", f"/dev/{zfs_member.name}"], + check=False)).returncode != 0: + callback(0, f"Warning: unable to wipe ZFS label from {zfs_member.name}: {result.stderr.rstrip()}") + pass + + break + if (result := await run(["wipefs", "-a", device], check=False)).returncode != 0: callback(0, f"Warning: unable to wipe partition table for {device}: {result.stderr.rstrip()}") - # Erase both typical metadata area. - await run(["sgdisk", "-Z", device], check=False) await run(["sgdisk", "-Z", device], check=False) + +async def format_disk(disks: list[Disk], device: str, set_pmbr: bool, callback: Callable): + await wipe_disk(disks, device, callback) + # Create BIOS boot partition await run(["sgdisk", "-a4096", "-n1:0:+1024K", "-t1:EF02", "-A1:set:2", device]) diff --git a/truenas_installer/installer_menu.py b/truenas_installer/installer_menu.py index 958b027..899f214 100644 --- a/truenas_installer/installer_menu.py +++ b/truenas_installer/installer_menu.py @@ -71,11 +71,31 @@ async def _install_upgrade_internal(self): ) continue + wipe_disks = [ + disk.name + for disk in disks + if ( + any(zfs_member.pool == "boot-pool" for zfs_member in disk.zfs_members) and + disk.name not in destination_disks + ) + ] + if wipe_disks: + # The presence of multiple `boot-pool` disks with different guids leads to boot pool import error + text = "\n".join([ + f"Disk(s) {', '.join(wipe_disks)} contain existing TrueNAS boot pool,", + "but they were not selected for TrueNAS installation. This configuration will not", + "work unless these disks are erased.", + "", + f"Proceed with erasing {', '.join(wipe_disks)}?" + ]) + if not await dialog_yesno("TrueNAS Installation", text): + continue + break text = "\n".join([ "WARNING:", - f"- This erases ALL partitions and data on {', '.join(destination_disks)}.", + f"- This erases ALL partitions and data on {', '.join(sorted(wipe_disks + destination_disks))}.", f"- {', '.join(destination_disks)} will be unavailable for use in storage pools.", "", "NOTE:", @@ -111,7 +131,16 @@ async def _install_upgrade_internal(self): sql = await serial_sql() try: - await install(destination_disks, set_pmbr, authentication_method, None, sql, self._callback) + await install( + disks, + destination_disks, + wipe_disks, + set_pmbr, + authentication_method, + None, + sql, + self._callback, + ) except InstallError as e: await dialog_msgbox("Installation Error", e.message) return False diff --git a/truenas_installer/server/api/info.py b/truenas_installer/server/api/info.py index de9e688..03d6e92 100644 --- a/truenas_installer/server/api/info.py +++ b/truenas_installer/server/api/info.py @@ -36,6 +36,16 @@ async def system_info(context): "size": {"type": "number"}, "model": {"type": "string"}, "label": {"type": "string"}, + "zfs_members": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "pool": {"type": "string"}, + }, + }, + }, "removable": {"type": "boolean"}, }, }, diff --git a/truenas_installer/server/api/install.py b/truenas_installer/server/api/install.py index c809d79..ced91f6 100644 --- a/truenas_installer/server/api/install.py +++ b/truenas_installer/server/api/install.py @@ -4,6 +4,7 @@ from aiohttp_rpc.protocol import JsonRpcRequest +from truenas_installer.disks import list_disks from truenas_installer.exception import InstallError from truenas_installer.install import install as install_ from truenas_installer.serial import serial_sql @@ -18,6 +19,10 @@ "required": ["disks", "set_pmbr", "authentication"], "additionalProperties": False, "properties": { + "wipe_disks": { + "type": "array", + "items": {"type": "string"}, + }, "disks": { "type": "array", "items": {"type": "string"}, @@ -77,8 +82,16 @@ async def install(context, params): Performs system installation. """ try: - await install_(params["disks"], params["set_pmbr"], params["authentication"], params.get("post_install", None), - await serial_sql(), functools.partial(callback, context.server)) + await install_( + await list_disks(), + params["disks"], + params.get("wipe_disks", []), + params["set_pmbr"], + params["authentication"], + params.get("post_install", None), + await serial_sql(), + functools.partial(callback, context.server), + ) except InstallError as e: raise Error(e.message, errno.EFAULT)