diff --git a/Cargo.lock b/Cargo.lock index fdcd53be6..25a3c1675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,11 +175,12 @@ version = "0.1.0" dependencies = [ "anyhow", "bootc-internal-utils", + "bootc-mount", "camino", + "cap-std-ext", "fn-error-context", "indoc", "libc", - "regex", "rustix", "serde", "serde_json", diff --git a/crates/blockdev/Cargo.toml b/crates/blockdev/Cargo.toml index ff3cfde2c..0f236e5cb 100644 --- a/crates/blockdev/Cargo.toml +++ b/crates/blockdev/Cargo.toml @@ -9,13 +9,14 @@ 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 } 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 a12527a94..8f3febb59 100644 --- a/crates/blockdev/src/blockdev.rs +++ b/crates/blockdev/src/blockdev.rs @@ -1,21 +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"; @@ -25,7 +19,7 @@ struct DevicesOutput { } #[allow(dead_code)] -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct Device { pub name: String, pub serial: Option, @@ -33,6 +27,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,20 +42,62 @@ 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 { - #[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()) } + /// 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<()> { @@ -82,148 +120,139 @@ 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()?; } Ok(()) } -} - -#[context("Listing device {dev}")] -pub fn list_dev(dev: &Utf8Path) -> Result { - let mut devs: DevicesOutput = Command::new("lsblk") - .args(["-J", "-b", "-O"]) - .arg(dev) - .log_debug() - .run_and_parse_json()?; - for dev in devs.blockdevices.iter_mut() { - dev.backfill_missing()?; - } - devs.blockdevices - .into_iter() - .next() - .ok_or_else(|| anyhow!("no device output from lsblk for {dev}")) -} - -#[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). + /// Query parent devices via `lsblk --inverse`. /// - /// 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")), + /// 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), } } -} -impl Partition { - #[allow(dead_code)] - pub fn path(&self) -> &Utf8Path { - self.node.as_str().into() + /// 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); + } + } + } } - /// Check if this partition's type matches the given UUID (case-insensitive). + /// Return this device's children, querying lsblk if not already populated. /// - /// 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) + /// 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(&[])) } +} - /// Check this partition's bootable property. - pub fn is_bootable(&self) -> bool { - self.bootable.unwrap_or(false) - } +/// 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 partitions of {dev}")] -pub fn partitions_of(dev: &Utf8Path) -> Result { - let o: SfDiskOutput = Command::new("sfdisk") - .args(["-J", dev.as_str()]) +/// 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 { + let output: DevicesOutput = Command::new("lsblk") + .args(["-J", "-b", "-O"]) + .arg(dev) + .log_debug() .run_and_parse_json()?; - Ok(o.partitiontable) + + let mut device = output + .blockdevices + .into_iter() + .next() + .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))?; + + device.backfill_missing()?; + Ok(device) } pub struct LoopbackDevice { @@ -404,52 +433,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 = [ @@ -501,9 +484,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" @@ -512,178 +498,12 @@ mod test { first_child.partuuid.as_deref().unwrap(), "3979e399-262f-4666-aabc-7ab5d3add2f0" ); - } - - #[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(()) + // 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)); } } diff --git a/crates/lib/src/bootc_composefs/boot.rs b/crates/lib/src/bootc_composefs/boot.rs index c4787a917..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::{ @@ -93,6 +91,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 +103,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 +209,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 +498,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 +510,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 +532,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)?.root_disk()?; + 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 +1049,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 +1062,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)?.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 1e1157ef8..ed61e39d7 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)?.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))?; + 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 a056d03b7..889a25fca 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, @@ -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 @@ -1872,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 } } @@ -2435,25 +2441,11 @@ 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; - loop { - tracing::debug!("Finding parents for {dev}"); - let mut parents = bootc_blockdev::find_parent_devices(&dev)?.into_iter(); - let Some(parent) = parents.next() else { - break; - }; - if let Some(next) = parents.next() { - anyhow::bail!( - "Found multiple parent devices {parent} and {next}; not currently supported" - ); - } - dev = parent; - } + let device_info = { + let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?.root_disk()?; + tracing::debug!("Backing device: {}", dev.path()); dev }; - tracing::debug!("Backing device: {backing_device}"); - let device_info = bootc_blockdev::partitions_of(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..73e839c75 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)?.root_disk()?; + 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")?,