From c2c16faaad3d8ca9299bbb55107104afd89d3c4a Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:35:52 -0500 Subject: [PATCH 01/12] blockdev: Add partn and pttype fields to Device Add two new fields to the Device struct that are already provided by lsblk's JSON output: - `partn`: Partition number (1-indexed), None for whole disk devices - `pttype`: Partition table type (e.g., "gpt", "dos") These fields will be used in subsequent commits to replace the sfdisk-based PartitionTable API with a unified lsblk-based Device API. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index a12527a94..67b60539e 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -33,6 +33,8 @@ pub struct Device { pub partlabel: Option, pub parttype: Option, pub partuuid: Option, + /// Partition number (1-indexed). None for whole disk devices. + pub partn: Option, pub children: Option>, pub size: u64, #[serde(rename = "maj:min")] @@ -46,6 +48,8 @@ pub struct Device { pub fstype: Option, pub uuid: Option, pub path: Option, + /// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices. + pub pttype: Option, } impl Device { @@ -501,9 +505,12 @@ mod test { let fixture = include_str!("../tests/fixtures/lsblk.json"); let devs: DevicesOutput = serde_json::from_str(fixture).unwrap(); let dev = devs.blockdevices.into_iter().next().unwrap(); + // The parent device has no partition number + assert_eq!(dev.partn, None); let children = dev.children.as_deref().unwrap(); assert_eq!(children.len(), 3); let first_child = &children[0]; + assert_eq!(first_child.partn, Some(1)); assert_eq!( first_child.parttype.as_deref().unwrap(), "21686148-6449-6e6f-744e-656564454649" From ccf1e9e2aa12a93ec3568e7b26317e8326355077 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:36:37 -0500 Subject: [PATCH 02/12] blockdev: Add partition-finding methods to Device Add new methods to the Device struct that provide the same functionality as the PartitionTable API but work directly on lsblk's Device structure: - node(): Alias for path() for compatibility - find_device_by_partno(partno): Find a child partition by number - find_partition_of_type(uuid): Find a child partition by type GUID - find_partition_of_esp(): Find the EFI System Partition These methods will replace the corresponding PartitionTable methods in subsequent commits. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 40 ++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index 67b60539e..daf16af5a 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -53,12 +53,43 @@ pub struct Device { } impl Device { - #[allow(dead_code)] // RHEL8's lsblk doesn't have PATH, so we do it pub fn path(&self) -> String { self.path.clone().unwrap_or(format!("/dev/{}", &self.name)) } + /// Alias for path() for compatibility + #[allow(dead_code)] + pub fn node(&self) -> String { + self.path() + } + + /// Find a child partition by partition number (1-indexed). + pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> { + self.children + .as_ref() + .ok_or_else(|| anyhow!("Device has no children"))? + .iter() + .find(|child| child.partn == Some(partno)) + .ok_or_else(|| anyhow!("Missing partition for index {partno}")) + } + + /// Find a child partition by partition type GUID. + pub fn find_partition_of_type(&self, uuid: &str) -> Option<&Device> { + self.children.as_ref()?.iter().find(|child| { + child + .parttype + .as_ref() + .is_some_and(|pt| pt.eq_ignore_ascii_case(uuid)) + }) + } + + /// Find the EFI System Partition (ESP) among children. + pub fn find_partition_of_esp(&self) -> Result<&Device> { + self.find_partition_of_type(ESP) + .ok_or(anyhow::anyhow!("ESP not found in partition table")) + } + #[allow(dead_code)] pub fn has_children(&self) -> bool { self.children.as_ref().is_some_and(|v| !v.is_empty()) @@ -519,6 +550,13 @@ mod test { first_child.partuuid.as_deref().unwrap(), "3979e399-262f-4666-aabc-7ab5d3add2f0" ); + // Verify find_device_by_partno works + let part2 = dev.find_device_by_partno(2).unwrap(); + assert_eq!(part2.partn, Some(2)); + assert_eq!(part2.parttype.as_deref().unwrap(), ESP); + // Verify find_partition_of_esp works + let esp = dev.find_partition_of_esp().unwrap(); + assert_eq!(esp.partn, Some(2)); } #[test] From c83ca617f00746e7cdca1f075736dd5279909ce1 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:37:44 -0500 Subject: [PATCH 03/12] blockdev: Add parent device lookup via list_dev_by_dir Add functionality to look up parent devices and list devices by directory: - Add `bootc-mount` and `cap-std-ext` dependencies for filesystem inspection - Add `parents: Option>` field (with `#[serde(skip)]`) to Device to store parent device hierarchy from lsblk --inverse - Add `list_parents()` private function to query parent devices - Modify `list_dev()` to populate the parents field - Add `list_dev_by_dir()` public function to find the device containing a filesystem mounted at a given directory This provides a replacement for the `find_parent_devices()` function and enables looking up block devices from mounted directories. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- Cargo.lock | 2 ++ crates/blockdev/Cargo.toml | 2 ++ crates/blockdev/src/blockdev.rs | 53 +++++++++++++++++++++++++++++++-- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdcd53be6..ede89d1f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,7 +175,9 @@ version = "0.1.0" dependencies = [ "anyhow", "bootc-internal-utils", + "bootc-mount", "camino", + "cap-std-ext", "fn-error-context", "indoc", "libc", diff --git a/crates/blockdev/Cargo.toml b/crates/blockdev/Cargo.toml index ff3cfde2c..7b99b384a 100644 --- a/crates/blockdev/Cargo.toml +++ b/crates/blockdev/Cargo.toml @@ -9,10 +9,12 @@ version = "0.1.0" [dependencies] # Internal crates bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.1.0" } +bootc-mount = { path = "../mount" } # Workspace dependencies anyhow = { workspace = true } camino = { workspace = true, features = ["serde1"] } +cap-std-ext = { workspace = true, features = ["fs_utf8"] } fn-error-context = { workspace = true } libc = { workspace = true } regex = { workspace = true } diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index daf16af5a..b14c8188f 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -6,6 +6,7 @@ use std::sync::OnceLock; use anyhow::{Context, Result, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; +use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; use regex::Regex; use serde::Deserialize; @@ -50,6 +51,10 @@ pub struct Device { pub path: Option, /// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices. pub pttype: Option, + + /// Parent devices (from lsblk --inverse). Not populated by JSON deserialization. + #[serde(skip)] + pub parents: Option>, } impl Device { @@ -129,6 +134,12 @@ impl Device { } } +/// List the device containing the filesystem mounted at the given directory. +pub fn list_dev_by_dir(dir: &Dir) -> Result { + let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?; + list_dev(&Utf8PathBuf::from(&fsinfo.source)) +} + #[context("Listing device {dev}")] pub fn list_dev(dev: &Utf8Path) -> Result { let mut devs: DevicesOutput = Command::new("lsblk") @@ -139,10 +150,48 @@ pub fn list_dev(dev: &Utf8Path) -> Result { for dev in devs.blockdevices.iter_mut() { dev.backfill_missing()?; } - devs.blockdevices + let mut device = devs + .blockdevices .into_iter() .next() - .ok_or_else(|| anyhow!("no device output from lsblk for {dev}")) + .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))?; + + // Get parent devices via lsblk --inverse + let parents = list_parents(dev)?; + device.parents = if parents.is_empty() { + None + } else { + Some(parents) + }; + + Ok(device) +} + +/// List parent devices of the given device using lsblk --inverse. +/// Returns an empty Vec if the device has no parents (e.g., a whole disk). +#[context("Listing parent devices of {dev}")] +fn list_parents(dev: &Utf8Path) -> Result> { + let mut output: DevicesOutput = Command::new("lsblk") + .args(["-J", "-b", "-O", "--inverse"]) + .arg(dev) + .log_debug() + .run_and_parse_json()?; + + // The first device in inverse output is the target device itself, + // its children (in inverse mode) are the parent devices + let target = output.blockdevices.first_mut(); + + match target { + Some(target) => { + // In inverse mode, "children" are actually parents + let mut parents: Vec = target.children.take().unwrap_or_default(); + for parent in parents.iter_mut() { + parent.backfill_missing()?; + } + Ok(parents) + } + None => Ok(Vec::new()), + } } #[derive(Debug, Deserialize)] From 201751f62ab84b7cdbc552fc55e3a31f633bf776 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:38:47 -0500 Subject: [PATCH 04/12] install: Mirror /run/udev for lsblk during install Add /run/udev to the list of host mounts that are mirrored during install-to-disk. This is required for lsblk to properly query device information when running inside the installation container. Without access to udev state, lsblk may fail to return complete device information needed for partition detection and device lookup. See: https://github.com/bootc-dev/bootc/pull/688 Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- crates/lib/src/install.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index a056d03b7..ce7c513ea 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1598,6 +1598,9 @@ async fn prepare_install( // In some cases we may create large files, and it's better not to have those // in our overlayfs. bootc_mount::ensure_mirrored_host_mount("/var/tmp")?; + // udev state is required for running lsblk during install to-disk + // see https://github.com/bootc-dev/bootc/pull/688 + bootc_mount::ensure_mirrored_host_mount("/run/udev")?; // We also always want /tmp to be a proper tmpfs on general principle. setup_tmp_mount()?; // Allocate a temporary directory we can use in various places to avoid From 5707e65720d0f5f48c5729e0cab6a9848f14c88e Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:41:11 -0500 Subject: [PATCH 05/12] lib: Migrate consumers from PartitionTable to Device API Migrate all consumers of the sfdisk-based PartitionTable API to use the new lsblk-based Device API: - Change RootSetup.device_info type from PartitionTable to Device - Update partition type checking to use pttype.as_deref() instead of the PartitionType enum - Replace partitions_of() calls with list_dev() - Replace find_partno() with find_device_by_partno() - Replace .node access with .path() - Remove esp_in() function from bootloader.rs - Remove get_esp_partition_node() from bootloader.rs - Remove get_sysroot_parent_dev() and get_esp_partition() from boot.rs - Update ESP finding to use Device.find_partition_of_esp() - Update parent device lookup to use list_dev_by_dir() The Device API provides a unified way to query block device information using lsblk, which is more reliable than sfdisk for partition discovery and works consistently across different device types. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 9 +++++ crates/lib/src/bootc_composefs/boot.rs | 51 +++++++----------------- crates/lib/src/bootloader.rs | 55 +++++++++++--------------- crates/lib/src/install.rs | 20 ++++++---- crates/lib/src/install/baseline.rs | 54 +++++++++++-------------- crates/lib/src/store/mod.rs | 11 +++--- 6 files changed, 88 insertions(+), 112 deletions(-) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index b14c8188f..b983bb3c5 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -100,6 +100,15 @@ impl Device { self.children.as_ref().is_some_and(|v| !v.is_empty()) } + /// Re-query this device's information from lsblk, updating all fields. + /// This is useful after partitioning when the device's children have changed. + pub fn refresh(&mut self) -> Result<()> { + let path = self.path(); + let new_device = list_dev(Utf8Path::new(&path))?; + *self = new_device; + Ok(()) + } + // The "start" parameter was only added in a version of util-linux that's only // in Fedora 40 as of this writing. fn backfill_start(&mut self) -> Result<()> { diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index c4787a917..90050a7d1 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -93,6 +93,7 @@ use rustix::{mount::MountFlags, path::Arg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::bootc_composefs::state::{get_booted_bls, write_composefs_state}; use crate::parsers::bls_config::{BLSConfig, BLSConfigType}; use crate::parsers::grub_menuconfig::MenuEntry; use crate::task::Task; @@ -104,10 +105,6 @@ use crate::{ bootc_composefs::repo::open_composefs_repo, store::{ComposefsFilesystem, Storage}, }; -use crate::{ - bootc_composefs::state::{get_booted_bls, write_composefs_state}, - bootloader::esp_in, -}; use crate::{ bootc_composefs::status::get_container_manifest_and_config, bootc_kargs::compute_new_kargs, }; @@ -214,30 +211,12 @@ fi ) } -pub fn get_esp_partition(device: &str) -> Result<(String, Option)> { - let device_info = bootc_blockdev::partitions_of(Utf8Path::new(device))?; - let esp = crate::bootloader::esp_in(&device_info)?; - - Ok((esp.node.clone(), esp.uuid.clone())) -} - /// Mount the ESP from the provided device pub fn mount_esp(device: &str) -> Result { let flags = MountFlags::NOEXEC | MountFlags::NOSUID; TempMount::mount_dev(device, "vfat", flags, Some(c"fmask=0177,dmask=0077")) } -pub fn get_sysroot_parent_dev(physical_root: &Dir) -> Result { - let fsinfo = inspect_filesystem_of_dir(physical_root)?; - let parent_devices = find_parent_devices(&fsinfo.source)?; - - let Some(parent) = parent_devices.into_iter().next() else { - anyhow::bail!("Could not find parent device of system root"); - }; - - Ok(parent) -} - /// Filename release field for primary (new/upgraded) entry. /// Grub parses this as the "release" field and sorts descending, so "1" > "0". pub(crate) const FILENAME_PRIORITY_PRIMARY: &str = "1"; @@ -521,11 +500,11 @@ pub(crate) fn setup_composefs_bls_boot( cmdline_options.extend(&Cmdline::from(&composefs_cmdline)); // Locate ESP partition device - let esp_part = esp_in(&root_setup.device_info)?; + let esp_part = root_setup.device_info.find_partition_of_esp()?; ( root_setup.physical_root_path.clone(), - esp_part.node.clone(), + esp_part.path(), cmdline_options, fs, postfetch.detected_bootloader.clone(), @@ -533,7 +512,6 @@ pub(crate) fn setup_composefs_bls_boot( } BootSetupType::Upgrade((storage, fs, host)) => { - let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); let boot_dir = storage.require_boot_dir()?; @@ -556,9 +534,13 @@ pub(crate) fn setup_composefs_bls_boot( Parameter::parse(¶m).context("Failed to create 'composefs=' parameter")?; cmdline.add_or_modify(¶m); + // Locate ESP partition device + let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?; + let esp_dev = root_dev.find_partition_of_esp()?; + ( Utf8PathBuf::from("/sysroot"), - get_esp_partition(&sysroot_parent)?.0, + esp_dev.path(), cmdline, fs, bootloader, @@ -1069,11 +1051,11 @@ pub(crate) fn setup_composefs_uki_boot( BootSetupType::Setup((root_setup, state, postfetch, ..)) => { state.require_no_kargs_for_uki()?; - let esp_part = esp_in(&root_setup.device_info)?; + let esp_part = root_setup.device_info.find_partition_of_esp()?; ( root_setup.physical_root_path.clone(), - esp_part.node.clone(), + esp_part.path(), postfetch.detected_bootloader.clone(), state.composefs_options.insecure, state.composefs_options.uki_addon.as_ref(), @@ -1082,16 +1064,13 @@ pub(crate) fn setup_composefs_uki_boot( BootSetupType::Upgrade((storage, _, host)) => { let sysroot = Utf8PathBuf::from("/sysroot"); // Still needed for root_path - let sysroot_parent = get_sysroot_parent_dev(&storage.physical_root)?; let bootloader = host.require_composefs_booted()?.bootloader.clone(); - ( - sysroot, - get_esp_partition(&sysroot_parent)?.0, - bootloader, - false, - None, - ) + // Locate ESP partition device + let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?; + let esp_dev = root_dev.find_partition_of_esp()?; + + (sysroot, esp_dev.path(), bootloader, false, None) } }; diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 1e1157ef8..921009af7 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -8,10 +8,9 @@ use cap_std_ext::cap_std::fs::Dir; use cap_std_ext::dirext::CapStdExtDirExt; use fn_error_context::context; -use bootc_blockdev::{Partition, PartitionTable}; use bootc_mount as mount; -use crate::bootc_composefs::boot::{SecurebootKeys, get_sysroot_parent_dev, mount_esp}; +use crate::bootc_composefs::boot::{SecurebootKeys, mount_esp}; use crate::{discoverable_partition_specification, utils}; /// The name of the mountpoint for efi (as a subdirectory of /boot, or at the toplevel) @@ -24,21 +23,6 @@ const BOOTUPD_UPDATES: &str = "usr/lib/bootupd/updates"; // from: https://github.com/systemd/systemd/blob/26b2085d54ebbfca8637362eafcb4a8e3faf832f/man/systemd-boot.xml#L392 const SYSTEMD_KEY_DIR: &str = "loader/keys"; -#[allow(dead_code)] -pub(crate) fn esp_in(device: &PartitionTable) -> Result<&Partition> { - device - .find_partition_of_type(discoverable_partition_specification::ESP) - .ok_or(anyhow::anyhow!("ESP not found in partition table")) -} - -/// Get esp partition node based on the root dir -pub(crate) fn get_esp_partition_node(root: &Dir) -> Result> { - let device = get_sysroot_parent_dev(&root)?; - let base_partitions = bootc_blockdev::partitions_of(Utf8Path::new(&device))?; - let esp = base_partitions.find_partition_of_esp()?; - Ok(esp.map(|v| v.node.clone())) -} - /// Mount ESP part at /boot/efi pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) -> Result<()> { let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR); @@ -60,9 +44,12 @@ pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) } else { root }; - if let Some(esp_part) = get_esp_partition_node(physical_root)? { - bootc_mount::mount(&esp_part, &root_path.join(&efi_path))?; - tracing::debug!("Mounted {esp_part} at /boot/efi"); + + let dev = bootc_blockdev::list_dev_by_dir(physical_root)?; + if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) { + let esp_path = esp_dev.path(); + bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?; + tracing::debug!("Mounted {esp_path} at /boot/efi"); } Ok(()) } @@ -82,7 +69,7 @@ pub(crate) fn supports_bootupd(root: &Dir) -> Result { #[context("Installing bootloader")] pub(crate) fn install_via_bootupd( - device: &PartitionTable, + device: &bootc_blockdev::Device, rootfs: &Utf8Path, configopts: &crate::install::InstallConfigOpts, deployment_path: Option<&str>, @@ -104,6 +91,8 @@ pub(crate) fn install_via_bootupd( println!("Installing bootloader via bootupd"); + let device_path = device.path(); + // Build the bootupctl arguments let mut bootupd_args: Vec<&str> = vec!["backend", "install"]; if configopts.bootupd_skip_boot_uuid { @@ -118,7 +107,7 @@ pub(crate) fn install_via_bootupd( if let Some(ref opts) = bootupd_opts { bootupd_args.extend(opts.iter().copied()); } - bootupd_args.extend(["--device", device.path().as_str(), rootfs_mount]); + bootupd_args.extend(["--device", &device_path, rootfs_mount]); // Run inside a bwrap container. It takes care of mounting and creating // the necessary API filesystems in the target deployment and acts as @@ -133,16 +122,20 @@ pub(crate) fn install_via_bootupd( let mut bwrap_args = vec!["bootupctl"]; bwrap_args.extend(bootupd_args); + // Collect partition paths first so they live long enough + let partition_paths: Vec = + device.children.iter().flatten().map(|p| p.path()).collect(); + let mut cmd = BwrapCmd::new(&target_root) // Bind mount /boot from the physical target root so bootupctl can find // the boot partition and install the bootloader there .bind(&boot_path, &"/boot") // Bind the target block device inside the bwrap container so bootupctl can access it - .bind_device(device.path().as_str()); + .bind_device(&device_path); - // Also bind all partitions of the tafet block device - for partition in &device.partitions { - cmd = cmd.bind_device(&partition.node); + // Also bind all partitions of the target block device + for part_path in &partition_paths { + cmd = cmd.bind_device(part_path); } // The $PATH in the bwrap env is not complete enough for some images @@ -165,7 +158,7 @@ pub(crate) fn install_via_bootupd( #[context("Installing bootloader")] pub(crate) fn install_systemd_boot( - device: &PartitionTable, + device: &bootc_blockdev::Device, _rootfs: &Utf8Path, _configopts: &crate::install::InstallConfigOpts, _deployment_path: Option<&str>, @@ -175,7 +168,7 @@ pub(crate) fn install_systemd_boot( .find_partition_of_type(discoverable_partition_specification::ESP) .ok_or_else(|| anyhow::anyhow!("ESP partition not found"))?; - let esp_mount = mount_esp(&esp_part.node).context("Mounting ESP")?; + let esp_mount = mount_esp(&esp_part.path()).context("Mounting ESP")?; let esp_path = Utf8Path::from_path(esp_mount.dir.path()) .ok_or_else(|| anyhow::anyhow!("Failed to convert ESP mount path to UTF-8"))?; @@ -215,7 +208,7 @@ pub(crate) fn install_systemd_boot( } #[context("Installing bootloader using zipl")] -pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Result<()> { +pub(crate) fn install_via_zipl(device: &bootc_blockdev::Device, boot_uuid: &str) -> Result<()> { // Identify the target boot partition from UUID let fs = mount::inspect_filesystem_by_uuid(boot_uuid)?; let boot_dir = Utf8Path::new(&fs.target); @@ -224,7 +217,7 @@ pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Resu // Ensure that the found partition is a part of the target device let device_path = device.path(); - let partitions = bootc_blockdev::list_dev(device_path)? + let partitions = bootc_blockdev::list_dev(Utf8Path::new(&device_path))? .children .with_context(|| format!("no partition found on {device_path}"))?; let boot_part = partitions @@ -283,7 +276,7 @@ pub(crate) fn install_via_zipl(device: &PartitionTable, boot_uuid: &str) -> Resu .args(["--image", image.as_str()]) .args(["--ramdisk", ramdisk.as_str()]) .args(["--parameters", options]) - .args(["--targetbase", device_path.as_str()]) + .args(["--targetbase", &device_path]) .args(["--targettype", "SCSI"]) .args(["--targetblocksize", "512"]) .args(["--targetoffset", &boot_part_offset.to_string()]) diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index ce7c513ea..a0fdb057a 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1271,7 +1271,7 @@ pub(crate) fn exec_in_host_mountns(args: &[std::ffi::OsString]) -> Result<()> { pub(crate) struct RootSetup { #[cfg(feature = "install-to-disk")] luks_device: Option, - pub(crate) device_info: bootc_blockdev::PartitionTable, + pub(crate) device_info: bootc_blockdev::Device, /// Absolute path to the location where we've mounted the physical /// root filesystem for the system we're installing. pub(crate) physical_root_path: Utf8PathBuf, @@ -1875,15 +1875,18 @@ async fn install_to_filesystem_impl( // Drop exclusive ownership since we're done with mutation let rootfs = &*rootfs; - match &rootfs.device_info.label { - bootc_blockdev::PartitionType::Dos => crate::utils::medium_visibility_warning( + match rootfs.device_info.pttype.as_deref() { + Some("dos") => crate::utils::medium_visibility_warning( "Installing to `dos` format partitions is not recommended", ), - bootc_blockdev::PartitionType::Gpt => { + Some("gpt") => { // The only thing we should be using in general } - bootc_blockdev::PartitionType::Unknown(o) => { - crate::utils::medium_visibility_warning(&format!("Unknown partition label {o}")) + Some(o) => { + crate::utils::medium_visibility_warning(&format!("Unknown partition table type {o}")) + } + None => { + // No partition table type - may be a filesystem install or loop device } } @@ -2451,12 +2454,13 @@ pub(crate) async fn install_to_filesystem( "Found multiple parent devices {parent} and {next}; not currently supported" ); } - dev = parent; + // Re-query to get this parent's own parents + dev = bootc_blockdev::list_dev(Utf8Path::new(&parent.path()))?; } dev }; tracing::debug!("Backing device: {backing_device}"); - let device_info = bootc_blockdev::partitions_of(Utf8Path::new(&backing_device))?; + let device_info = bootc_blockdev::list_dev(Utf8Path::new(&backing_device))?; let rootarg = format!("root={}", root_info.mount_spec); // CLI takes precedence over config file. diff --git a/crates/lib/src/install/baseline.rs b/crates/lib/src/install/baseline.rs index d05604ed5..9b393b467 100644 --- a/crates/lib/src/install/baseline.rs +++ b/crates/lib/src/install/baseline.rs @@ -182,9 +182,7 @@ pub(crate) fn install_create_rootfs( .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?; // Verify that the target is empty (if not already wiped in particular, but it's // also good to verify that the wipe worked) - let device = bootc_blockdev::list_dev(&opts.device)?; - // Canonicalize devpath - let devpath: Utf8PathBuf = device.path().into(); + let mut device = bootc_blockdev::list_dev(&opts.device)?; // Always disallow writing to mounted device if is_mounted_in_pid1_mountns(&device.path())? { @@ -333,23 +331,24 @@ pub(crate) fn install_create_rootfs( // we're targeting, but this is a simple coarse hammer. udev_settle()?; - // Re-read what we wrote into structured information - let base_partitions = &bootc_blockdev::partitions_of(&devpath)?; + // Re-read partition table to get updated children + device.refresh()?; - let root_partition = base_partitions.find_partno(rootpn)?; + let root_device = device.find_device_by_partno(rootpn)?; // Verify the partition type matches the DPS root partition type for this architecture let expected_parttype = crate::discoverable_partition_specification::this_arch_root(); - if !root_partition + if !root_device .parttype - .eq_ignore_ascii_case(expected_parttype) + .as_ref() + .is_some_and(|pt| pt.eq_ignore_ascii_case(expected_parttype)) { anyhow::bail!( "root partition {rootpn} has type {}; expected {expected_parttype}", - root_partition.parttype.as_str() + root_device.parttype.as_deref().unwrap_or("") ); } - let (rootdev, root_blockdev_kargs) = match block_setup { - BlockSetup::Direct => (root_partition.node.to_owned(), None), + let (rootdev_path, root_blockdev_kargs) = match block_setup { + BlockSetup::Direct => (root_device.path(), None), BlockSetup::Tpm2Luks => { let uuid = uuid::Uuid::new_v4().to_string(); // This will be replaced via --wipe-slot=all when binding to tpm below @@ -360,23 +359,23 @@ pub(crate) fn install_create_rootfs( let tmp_keyfile = tmp_keyfile.path(); let dummy_passphrase_input = Some(dummy_passphrase.as_bytes()); - let root_devpath = root_partition.path(); + let root_devpath = root_device.path(); Task::new("Initializing LUKS for root", "cryptsetup") .args(["luksFormat", "--uuid", uuid.as_str(), "--key-file"]) .args([tmp_keyfile]) - .args([root_devpath]) + .arg(&root_devpath) .run()?; // The --wipe-slot=all removes our temporary passphrase, and binds to the local TPM device. // We also use .verbose() here as the details are important/notable. Task::new("Enrolling root device with TPM", "systemd-cryptenroll") .args(["--wipe-slot=all", "--tpm2-device=auto", "--unlock-key-file"]) .args([tmp_keyfile]) - .args([root_devpath]) + .arg(&root_devpath) .verbose() .run_with_stdin_buf(dummy_passphrase_input)?; Task::new("Opening root LUKS device", "cryptsetup") - .args(["luksOpen", root_devpath.as_str(), luks_name]) + .args(["luksOpen", &root_devpath, luks_name]) .run()?; let rootdev = format!("/dev/mapper/{luks_name}"); let kargs = vec![ @@ -389,20 +388,14 @@ pub(crate) fn install_create_rootfs( // Initialize the /boot filesystem let bootdev = if let Some(bootpn) = boot_partno { - Some(base_partitions.find_partno(bootpn)?) + Some(device.find_device_by_partno(bootpn)?) } else { None }; let boot_uuid = if let Some(bootdev) = bootdev { Some( - mkfs( - bootdev.node.as_str(), - root_filesystem, - "boot", - opts.wipe, - [], - ) - .context("Initializing /boot")?, + mkfs(&bootdev.path(), root_filesystem, "boot", opts.wipe, []) + .context("Initializing /boot")?, ) } else { None @@ -416,7 +409,7 @@ pub(crate) fn install_create_rootfs( // Initialize rootfs let root_uuid = mkfs( - &rootdev, + &rootdev_path, root_filesystem, "root", opts.wipe, @@ -456,7 +449,7 @@ pub(crate) fn install_create_rootfs( } } - bootc_mount::mount(&rootdev, &physical_root_path)?; + bootc_mount::mount(&rootdev_path, &physical_root_path)?; let target_rootfs = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?; crate::lsm::ensure_dir_labeled(&target_rootfs, "", Some("/".into()), 0o755.into(), sepolicy)?; let physical_root = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?; @@ -464,16 +457,16 @@ pub(crate) fn install_create_rootfs( // Create the underlying mount point directory, which should be labeled crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?; if let Some(bootdev) = bootdev { - bootc_mount::mount(bootdev.node.as_str(), &bootfs)?; + bootc_mount::mount(&bootdev.path(), &bootfs)?; } // And we want to label the root mount of /boot crate::lsm::ensure_dir_labeled(&target_rootfs, "boot", None, 0o755.into(), sepolicy)?; // Create the EFI system partition, if applicable if let Some(esp_partno) = esp_partno { - let espdev = base_partitions.find_partno(esp_partno)?; + let espdev = device.find_device_by_partno(esp_partno)?; Task::new("Creating ESP filesystem", "mkfs.fat") - .args([espdev.node.as_str(), "-n", "EFI-SYSTEM"]) + .args([&espdev.path(), "-n", "EFI-SYSTEM"]) .verbose() .quiet_output() .run()?; @@ -485,10 +478,9 @@ pub(crate) fn install_create_rootfs( BlockSetup::Direct => None, BlockSetup::Tpm2Luks => Some(luks_name.to_string()), }; - let device_info = bootc_blockdev::partitions_of(&devpath)?; Ok(RootSetup { luks_device, - device_info, + device_info: device, physical_root_path, physical_root, target_root_path: None, diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 260fe96b5..755c1b91a 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -34,7 +34,7 @@ use ostree_ext::sysroot::SysrootLock; use ostree_ext::{gio, ostree}; use rustix::fs::Mode; -use crate::bootc_composefs::boot::{get_esp_partition, get_sysroot_parent_dev, mount_esp}; +use crate::bootc_composefs::boot::mount_esp; use crate::bootc_composefs::status::{ComposefsCmdline, composefs_booted, get_bootloader}; use crate::lsm; use crate::podstorage::CStorage; @@ -172,11 +172,10 @@ impl BootedStorage { } let composefs = Arc::new(composefs); - // NOTE: This is assuming that we'll only have composefs in a UEFI system - // We do have this assumptions in a lot of other places - let parent = get_sysroot_parent_dev(&physical_root)?; - let (esp_part, ..) = get_esp_partition(&parent)?; - let esp_mount = mount_esp(&esp_part)?; + //TODO: this assumes a single ESP on the root device + let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?; + let esp_dev = root_dev.find_partition_of_esp()?; + let esp_mount = mount_esp(&esp_dev.path())?; let boot_dir = match get_bootloader()? { Bootloader::Grub => physical_root.open_dir("boot").context("Opening boot")?, From 6a243657d08b0a42605a7ff405d4eeb33078eee2 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 10:44:10 -0500 Subject: [PATCH 06/12] blockdev: Recursively populate parent devices in list_dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The parent device traversal in install.rs exits early when the root filesystem is on LVM on a partition (e.g., /dev/BL/root02 → /dev/loop0p4 → /dev/loop0). The issue is that list_parents() uses lsblk --inverse which returns a nested structure where parent devices have their own parents in their "children" field (inverse terminology). We were only extracting the immediate parents without converting their children to parents. Fix by adding convert_inverse_children_to_parents() which recursively walks the inverse device tree and converts each device's children field to a parents field, enabling full traversal up the device hierarchy. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- Cargo.lock | 1 - crates/blockdev/Cargo.toml | 1 - crates/blockdev/src/blockdev.rs | 349 +------------------------------- crates/lib/src/install.rs | 25 ++- 4 files changed, 25 insertions(+), 351 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ede89d1f5..25a3c1675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,6 @@ dependencies = [ "fn-error-context", "indoc", "libc", - "regex", "rustix", "serde", "serde_json", diff --git a/crates/blockdev/Cargo.toml b/crates/blockdev/Cargo.toml index 7b99b384a..0f236e5cb 100644 --- a/crates/blockdev/Cargo.toml +++ b/crates/blockdev/Cargo.toml @@ -17,7 +17,6 @@ camino = { workspace = true, features = ["serde1"] } cap-std-ext = { workspace = true, features = ["fs_utf8"] } fn-error-context = { workspace = true } libc = { workspace = true } -regex = { workspace = true } rustix = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index b983bb3c5..f447cfc7b 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -1,22 +1,15 @@ -use std::collections::HashMap; use std::env; use std::path::Path; use std::process::{Command, Stdio}; -use std::sync::OnceLock; use anyhow::{Context, Result, anyhow}; use camino::{Utf8Path, Utf8PathBuf}; use cap_std_ext::cap_std::fs::Dir; use fn_error_context::context; -use regex::Regex; use serde::Deserialize; use bootc_utils::CommandRunExt; -/// EFI System Partition (ESP) on MBR -/// Refer to -pub const ESP_ID_MBR: &[u8] = &[0x06, 0xEF]; - /// EFI System Partition (ESP) for UEFI boot on GPT pub const ESP: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b"; @@ -196,6 +189,8 @@ fn list_parents(dev: &Utf8Path) -> Result> { let mut parents: Vec = target.children.take().unwrap_or_default(); for parent in parents.iter_mut() { parent.backfill_missing()?; + // Recursively convert children to parents for the full chain + convert_inverse_children_to_parents(parent)?; } Ok(parents) } @@ -203,120 +198,17 @@ fn list_parents(dev: &Utf8Path) -> Result> { } } -#[derive(Debug, Deserialize)] -struct SfDiskOutput { - partitiontable: PartitionTable, -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct Partition { - pub node: String, - pub start: u64, - pub size: u64, - #[serde(rename = "type")] - pub parttype: String, - pub uuid: Option, - pub name: Option, - pub bootable: Option, -} - -#[derive(Debug, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum PartitionType { - Dos, - Gpt, - Unknown(String), -} - -#[derive(Debug, Deserialize)] -#[allow(dead_code)] -pub struct PartitionTable { - pub label: PartitionType, - pub id: String, - pub device: String, - // We're not using these fields - // pub unit: String, - // pub firstlba: u64, - // pub lastlba: u64, - // pub sectorsize: u64, - pub partitions: Vec, -} - -impl PartitionTable { - /// Find the partition with the given device name - #[allow(dead_code)] - pub fn find<'a>(&'a self, devname: &str) -> Option<&'a Partition> { - self.partitions.iter().find(|p| p.node.as_str() == devname) - } - - pub fn path(&self) -> &Utf8Path { - self.device.as_str().into() - } - - // Find the partition with the given offset (starting at 1) - #[allow(dead_code)] - pub fn find_partno(&self, partno: u32) -> Result<&Partition> { - let r = self - .partitions - .get(partno.checked_sub(1).expect("1 based partition offset") as usize) - .ok_or_else(|| anyhow::anyhow!("Missing partition for index {partno}"))?; - Ok(r) - } - - /// Find the partition with the given type UUID (case-insensitive). - /// - /// Partition type UUIDs are compared case-insensitively per the GPT specification, - /// as different tools may report them in different cases. - pub fn find_partition_of_type(&self, uuid: &str) -> Option<&Partition> { - self.partitions.iter().find(|p| p.parttype_matches(uuid)) - } - - /// Find the partition with bootable is 'true'. - pub fn find_partition_of_bootable(&self) -> Option<&Partition> { - self.partitions.iter().find(|p| p.is_bootable()) - } - - /// Find the esp partition. - pub fn find_partition_of_esp(&self) -> Result> { - match &self.label { - PartitionType::Dos => Ok(self.partitions.iter().find(|b| { - u8::from_str_radix(&b.parttype, 16) - .map(|pt| ESP_ID_MBR.contains(&pt)) - .unwrap_or(false) - })), - PartitionType::Gpt => Ok(self.find_partition_of_type(ESP)), - _ => Err(anyhow::anyhow!("Unsupported partition table type")), +/// In lsblk --inverse output, "children" are actually parent devices. +/// This function recursively converts the children field to the parents field. +fn convert_inverse_children_to_parents(dev: &mut Device) -> Result<()> { + if let Some(mut children) = dev.children.take() { + for child in children.iter_mut() { + child.backfill_missing()?; + convert_inverse_children_to_parents(child)?; } + dev.parents = Some(children); } -} - -impl Partition { - #[allow(dead_code)] - pub fn path(&self) -> &Utf8Path { - self.node.as_str().into() - } - - /// Check if this partition's type matches the given UUID (case-insensitive). - /// - /// Partition type UUIDs are compared case-insensitively per the GPT specification, - /// as different tools may report them in different cases. - pub fn parttype_matches(&self, uuid: &str) -> bool { - self.parttype.eq_ignore_ascii_case(uuid) - } - - /// Check this partition's bootable property. - pub fn is_bootable(&self) -> bool { - self.bootable.unwrap_or(false) - } -} - -#[context("Listing partitions of {dev}")] -pub fn partitions_of(dev: &Utf8Path) -> Result { - let o: SfDiskOutput = Command::new("sfdisk") - .args(["-J", dev.as_str()]) - .run_and_parse_json()?; - Ok(o.partitiontable) + Ok(()) } pub struct LoopbackDevice { @@ -497,52 +389,6 @@ pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> { } } -/// Parse key-value pairs from lsblk --pairs. -/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't. -fn split_lsblk_line(line: &str) -> HashMap { - static REGEX: OnceLock = OnceLock::new(); - let regex = REGEX.get_or_init(|| Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap()); - let mut fields: HashMap = HashMap::new(); - for cap in regex.captures_iter(line) { - fields.insert(cap[1].to_string(), cap[2].to_string()); - } - fields -} - -/// This is a bit fuzzy, but... this function will return every block device in the parent -/// hierarchy of `device` capable of containing other partitions. So e.g. parent devices of type -/// "part" doesn't match, but "disk" and "mpath" does. -pub fn find_parent_devices(device: &str) -> Result> { - let output = Command::new("lsblk") - // Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option - .arg("--pairs") - .arg("--paths") - .arg("--inverse") - .arg("--output") - .arg("NAME,TYPE") - .arg(device) - .run_get_string()?; - let mut parents = Vec::new(); - // skip first line, which is the device itself - for line in output.lines().skip(1) { - let dev = split_lsblk_line(line); - let name = dev - .get("NAME") - .with_context(|| format!("device in hierarchy of {device} missing NAME"))?; - let kind = dev - .get("TYPE") - .with_context(|| format!("device in hierarchy of {device} missing TYPE"))?; - if kind == "disk" || kind == "loop" { - parents.push(name.clone()); - } else if kind == "mpath" { - parents.push(name.clone()); - // we don't need to know what disks back the multipath - break; - } - } - Ok(parents) -} - /// Parse a string into mibibytes pub fn parse_size_mib(mut s: &str) -> Result { let suffixes = [ @@ -616,177 +462,4 @@ mod test { let esp = dev.find_partition_of_esp().unwrap(); assert_eq!(esp.partn, Some(2)); } - - #[test] - fn test_parse_sfdisk() -> Result<()> { - let fixture = indoc::indoc! { r#" - { - "partitiontable": { - "label": "gpt", - "id": "A67AA901-2C72-4818-B098-7F1CAC127279", - "device": "/dev/loop0", - "unit": "sectors", - "firstlba": 34, - "lastlba": 20971486, - "sectorsize": 512, - "partitions": [ - { - "node": "/dev/loop0p1", - "start": 2048, - "size": 8192, - "type": "9E1A2D38-C612-4316-AA26-8B49521E5A8B", - "uuid": "58A4C5F0-BD12-424C-B563-195AC65A25DD", - "name": "PowerPC-PReP-boot" - },{ - "node": "/dev/loop0p2", - "start": 10240, - "size": 20961247, - "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "uuid": "F51ABB0D-DA16-4A21-83CB-37F4C805AAA0", - "name": "root" - } - ] - } - } - "# }; - let table: SfDiskOutput = serde_json::from_str(fixture).unwrap(); - assert_eq!( - table.partitiontable.find("/dev/loop0p2").unwrap().size, - 20961247 - ); - Ok(()) - } - - #[test] - fn test_parttype_matches() { - let partition = Partition { - node: "/dev/loop0p1".to_string(), - start: 2048, - size: 8192, - parttype: "c12a7328-f81f-11d2-ba4b-00a0c93ec93b".to_string(), // lowercase ESP UUID - uuid: Some("58A4C5F0-BD12-424C-B563-195AC65A25DD".to_string()), - name: Some("EFI System".to_string()), - bootable: None, - }; - - // Test exact match (lowercase) - assert!(partition.parttype_matches("c12a7328-f81f-11d2-ba4b-00a0c93ec93b")); - - // Test case-insensitive match (uppercase) - assert!(partition.parttype_matches("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")); - - // Test case-insensitive match (mixed case) - assert!(partition.parttype_matches("C12a7328-F81f-11d2-Ba4b-00a0C93ec93b")); - - // Test non-match - assert!(!partition.parttype_matches("0FC63DAF-8483-4772-8E79-3D69D8477DE4")); - } - - #[test] - fn test_find_partition_of_type() -> Result<()> { - let fixture = indoc::indoc! { r#" - { - "partitiontable": { - "label": "gpt", - "id": "A67AA901-2C72-4818-B098-7F1CAC127279", - "device": "/dev/loop0", - "unit": "sectors", - "firstlba": 34, - "lastlba": 20971486, - "sectorsize": 512, - "partitions": [ - { - "node": "/dev/loop0p1", - "start": 2048, - "size": 8192, - "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "uuid": "58A4C5F0-BD12-424C-B563-195AC65A25DD", - "name": "EFI System" - },{ - "node": "/dev/loop0p2", - "start": 10240, - "size": 20961247, - "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "uuid": "F51ABB0D-DA16-4A21-83CB-37F4C805AAA0", - "name": "root" - } - ] - } - } - "# }; - let table: SfDiskOutput = serde_json::from_str(fixture).unwrap(); - - // Find ESP partition using lowercase UUID (should match uppercase in fixture) - let esp = table - .partitiontable - .find_partition_of_type("c12a7328-f81f-11d2-ba4b-00a0c93ec93b"); - assert!(esp.is_some()); - assert_eq!(esp.unwrap().node, "/dev/loop0p1"); - - // Find root partition using uppercase UUID (should match case-insensitively) - let root = table - .partitiontable - .find_partition_of_type("0fc63daf-8483-4772-8e79-3d69d8477de4"); - assert!(root.is_some()); - assert_eq!(root.unwrap().node, "/dev/loop0p2"); - - // Try to find non-existent partition type - let nonexistent = table - .partitiontable - .find_partition_of_type("00000000-0000-0000-0000-000000000000"); - assert!(nonexistent.is_none()); - - // Find esp partition on GPT - let esp = table.partitiontable.find_partition_of_esp()?.unwrap(); - assert_eq!(esp.node, "/dev/loop0p1"); - - Ok(()) - } - #[test] - fn test_find_partition_of_type_mbr() -> Result<()> { - let fixture = indoc::indoc! { r#" - { - "partitiontable": { - "label": "dos", - "id": "0xc1748067", - "device": "/dev/mmcblk0", - "unit": "sectors", - "sectorsize": 512, - "partitions": [ - { - "node": "/dev/mmcblk0p1", - "start": 2048, - "size": 1026048, - "type": "6", - "bootable": true - },{ - "node": "/dev/mmcblk0p2", - "start": 1028096, - "size": 2097152, - "type": "83" - },{ - "node": "/dev/mmcblk0p3", - "start": 3125248, - "size": 121610240, - "type": "ef" - } - ] - } - } - "# }; - let table: SfDiskOutput = serde_json::from_str(fixture).unwrap(); - - // Find ESP partition using bootalbe is true - assert_eq!(table.partitiontable.label, PartitionType::Dos); - let esp = table - .partitiontable - .find_partition_of_bootable() - .expect("bootable partition not found"); - assert_eq!(esp.node, "/dev/mmcblk0p1"); - - // Find esp partition on MBR - let esp1 = table.partitiontable.find_partition_of_esp()?.unwrap(); - assert_eq!(esp1.node, "/dev/mmcblk0p1"); - Ok(()) - } } diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index a0fdb057a..e9165bb72 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2441,26 +2441,29 @@ pub(crate) async fn install_to_filesystem( // Find the real underlying backing device for the root. This is currently just required // for GRUB (BIOS) and in the future zipl (I think). - let backing_device = { - let mut dev = inspect.source; + let device_info = { + let mut dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?; loop { - tracing::debug!("Finding parents for {dev}"); - let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter(); - let Some(parent) = parents.next() else { + tracing::debug!("Finding parents for {}", dev.path()); + let Some(parents) = dev.parents.take() else { break; }; - if let Some(next) = parents.next() { + let mut parents_iter = parents.into_iter(); + let Some(parent) = parents_iter.next() else { + break; + }; + if let Some(next) = parents_iter.next() { anyhow::bail!( - "Found multiple parent devices {parent} and {next}; not currently supported" + "Found multiple parent devices {} and {}; not currently supported", + parent.path(), + next.path() ); } - // Re-query to get this parent's own parents - dev = bootc_blockdev::list_dev(Utf8Path::new(&parent.path()))?; + dev = parent; } + tracing::debug!("Backing device: {}", dev.path()); dev }; - tracing::debug!("Backing device: {backing_device}"); - let device_info = bootc_blockdev::list_dev(Utf8Path::new(&backing_device))?; let rootarg = format!("root={}", root_info.mount_spec); // CLI takes precedence over config file. From e17fb6185b3c0ddd6f0c2e78143004df2859a6b3 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Thu, 5 Feb 2026 15:29:10 -0500 Subject: [PATCH 07/12] blockdev: Build complete device tree with parents and children Rewrite list_dev() to build a complete device tree where every node has both parents and children populated. This is done with two lsblk calls: 1. Inverse lsblk on target device to find the root (top of hierarchy) 2. Regular lsblk on root to get the full tree with all children Then we populate parent links by walking the tree from root down, and return the target device from the fully-populated tree. This fixes the install-to-filesystem test where the root device (e.g., /dev/loop0) needs its children (partitions) populated for bwrap to bind them during bootloader installation. Assisted-by: Claude Code (Opus 4.5) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 127 ++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 49 deletions(-) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index f447cfc7b..da8ecefcd 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -19,7 +19,7 @@ struct DevicesOutput { } #[allow(dead_code)] -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Device { pub name: String, pub serial: Option, @@ -142,75 +142,104 @@ pub fn list_dev_by_dir(dir: &Dir) -> Result { list_dev(&Utf8PathBuf::from(&fsinfo.source)) } +/// List a device and build a complete tree with both parents and children populated. +/// Uses two lsblk calls: one inverse to find the root, one regular on the root for the full tree. #[context("Listing device {dev}")] pub fn list_dev(dev: &Utf8Path) -> Result { - let mut devs: DevicesOutput = Command::new("lsblk") - .args(["-J", "-b", "-O"]) + // Call 1: Inverse lsblk to find the root device (top of the hierarchy) + let inverse_output: DevicesOutput = Command::new("lsblk") + .args(["-J", "-b", "-O", "--inverse"]) .arg(dev) .log_debug() .run_and_parse_json()?; - for dev in devs.blockdevices.iter_mut() { - dev.backfill_missing()?; - } - let mut device = devs + + let root_path = find_root_path(&inverse_output)?; + + // Call 2: Regular lsblk on root to get complete tree with all children + let tree_output: DevicesOutput = Command::new("lsblk") + .args(["-J", "-b", "-O"]) + .arg(&root_path) + .log_debug() + .run_and_parse_json()?; + + let mut root = tree_output .blockdevices .into_iter() .next() - .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))?; - - // Get parent devices via lsblk --inverse - let parents = list_parents(dev)?; - device.parents = if parents.is_empty() { - None - } else { - Some(parents) - }; + .ok_or_else(|| anyhow!("no device output from lsblk for {}", root_path))?; + + // Populate parent links throughout the tree + populate_parents(&mut root, None)?; + + // If target is the root, return it directly + if dev.as_str() == root.path() { + return Ok(root); + } - Ok(device) + // Otherwise, find and return the target device from the tree + find_device_in_tree(&root, dev) + .ok_or_else(|| anyhow!("device {} not found in tree rooted at {}", dev, root.path())) } -/// List parent devices of the given device using lsblk --inverse. -/// Returns an empty Vec if the device has no parents (e.g., a whole disk). -#[context("Listing parent devices of {dev}")] -fn list_parents(dev: &Utf8Path) -> Result> { - let mut output: DevicesOutput = Command::new("lsblk") - .args(["-J", "-b", "-O", "--inverse"]) - .arg(dev) - .log_debug() - .run_and_parse_json()?; +/// Find the root device path by traversing the inverse lsblk output. +/// In inverse mode, "children" are actually parents, so we follow them to the top. +fn find_root_path(inverse_output: &DevicesOutput) -> Result { + let device = inverse_output + .blockdevices + .first() + .ok_or_else(|| anyhow!("no device in inverse output"))?; - // The first device in inverse output is the target device itself, - // its children (in inverse mode) are the parent devices - let target = output.blockdevices.first_mut(); - - match target { - Some(target) => { - // In inverse mode, "children" are actually parents - let mut parents: Vec = target.children.take().unwrap_or_default(); - for parent in parents.iter_mut() { - parent.backfill_missing()?; - // Recursively convert children to parents for the full chain - convert_inverse_children_to_parents(parent)?; - } - Ok(parents) - } - None => Ok(Vec::new()), + Ok(Utf8PathBuf::from(find_root_device(device).path())) +} + +/// Recursively find the root device by following children (parents in inverse mode). +fn find_root_device(device: &Device) -> &Device { + match &device.children { + Some(children) if !children.is_empty() => find_root_device(&children[0]), + _ => device, } } -/// In lsblk --inverse output, "children" are actually parent devices. -/// This function recursively converts the children field to the parents field. -fn convert_inverse_children_to_parents(dev: &mut Device) -> Result<()> { - if let Some(mut children) = dev.children.take() { +/// Recursively populate parent links throughout the device tree. +/// Each device's `parents` field is set to contain its parent device. +fn populate_parents(device: &mut Device, parent: Option) -> Result<()> { + device.backfill_missing()?; + device.parents = parent.map(|p| vec![p]); + + // Create parent reference before borrowing children mutably + // Use a stripped version without children/parents to avoid deep cloning + let parent_ref = Device { + children: None, + parents: None, + ..device.clone() + }; + + if let Some(children) = device.children.as_mut() { for child in children.iter_mut() { - child.backfill_missing()?; - convert_inverse_children_to_parents(child)?; + populate_parents(child, Some(parent_ref.clone()))?; } - dev.parents = Some(children); } + Ok(()) } +/// Find a device in the tree by its path. +fn find_device_in_tree(root: &Device, target_path: &Utf8Path) -> Option { + if root.path() == target_path.as_str() { + return Some(root.clone()); + } + + if let Some(children) = &root.children { + for child in children { + if let Some(found) = find_device_in_tree(child, target_path) { + return Some(found); + } + } + } + + None +} + pub struct LoopbackDevice { pub dev: Option, // Handle to the cleanup helper process From 58a0826462434a0b98a0ab6dd95158fd92110a0a Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Tue, 10 Feb 2026 14:59:45 -0500 Subject: [PATCH 08/12] blockdev: Simplify list_dev and add lazy parent/children resolution Remove eager parent population from list_dev(), which previously made two lsblk calls (inverse + regular) and built a complete tree with parent links. The parents field stored stripped clones (no children/parents), so traversing grandparents from any intermediate node would yield None. Instead, simplify list_dev() to a single `lsblk -J -b -O ` call plus backfill_missing(). Parents are now resolved on demand via Device::list_parents(), which calls `lsblk --inverse` and returns the parent chain through nested children fields. Device::list_children() handles the case where a Device obtained from an inverse tree lacks children by querying lsblk on demand. This removes the helper functions populate_parents(), find_root_path(), find_root_device(), and find_device_in_tree(). The parents field is retained for now but no longer populated by list_dev(). Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 138 ++++++++++++-------------------- 1 file changed, 52 insertions(+), 86 deletions(-) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index da8ecefcd..54b15f40d 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -45,7 +45,7 @@ pub struct Device { /// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices. pub pttype: Option, - /// Parent devices (from lsblk --inverse). Not populated by JSON deserialization. + /// Parent devices. Deprecated: use [`Device::list_parents`] instead. #[serde(skip)] pub parents: Option>, } @@ -134,6 +134,49 @@ impl Device { } Ok(()) } + + /// Query parent devices via `lsblk --inverse`. + /// + /// Returns `Ok(None)` if this device is already a root device (no parents). + /// In the returned `Vec`, each device's `children` field contains + /// *its own* parents (grandparents, etc.), forming the full chain to the + /// root device(s). A device can have multiple parents (e.g. RAID, LVM). + pub fn list_parents(&self) -> Result>> { + let path = self.path(); + let output: DevicesOutput = Command::new("lsblk") + .args(["-J", "-b", "-O", "--inverse"]) + .arg(&path) + .log_debug() + .run_and_parse_json()?; + + let device = output + .blockdevices + .into_iter() + .next() + .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?; + + match device.children { + Some(mut children) if !children.is_empty() => { + for child in &mut children { + child.backfill_missing()?; + } + Ok(Some(children)) + } + _ => Ok(None), + } + } + + /// Return this device's children, querying lsblk if not already populated. + /// + /// Devices obtained from a parent (inverse) chain lack children; + /// this method resolves them on demand. + pub fn list_children(&mut self) -> Result<&[Device]> { + if self.children.is_none() { + let new = list_dev(Utf8Path::new(&self.path()))?; + self.children = new.children; + } + Ok(self.children.as_deref().unwrap_or(&[])) + } } /// List the device containing the filesystem mounted at the given directory. @@ -142,102 +185,25 @@ pub fn list_dev_by_dir(dir: &Dir) -> Result { list_dev(&Utf8PathBuf::from(&fsinfo.source)) } -/// List a device and build a complete tree with both parents and children populated. -/// Uses two lsblk calls: one inverse to find the root, one regular on the root for the full tree. +/// List a device via lsblk. +/// +/// Parents are not eagerly populated; use [`Device::list_parents`] to query them. #[context("Listing device {dev}")] pub fn list_dev(dev: &Utf8Path) -> Result { - // Call 1: Inverse lsblk to find the root device (top of the hierarchy) - let inverse_output: DevicesOutput = Command::new("lsblk") - .args(["-J", "-b", "-O", "--inverse"]) - .arg(dev) - .log_debug() - .run_and_parse_json()?; - - let root_path = find_root_path(&inverse_output)?; - - // Call 2: Regular lsblk on root to get complete tree with all children - let tree_output: DevicesOutput = Command::new("lsblk") + let output: DevicesOutput = Command::new("lsblk") .args(["-J", "-b", "-O"]) - .arg(&root_path) + .arg(dev) .log_debug() .run_and_parse_json()?; - let mut root = tree_output + let mut device = output .blockdevices .into_iter() .next() - .ok_or_else(|| anyhow!("no device output from lsblk for {}", root_path))?; - - // Populate parent links throughout the tree - populate_parents(&mut root, None)?; - - // If target is the root, return it directly - if dev.as_str() == root.path() { - return Ok(root); - } - - // Otherwise, find and return the target device from the tree - find_device_in_tree(&root, dev) - .ok_or_else(|| anyhow!("device {} not found in tree rooted at {}", dev, root.path())) -} - -/// Find the root device path by traversing the inverse lsblk output. -/// In inverse mode, "children" are actually parents, so we follow them to the top. -fn find_root_path(inverse_output: &DevicesOutput) -> Result { - let device = inverse_output - .blockdevices - .first() - .ok_or_else(|| anyhow!("no device in inverse output"))?; - - Ok(Utf8PathBuf::from(find_root_device(device).path())) -} + .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))?; -/// Recursively find the root device by following children (parents in inverse mode). -fn find_root_device(device: &Device) -> &Device { - match &device.children { - Some(children) if !children.is_empty() => find_root_device(&children[0]), - _ => device, - } -} - -/// Recursively populate parent links throughout the device tree. -/// Each device's `parents` field is set to contain its parent device. -fn populate_parents(device: &mut Device, parent: Option) -> Result<()> { device.backfill_missing()?; - device.parents = parent.map(|p| vec![p]); - - // Create parent reference before borrowing children mutably - // Use a stripped version without children/parents to avoid deep cloning - let parent_ref = Device { - children: None, - parents: None, - ..device.clone() - }; - - if let Some(children) = device.children.as_mut() { - for child in children.iter_mut() { - populate_parents(child, Some(parent_ref.clone()))?; - } - } - - Ok(()) -} - -/// Find a device in the tree by its path. -fn find_device_in_tree(root: &Device, target_path: &Utf8Path) -> Option { - if root.path() == target_path.as_str() { - return Some(root.clone()); - } - - if let Some(children) = &root.children { - for child in children { - if let Some(found) = find_device_in_tree(child, target_path) { - return Some(found); - } - } - } - - None + Ok(device) } pub struct LoopbackDevice { From dc0ed00bf4966ed438a194922970b1e49f9abcb8 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Tue, 10 Feb 2026 15:00:30 -0500 Subject: [PATCH 09/12] install: Use Device::list_parents for parent traversal Replace the loop over dev.parents.take() with the new Device::list_parents() API. The inverse lsblk tree is walked to the root, with multi-parent detection preserved. Once the root device is found, refresh it via lsblk to populate its actual children (partitions). Devices from the inverse tree only carry parent links, not real children, so without this the bwrap container would lack partition device bind mounts. With no remaining consumers of the parents field, remove it from the Device struct. Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 4 --- crates/lib/src/install.rs | 53 +++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index 54b15f40d..4863058a8 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -44,10 +44,6 @@ pub struct Device { pub path: Option, /// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices. pub pttype: Option, - - /// Parent devices. Deprecated: use [`Device::list_parents`] instead. - #[serde(skip)] - pub parents: Option>, } impl Device { diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index e9165bb72..21ff07bbc 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2442,27 +2442,42 @@ pub(crate) async fn install_to_filesystem( // Find the real underlying backing device for the root. This is currently just required // for GRUB (BIOS) and in the future zipl (I think). let device_info = { - let mut dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?; - loop { - tracing::debug!("Finding parents for {}", dev.path()); - let Some(parents) = dev.parents.take() else { - break; - }; - let mut parents_iter = parents.into_iter(); - let Some(parent) = parents_iter.next() else { - break; - }; - if let Some(next) = parents_iter.next() { - anyhow::bail!( - "Found multiple parent devices {} and {}; not currently supported", - parent.path(), - next.path() - ); + let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?; + // Walk the inverse tree to the root device. + // In inverse lsblk output, "children" are actually parents. + match dev.list_parents()? { + None => { + tracing::debug!("Backing device: {}", dev.path()); + dev + } + Some(parents) => { + let mut current = parents; + loop { + if current.len() > 1 { + anyhow::bail!( + "Found multiple parent devices {} and {}; not currently supported", + current[0].path(), + current[1].path() + ); + } + let mut parent = current.into_iter().next().unwrap(); + // In inverse output, children = grandparents + match parent.children.take() { + Some(grandparents) if !grandparents.is_empty() => { + current = grandparents; + } + _ => { + tracing::debug!("Backing device: {}", parent.path()); + // Re-query the root device to populate its actual + // children (partitions). The inverse tree only + // carries parent links, not real children. + parent.refresh()?; + break parent; + } + } + } } - dev = parent; } - tracing::debug!("Backing device: {}", dev.path()); - dev }; let rootarg = format!("root={}", root_info.mount_spec); From 62ea0786e2c37edc10fc4a460f2d7d52d8b80f4d Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Wed, 11 Feb 2026 07:05:05 -0500 Subject: [PATCH 10/12] blockdev: Backfill partition number from sysfs for older lsblk The PARTN column was added in util-linux 2.39, but CentOS 9 / RHEL 9 ship util-linux 2.37. When lsblk -O is used on these systems, the partn field is absent from JSON output, causing find_device_by_partno() to fail with "Missing partition for index N". Add backfill_partn() alongside the existing backfill_start() to read the partition number from /sys/dev/block/{maj:min}/partition when lsblk doesn't provide it. Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index 4863058a8..64f955d05 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -120,10 +120,32 @@ impl Device { Ok(()) } + // The "partn" column was added in util-linux 2.39, which is newer than + // what CentOS 9 / RHEL 9 ship (2.37). + fn backfill_partn(&mut self) -> Result<()> { + let Some(majmin) = self.maj_min.as_deref() else { + return Ok(()); + }; + let sysfs_partn_path = format!("/sys/dev/block/{majmin}/partition"); + if Utf8Path::new(&sysfs_partn_path).try_exists()? { + let partn = std::fs::read_to_string(&sysfs_partn_path) + .with_context(|| format!("Reading {sysfs_partn_path}"))?; + tracing::debug!("backfilled partn to {partn}"); + self.partn = Some( + partn + .trim() + .parse() + .context("Parsing sysfs partition property")?, + ); + } + Ok(()) + } + /// Older versions of util-linux may be missing some properties. Backfill them if they're missing. pub fn backfill_missing(&mut self) -> Result<()> { // Add new properties to backfill here self.backfill_start()?; + self.backfill_partn()?; // And recurse to child devices for child in self.children.iter_mut().flatten() { child.backfill_missing()?; From 4b9202ba7d00997397e42c8461ff36b9b679b78c Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Wed, 11 Feb 2026 09:11:19 -0500 Subject: [PATCH 11/12] blockdev: Add Device::root_disk() to find the whole-disk ancestor Add a root_disk() method that walks the parent chain via lsblk --inverse to find the root (whole disk) device, then refreshes it to populate its actual children (partitions). This is needed by callers that start from a partition device (e.g. from list_dev_by_dir on /sysroot) and need to find sibling partitions such as the ESP. Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac --- crates/blockdev/src/blockdev.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index 64f955d05..8f3febb59 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -184,6 +184,37 @@ impl Device { } } + /// Walk the parent chain to find the root (whole disk) device. + /// + /// Returns the root device with its children (partitions) populated. + /// If this device is already a root device, returns a clone of `self`. + /// Fails if the device has multiple parents at any level. + pub fn root_disk(&self) -> Result { + let Some(parents) = self.list_parents()? else { + // Already a root device; re-query to ensure children are populated + return list_dev(Utf8Path::new(&self.path())); + }; + let mut current = parents; + loop { + anyhow::ensure!( + current.len() == 1, + "Device {} has multiple parents; cannot determine root disk", + self.path() + ); + let mut parent = current.into_iter().next().unwrap(); + match parent.children.take() { + Some(grandparents) if !grandparents.is_empty() => { + current = grandparents; + } + _ => { + // Found the root; re-query to populate its actual children + parent.refresh()?; + return Ok(parent); + } + } + } + } + /// Return this device's children, querying lsblk if not already populated. /// /// Devices obtained from a parent (inverse) chain lack children; From d399af26db33fc04757e2db8a10160032f9cfd15 Mon Sep 17 00:00:00 2001 From: ckyrouac Date: Wed, 11 Feb 2026 09:11:27 -0500 Subject: [PATCH 12/12] lib: Use Device::root_disk() for ESP and parent device lookups All callers that need the whole-disk device (e.g. to find the ESP partition) now use root_disk() to traverse from the filesystem partition to the root disk. This fixes list_dev_by_dir() consumers (store, bootloader, composefs boot) that were searching for ESP partitions on a partition device rather than the whole disk, and simplifies install.rs which had an open-coded parent walk. Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac --- crates/lib/src/bootc_composefs/boot.rs | 6 ++-- crates/lib/src/bootloader.rs | 2 +- crates/lib/src/install.rs | 39 ++------------------------ crates/lib/src/store/mod.rs | 2 +- 4 files changed, 7 insertions(+), 42 deletions(-) diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index 90050a7d1..0435288ba 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -67,9 +67,7 @@ use std::io::Write; use std::path::Path; use anyhow::{Context, Result, anyhow, bail}; -use bootc_blockdev::find_parent_devices; use bootc_kernel_cmdline::utf8::{Cmdline, Parameter, ParameterKey}; -use bootc_mount::inspect_filesystem_of_dir; use bootc_mount::tempmount::TempMount; use camino::{Utf8Path, Utf8PathBuf}; use cap_std_ext::{ @@ -535,7 +533,7 @@ pub(crate) fn setup_composefs_bls_boot( cmdline.add_or_modify(¶m); // Locate ESP partition device - let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?; + let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.root_disk()?; let esp_dev = root_dev.find_partition_of_esp()?; ( @@ -1067,7 +1065,7 @@ pub(crate) fn setup_composefs_uki_boot( let bootloader = host.require_composefs_booted()?.bootloader.clone(); // Locate ESP partition device - let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?; + let root_dev = bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.root_disk()?; let esp_dev = root_dev.find_partition_of_esp()?; (sysroot, esp_dev.path(), bootloader, false, None) diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index 921009af7..ed61e39d7 100644 --- a/crates/lib/src/bootloader.rs +++ b/crates/lib/src/bootloader.rs @@ -45,7 +45,7 @@ pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) root }; - let dev = bootc_blockdev::list_dev_by_dir(physical_root)?; + let dev = bootc_blockdev::list_dev_by_dir(physical_root)?.root_disk()?; if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) { let esp_path = esp_dev.path(); bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 21ff07bbc..889a25fca 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -2442,42 +2442,9 @@ pub(crate) async fn install_to_filesystem( // Find the real underlying backing device for the root. This is currently just required // for GRUB (BIOS) and in the future zipl (I think). let device_info = { - let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?; - // Walk the inverse tree to the root device. - // In inverse lsblk output, "children" are actually parents. - match dev.list_parents()? { - None => { - tracing::debug!("Backing device: {}", dev.path()); - dev - } - Some(parents) => { - let mut current = parents; - loop { - if current.len() > 1 { - anyhow::bail!( - "Found multiple parent devices {} and {}; not currently supported", - current[0].path(), - current[1].path() - ); - } - let mut parent = current.into_iter().next().unwrap(); - // In inverse output, children = grandparents - match parent.children.take() { - Some(grandparents) if !grandparents.is_empty() => { - current = grandparents; - } - _ => { - tracing::debug!("Backing device: {}", parent.path()); - // Re-query the root device to populate its actual - // children (partitions). The inverse tree only - // carries parent links, not real children. - parent.refresh()?; - break parent; - } - } - } - } - } + let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?.root_disk()?; + tracing::debug!("Backing device: {}", dev.path()); + dev }; let rootarg = format!("root={}", root_info.mount_spec); diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index 755c1b91a..73e839c75 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -173,7 +173,7 @@ impl BootedStorage { let composefs = Arc::new(composefs); //TODO: this assumes a single ESP on the root device - let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?; + let root_dev = bootc_blockdev::list_dev_by_dir(&physical_root)?.root_disk()?; let esp_dev = root_dev.find_partition_of_esp()?; let esp_mount = mount_esp(&esp_dev.path())?;