diff --git a/crates/blockdev/src/blockdev.rs b/crates/blockdev/src/blockdev.rs index 6b570cfa8..f544aa0be 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -19,6 +19,9 @@ 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"; +/// BIOS boot partition type GUID for GPT +pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649"; + #[derive(Debug, Deserialize)] struct DevicesOutput { blockdevices: Vec, @@ -69,6 +72,79 @@ impl Device { self.children.as_ref().is_some_and(|v| !v.is_empty()) } + // Check if the device is mpath + pub fn is_mpath(&self) -> Result { + let dm_path = Utf8PathBuf::from_path_buf(std::fs::canonicalize(self.path())?) + .map_err(|_| anyhow::anyhow!("Non-UTF8 path"))?; + let dm_name = dm_path.file_name().unwrap_or(""); + let uuid_path = Utf8PathBuf::from(format!("/sys/class/block/{dm_name}/dm/uuid")); + + if uuid_path.exists() { + let uuid = std::fs::read_to_string(&uuid_path) + .with_context(|| format!("Failed to read {uuid_path}"))?; + if uuid.trim_start().starts_with("mpath-") { + return Ok(true); + } + } + Ok(false) + } + + /// Get the numeric partition index of the ESP (e.g. "1", "2"). + /// + /// We read `/sys/class/block//partition` rather than parsing device + /// names because naming conventions vary across disk types (sd, nvme, dm, etc.). + /// On multipath devices the sysfs `partition` attribute doesn't exist, so we + /// fall back to the `partn` field reported by lsblk. + pub fn get_esp_partition_number(&self) -> Result { + let esp_device = self.find_partition_of_esp()?; + let devname = &esp_device.name; + + let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition")); + if partition_path.exists() { + return std::fs::read_to_string(&partition_path) + .with_context(|| format!("Failed to read {partition_path}")); + } + + // On multipath the partition attribute is not existing + if self.is_mpath()? { + if let Some(partn) = esp_device.partn { + return Ok(partn.to_string()); + } + } + anyhow::bail!("Not supported for {devname}") + } + + /// Find BIOS boot partition among children. + pub fn find_partition_of_bios_boot(&self) -> Option<&Device> { + self.find_partition_of_type(BIOS_BOOT) + } + + /// Find all ESP partitions across all root devices backing this device. + /// Calls find_all_roots() to discover physical disks, then searches each for an ESP. + /// Returns None if no ESPs are found. + pub fn find_colocated_esps(&self) -> Result>> { + let esps: Vec<_> = self + .find_all_roots()? + .iter() + .flat_map(|root| root.find_partition_of_esp().ok()) + .cloned() + .collect(); + Ok((!esps.is_empty()).then_some(esps)) + } + + /// Find all BIOS boot partitions across all root devices backing this device. + /// Calls find_all_roots() to discover physical disks, then searches each for a BIOS boot partition. + /// Returns None if no BIOS boot partitions are found. + pub fn find_colocated_bios_boot(&self) -> Result>> { + let bios_boots: Vec<_> = self + .find_all_roots()? + .iter() + .filter_map(|root| root.find_partition_of_bios_boot()) + .cloned() + .collect(); + Ok((!bios_boots.is_empty()).then_some(bios_boots)) + } + /// Find a child partition by partition type (case-insensitive). pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> { self.children.as_ref()?.iter().find(|child| { @@ -203,34 +279,48 @@ impl Device { } } - /// Walk the parent chain to find the root (whole disk) device. + /// Walk the parent chain to find all root (whole disk) devices, + /// and fail if more than one root is found. + /// + /// This is a convenience wrapper around `find_all_roots` for callers + /// that expect exactly one backing device (e.g. non-RAID setups). + pub fn require_single_root(&self) -> Result { + let mut roots = self.find_all_roots()?; + match roots.len() { + 1 => Ok(roots.remove(0)), + n => anyhow::bail!( + "Expected a single root device for {}, but found {n}", + self.path() + ), + } + } + + /// Walk the parent chain to find all root (whole disk) devices. /// - /// 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 { + /// Returns all root devices with their children (partitions) populated. + /// This handles devices backed by multiple parents (e.g. RAID arrays) + /// by following all branches of the parent tree. + /// If this device is already a root device, returns a single-element list. + pub fn find_all_roots(&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())); + return Ok(vec![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() { + + let mut roots = Vec::new(); + let mut queue = parents; + while let Some(mut device) = queue.pop() { + match device.children.take() { Some(grandparents) if !grandparents.is_empty() => { - current = grandparents; + queue.extend(grandparents); } _ => { - // Found the root; re-query to populate its actual children - return list_dev(Utf8Path::new(&parent.path())); + // Found a root; re-query to populate its actual children + roots.push(list_dev(Utf8Path::new(&device.path()))?); } } } + Ok(roots) } } @@ -506,6 +596,10 @@ mod test { // Verify find_partition_of_esp works let esp = dev.find_partition_of_esp().unwrap(); assert_eq!(esp.partn, Some(2)); + // Verify find_partition_of_bios_boot works (vda1 is BIOS-BOOT) + let bios = dev.find_partition_of_bios_boot().unwrap(); + assert_eq!(bios.partn, Some(1)); + assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT); } #[test] diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index f64540c5b..5c6895bd4 100644 --- a/crates/lib/src/bootc_composefs/boot.rs +++ b/crates/lib/src/bootc_composefs/boot.rs @@ -545,7 +545,8 @@ 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)?.root_disk()?; + let root_dev = + bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.require_single_root()?; let esp_dev = root_dev.find_partition_of_esp()?; ( @@ -1083,7 +1084,8 @@ 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)?.root_disk()?; + let root_dev = + bootc_blockdev::list_dev_by_dir(&storage.physical_root)?.require_single_root()?; let esp_dev = root_dev.find_partition_of_esp()?; ( @@ -1255,7 +1257,10 @@ pub(crate) async fn setup_composefs_boot( if cfg!(target_arch = "s390x") { // TODO: Integrate s390x support into install_via_bootupd - crate::bootloader::install_via_zipl(&root_setup.device_info, boot_uuid)?; + crate::bootloader::install_via_zipl( + &root_setup.device_info.require_single_root()?, + boot_uuid, + )?; } else if postfetch.detected_bootloader == Bootloader::Grub { crate::bootloader::install_via_bootupd( &root_setup.device_info, diff --git a/crates/lib/src/bootloader.rs b/crates/lib/src/bootloader.rs index ed61e39d7..b621adeb5 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)?.root_disk()?; + let dev = bootc_blockdev::list_dev_by_dir(physical_root)?.require_single_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))?; diff --git a/crates/lib/src/install.rs b/crates/lib/src/install.rs index 6ab5a3cc0..92cf28144 100644 --- a/crates/lib/src/install.rs +++ b/crates/lib/src/install.rs @@ -1795,7 +1795,8 @@ async fn install_with_sysroot( if cfg!(target_arch = "s390x") { // TODO: Integrate s390x support into install_via_bootupd - crate::bootloader::install_via_zipl(&rootfs.device_info, boot_uuid)?; + // zipl only supports single device + crate::bootloader::install_via_zipl(&rootfs.device_info.require_single_root()?, boot_uuid)?; } else { match postfetch.detected_bootloader { Bootloader::Grub => { @@ -2514,7 +2515,8 @@ 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))?.root_disk()?; + let dev = + bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?.require_single_root()?; tracing::debug!("Backing device: {}", dev.path()); dev }; diff --git a/crates/lib/src/store/mod.rs b/crates/lib/src/store/mod.rs index a5f844256..21531cd7d 100644 --- a/crates/lib/src/store/mod.rs +++ b/crates/lib/src/store/mod.rs @@ -196,7 +196,8 @@ 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)?.root_disk()?; + let root_dev = + bootc_blockdev::list_dev_by_dir(&physical_root)?.require_single_root()?; let esp_dev = root_dev.find_partition_of_esp()?; let esp_mount = mount_esp(&esp_dev.path())?;