Skip to content

Commit 55b7798

Browse files
committed
blockdev: Unify ESP and parent device lookups via Device API
Add partition and device lookup methods to the Device struct (find_partition_of_type, find_partition_of_esp, find_device_by_partno, refresh, list_parents, root_disk) and list_dev_by_dir for resolving a mounted filesystem's backing device. Migrate all callers from the scattered PartitionTable/Partition-based helpers (esp_in, get_esp_partition, get_sysroot_parent_dev, get_esp_partition_node) to the unified Device API, and change RootSetup.device_info from PartitionTable to Device. Assisted-by: Claude Code (claude-opus-4-5-20251101) Signed-off-by: ckyrouac <ckyrouac@redhat.com>
1 parent 6ac06b7 commit 55b7798

9 files changed

Lines changed: 626 additions & 131 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/blockdev/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ version = "0.1.0"
99
[dependencies]
1010
# Internal crates
1111
bootc-utils = { package = "bootc-internal-utils", path = "../utils", version = "0.1.0" }
12+
bootc-mount = { path = "../mount" }
1213

1314
# Workspace dependencies
1415
anyhow = { workspace = true }
1516
camino = { workspace = true, features = ["serde1"] }
17+
cap-std-ext = { workspace = true, features = ["fs_utf8"] }
1618
fn-error-context = { workspace = true }
1719
libc = { workspace = true }
1820
regex = { workspace = true }

crates/blockdev/src/blockdev.rs

Lines changed: 231 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ use std::sync::OnceLock;
66

77
use anyhow::{Context, Result, anyhow};
88
use camino::{Utf8Path, Utf8PathBuf};
9+
use cap_std_ext::cap_std::fs::Dir;
910
use fn_error_context::context;
1011
use regex::Regex;
1112
use serde::Deserialize;
1213

1314
use bootc_utils::CommandRunExt;
1415

15-
/// EFI System Partition (ESP) on MBR
16+
/// MBR partition type IDs that indicate an EFI System Partition.
17+
/// 0x06 is FAT16 (used as ESP on some MBR systems), 0xEF is the
18+
/// explicit EFI System Partition type.
1619
/// Refer to <https://en.wikipedia.org/wiki/Partition_type>
1720
pub const ESP_ID_MBR: &[u8] = &[0x06, 0xEF];
1821

@@ -25,7 +28,7 @@ struct DevicesOutput {
2528
}
2629

