Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 112 additions & 18 deletions crates/blockdev/src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Device>,
Expand Down Expand Up @@ -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<bool> {
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/<name>/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<String> {
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<Option<Vec<Device>>> {
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<Option<Vec<Device>>> {
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| {
Expand Down Expand Up @@ -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<Device> {
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<Device> {
/// 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<Vec<Device>> {
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)
}
}

Expand Down Expand Up @@ -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]
Expand Down
11 changes: 8 additions & 3 deletions crates/lib/src/bootc_composefs/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,8 @@ pub(crate) fn setup_composefs_bls_boot(
cmdline.add_or_modify(&param);

// 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()?;

(
Expand Down Expand Up @@ -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()?;

(
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion crates/lib/src/bootloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))?;
Expand Down
6 changes: 4 additions & 2 deletions crates/lib/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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
};
Expand Down
3 changes: 2 additions & 1 deletion crates/lib/src/store/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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())?;

Expand Down