Skip to content

Commit

Permalink
Wipe unused boot-pool disks
Browse files Browse the repository at this point in the history
  • Loading branch information
themylogin committed Jul 19, 2024
1 parent 3483407 commit a6c86d8
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 15 deletions.
16 changes: 13 additions & 3 deletions truenas_installer/disks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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]:
Expand All @@ -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
35 changes: 27 additions & 8 deletions truenas_installer/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}")
Expand All @@ -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", "-a", f"/dev/{zfs_member.name}"],

This comment has been minimized.

Copy link
@yocalebo

yocalebo Jul 19, 2024

Contributor

There is no -a flag with zpool labelclear command.

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])

Expand Down
33 changes: 31 additions & 2 deletions truenas_installer/installer_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions truenas_installer/server/api/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
},
Expand Down
17 changes: 15 additions & 2 deletions truenas_installer/server/api/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"},
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit a6c86d8

Please sign in to comment.