2730
#[allow(dead_code)]
28-
#[derive(Debug, Deserialize)]
31+
#[derive(Debug, Clone, Deserialize)]
2932
pub struct Device {
3033
pub name: String,
3134
pub serial: Option<String>,
@@ -48,6 +51,8 @@ pub struct Device {
4851
pub fstype: Option<String>,
4952
pub uuid: Option<String>,
5053
pub path: Option<String>,
54+
/// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices.
55+
pub pttype: Option<String>,
5156
}
5257

5358
impl Device {
@@ -62,6 +67,67 @@ impl Device {
6267
self.children.as_ref().is_some_and(|v| !v.is_empty())
6368
}
6469

70+
/// Find a child partition by partition type (case-insensitive).
71+
pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
72+
self.children.as_ref()?.iter().find(|child| {
73+
child
74+
.parttype
75+
.as_ref()
76+
.is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
77+
})
78+
}
79+
80+
/// Find the EFI System Partition (ESP) among children.
81+
///
82+
/// For GPT disks, this matches by the ESP partition type GUID.
83+
/// For MBR (dos) disks, this matches by the MBR partition type IDs (0x06 or 0xEF).
84+
pub fn find_partition_of_esp(&self) -> Result<&Device> {
85+
let children = self
86+
.children
87+
.as_ref()
88+
.ok_or_else(|| anyhow!("Device has no children"))?;
89+
match self.pttype.as_deref() {
90+
Some("dos") => children
91+
.iter()
92+
.find(|child| {
93+
child
94+
.parttype
95+
.as_ref()
96+
.and_then(|pt| {
97+
let pt = pt.strip_prefix("0x").unwrap_or(pt);
98+
u8::from_str_radix(pt, 16).ok()
99+
})
100+
.is_some_and(|pt| ESP_ID_MBR.contains(&pt))
101+
})
102+
.ok_or_else(|| anyhow!("ESP not found in MBR partition table")),
103+
// When pttype is None (e.g. older lsblk or partition devices), default
104+
// to GPT UUID matching which will simply not match MBR hex types.
105+
Some("gpt") | None => self
106+
.find_partition_of_type(ESP)
107+
.ok_or_else(|| anyhow!("ESP not found in GPT partition table")),
108+
Some(other) => Err(anyhow!("Unsupported partition table type: {other}")),
109+
}
110+
}
111+
112+
/// Find a child partition by partition number (1-indexed).
113+
pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
114+
self.children
115+
.as_ref()
116+
.ok_or_else(|| anyhow!("Device has no children"))?
117+
.iter()
118+
.find(|child| child.partn == Some(partno))
119+
.ok_or_else(|| anyhow!("Missing partition for index {partno}"))
120+
}
121+
122+
/// Re-query this device's information from lsblk, updating all fields.
123+
/// This is useful after partitioning when the device's children have changed.
124+
pub fn refresh(&mut self) -> Result<()> {
125+
let path = self.path();
126+
let new_device = list_dev(Utf8Path::new(&path))?;
127+
*self = new_device;
128+
Ok(())
129+
}
130+
65131
/// Read a sysfs property for this device and parse it as the target type.
66132
fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
67133
where
@@ -103,6 +169,67 @@ impl Device {
103169
}
104170
Ok(())
105171
}
172+
173+
/// Query parent devices via `lsblk --inverse`.
174+
///
175+
/// Returns `Ok(None)` if this device is already a root device (no parents).
176+
/// In the returned `Vec<Device>`, each device's `children` field contains
177+
/// *its own* parents (grandparents, etc.), forming the full chain to the
178+
/// root device(s). A device can have multiple parents (e.g. RAID, LVM).
179+
pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
180+
let path = self.path();
181+
let output: DevicesOutput = Command::new("lsblk")
182+
.args(["-J", "-b", "-O", "--inverse"])
183+
.arg(&path)
184+
.log_debug()
185+
.run_and_parse_json()?;
186+
187+
let device = output
188+
.blockdevices
189+
.into_iter()
190+
.next()
191+
.ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
192+
193+
match device.children {
194+
Some(mut children) if !children.is_empty() => {
195+
for child in &mut children {
196+
child.backfill_missing()?;
197+
}
198+
Ok(Some(children))
199+
}
200+
_ => Ok(None),
201+
}
202+
}
203+
204+
/// Walk the parent chain to find the root (whole disk) device.
205+
///
206+
/// Returns the root device with its children (partitions) populated.
207+
/// If this device is already a root device, returns a clone of `self`.
208+
/// Fails if the device has multiple parents at any level.
209+
pub fn root_disk(&self) -> Result<Device> {
210+
let Some(parents) = self.list_parents()? else {
211+
// Already a root device; re-query to ensure children are populated
212+
return list_dev(Utf8Path::new(&self.path()));
213+
};
214+
let mut current = parents;
215+
loop {
216+
anyhow::ensure!(
217+
current.len() == 1,
218+
"Device {} has multiple parents; cannot determine root disk",
219+
self.path()
220+
);
221+
let mut parent = current.into_iter().next().unwrap();
222+
match parent.children.take() {
223+
Some(grandparents) if !grandparents.is_empty() => {
224+
current = grandparents;
225+
}
226+
_ => {
227+
// Found the root; re-query to populate its actual children
228+
return list_dev(Utf8Path::new(&parent.path()));
229+
}
230+
}
231+
}
232+
}
106233
}
107234

108235
#[context("Listing device {dev}")]
@@ -121,6 +248,12 @@ pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
121248
.ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
122249
}
123250

251+
/// List the device containing the filesystem mounted at the given directory.
252+
pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
253+
let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
254+
list_dev(&Utf8PathBuf::from(&fsinfo.source))
255+
}
256+
124257
#[derive(Debug, Deserialize)]
125258
struct SfDiskOutput {
126259
partitiontable: PartitionTable,
@@ -697,4 +830,100 @@ mod test {
697830
assert_eq!(esp1.node, "/dev/mmcblk0p1");
698831
Ok(())
699832
}
833+
834+
#[test]
835+
fn test_parse_lsblk_mbr() {
836+
let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
837+
let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
838+
let dev = devs.blockdevices.into_iter().next().unwrap();
839+
// The parent device has no partition number and is MBR
840+
assert_eq!(dev.partn, None);
841+
assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
842+
let children = dev.children.as_deref().unwrap();
843+
assert_eq!(children.len(), 3);
844+
// First partition: FAT16 boot partition (MBR type 0x06, an ESP type)
845+
let first_child = &children[0];
846+
assert_eq!(first_child.partn, Some(1));
847+
assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
848+
assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
849+
assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
850+
// MBR partitions have no partlabel
851+
assert!(first_child.partlabel.is_none());
852+
// Second partition: Linux root (MBR type 0x83)
853+
let second_child = &children[1];
854+
assert_eq!(second_child.partn, Some(2));
855+
assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
856+
assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
857+
// Third partition: EFI System Partition (MBR type 0xef)
858+
let third_child = &children[2];
859+
assert_eq!(third_child.partn, Some(3));
860+
assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
861+
assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
862+
// Verify find_device_by_partno works on MBR
863+
let part1 = dev.find_device_by_partno(1).unwrap();
864+
assert_eq!(part1.partn, Some(1));
865+
// find_partition_of_esp returns the first matching ESP type (0x06 on partition 1)
866+
let esp = dev.find_partition_of_esp().unwrap();
867+
assert_eq!(esp.partn, Some(1));
868+
}
869+
870+
/// Helper to construct a minimal MBR disk Device with given child partition types.
871+
fn make_mbr_disk(parttypes: &[&str]) -> Device {
872+
Device {
873+
name: "vda".into(),
874+
serial: None,
875+
model: None,
876+
partlabel: None,
877+
parttype: None,
878+
partuuid: None,
879+
partn: None,
880+
size: 10737418240,
881+
maj_min: None,
882+
start: None,
883+
label: None,
884+
fstype: None,
885+
uuid: None,
886+
path: Some("/dev/vda".into()),
887+
pttype: Some("dos".into()),
888+
children: Some(
889+
parttypes
890+
.iter()
891+
.enumerate()
892+
.map(|(i, pt)| Device {
893+
name: format!("vda{}", i + 1),
894+
serial: None,
895+
model: None,
896+
partlabel: None,
897+
parttype: Some(pt.to_string()),
898+
partuuid: None,
899+
partn: Some(i as u32 + 1),
900+
size: 1048576,
901+
maj_min: None,
902+
start: Some(2048),
903+
label: None,
904+
fstype: None,
905+
uuid: None,
906+
path: None,
907+
pttype: Some("dos".into()),
908+
children: None,
909+
})
910+
.collect(),
911+
),
912+
}
913+
}
914+
915+
#[test]
916+
fn test_mbr_esp_detection() {
917+
// 0x06 (FAT16) is recognized as ESP
918+
let dev = make_mbr_disk(&["0x06"]);
919+
assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
920+
921+
// 0xef (EFI System Partition) is recognized as ESP
922+
let dev = make_mbr_disk(&["0x83", "0xef"]);
923+
assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
924+
925+
// No ESP types present: 0x83 (Linux) and 0x82 (swap)
926+
let dev = make_mbr_disk(&["0x83", "0x82"]);
927+
assert!(dev.find_partition_of_esp().is_err());
928+
}
700929
}

0 commit comments

Comments
 (0)