Skip to content

Commit d3e146c

Browse files
committed
Add blockdev.rs to parse block device
1 parent e296f82 commit d3e146c

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

src/blockdev.rs

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
use std::collections::HashMap;
2+
use std::path::Path;
3+
use std::process::Command;
4+
use std::sync::OnceLock;
5+
6+
use crate::util;
7+
use anyhow::{bail, Context, Result};
8+
use fn_error_context::context;
9+
use regex::Regex;
10+
use serde::{Deserialize, Serialize};
11+
12+
#[derive(Serialize, Deserialize, Debug)]
13+
struct BlockDevices {
14+
blockdevices: Vec<Device>,
15+
}
16+
17+
#[derive(Serialize, Deserialize, Debug)]
18+
struct Device {
19+
path: String,
20+
pttype: Option<String>,
21+
parttype: Option<String>,
22+
parttypename: Option<String>,
23+
}
24+
25+
impl Device {
26+
#[allow(dead_code)]
27+
pub(crate) fn is_esp_part(&self) -> bool {
28+
const ESP_TYPE_GUID: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
29+
if let Some(parttype) = &self.parttype {
30+
if parttype.to_lowercase() == ESP_TYPE_GUID {
31+
return true;
32+
}
33+
}
34+
false
35+
}
36+
37+
pub(crate) fn is_bios_boot_part(&self) -> bool {
38+
const BIOS_BOOT_TYPE_GUID: &str = "21686148-6449-6e6f-744e-656564454649";
39+
if let Some(parttype) = &self.parttype {
40+
if parttype.to_lowercase() == BIOS_BOOT_TYPE_GUID
41+
&& self.pttype.as_deref() == Some("gpt")
42+
{
43+
return true;
44+
}
45+
}
46+
false
47+
}
48+
}
49+
50+
/// Parse key-value pairs from lsblk --pairs.
51+
/// Newer versions of lsblk support JSON but the one in CentOS 7 doesn't.
52+
fn split_lsblk_line(line: &str) -> HashMap<String, String> {
53+
static REGEX: OnceLock<Regex> = OnceLock::new();
54+
let regex = REGEX.get_or_init(|| Regex::new(r#"([A-Z-_]+)="([^"]+)""#).unwrap());
55+
let mut fields: HashMap<String, String> = HashMap::new();
56+
for cap in regex.captures_iter(line) {
57+
fields.insert(cap[1].to_string(), cap[2].to_string());
58+
}
59+
fields
60+
}
61+
62+
/// This is a bit fuzzy, but... this function will return every block device in the parent
63+
/// hierarchy of `device` capable of containing other partitions. So e.g. parent devices of type
64+
/// "part" doesn't match, but "disk" and "mpath" does.
65+
pub(crate) fn find_parent_devices(device: &str) -> Result<Vec<String>> {
66+
let mut cmd = Command::new("lsblk");
67+
// Older lsblk, e.g. in CentOS 7.6, doesn't support PATH, but --paths option
68+
cmd.arg("--pairs")
69+
.arg("--paths")
70+
.arg("--inverse")
71+
.arg("--output")
72+
.arg("NAME,TYPE")
73+
.arg(device);
74+
let output = util::cmd_output(&mut cmd)?;
75+
let mut parents = Vec::new();
76+
// skip first line, which is the device itself
77+
for line in output.lines().skip(1) {
78+
let dev = split_lsblk_line(line);
79+
let name = dev
80+
.get("NAME")
81+
.with_context(|| format!("device in hierarchy of {device} missing NAME"))?;
82+
let kind = dev
83+
.get("TYPE")
84+
.with_context(|| format!("device in hierarchy of {device} missing TYPE"))?;
85+
if kind == "disk" {
86+
parents.push(name.clone());
87+
} else if kind == "mpath" {
88+
parents.push(name.clone());
89+
// we don't need to know what disks back the multipath
90+
break;
91+
}
92+
}
93+
if parents.is_empty() {
94+
bail!("no parent devices found for {}", device);
95+
}
96+
Ok(parents)
97+
}
98+
99+
#[context("get backing devices from mountpoint boot")]
100+
pub fn get_backing_devices<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
101+
let target_root = target_root.as_ref();
102+
let bootdir = target_root.join("boot");
103+
if !bootdir.exists() {
104+
bail!("{} does not exist", bootdir.display());
105+
}
106+
let bootdir = openat::Dir::open(&bootdir)?;
107+
let fsinfo = crate::filesystem::inspect_filesystem(&bootdir, ".")?;
108+
// Find the real underlying backing device for the root.
109+
let backing_devices = find_parent_devices(&fsinfo.source)
110+
.with_context(|| format!("while looking for backing devices of {}", fsinfo.source))?;
111+
log::debug!("Find backing devices: {backing_devices:?}");
112+
Ok(backing_devices)
113+
}
114+
115+
#[context("Listing parttype for device {device}")]
116+
fn list_dev(device: &str) -> Result<BlockDevices> {
117+
let mut cmd = Command::new("lsblk");
118+
cmd.args([
119+
"--json",
120+
"--output",
121+
"PATH,PTTYPE,PARTTYPE,PARTTYPENAME",
122+
device,
123+
]);
124+
let output = util::cmd_output(&mut cmd)?;
125+
// Parse the JSON string into the `BlockDevices` struct
126+
let Ok(devs) = serde_json::from_str::<BlockDevices>(&output) else {
127+
bail!("Could not deserialize JSON output from lsblk");
128+
};
129+
Ok(devs)
130+
}
131+
132+
/// Find esp partition on the same device
133+
pub fn get_esp_partition(device: &str) -> Result<Option<String>> {
134+
let dev = list_dev(&device)?;
135+
// Find the ESP part on the disk
136+
for part in dev.blockdevices {
137+
if part.is_esp_part() {
138+
return Ok(Some(part.path));
139+
}
140+
}
141+
log::debug!("Not found any esp partition");
142+
Ok(None)
143+
}
144+
145+
/// Find all ESP partitions on the backing devices with mountpoint boot
146+
#[allow(dead_code)]
147+
pub fn find_colocated_esps<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
148+
// first, get the parent device
149+
let backing_devices =
150+
get_backing_devices(&target_root).with_context(|| "while looking for colocated ESPs")?;
151+
152+
// now, look for all ESPs on those devices
153+
let mut esps = Vec::new();
154+
for parent_device in backing_devices {
155+
if let Some(esp) = get_esp_partition(&parent_device)? {
156+
esps.push(esp)
157+
}
158+
}
159+
log::debug!("Find esp partitions: {esps:?}");
160+
Ok(esps)
161+
}
162+
163+
/// Find bios_boot partition on the same device
164+
pub fn get_bios_boot_partition(device: &str) -> Result<Option<String>> {
165+
let dev = list_dev(&device)?;
166+
// Find the BIOS BOOT part on the disk
167+
for part in dev.blockdevices {
168+
if part.is_bios_boot_part() {
169+
return Ok(Some(part.path));
170+
}
171+
}
172+
log::debug!("Not found any bios_boot partition");
173+
Ok(None)
174+
}
175+
176+
/// Find all bios_boot partitions on the backing devices with mountpoint boot
177+
#[allow(dead_code)]
178+
pub fn find_colocated_bios_boot<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
179+
// first, get the parent device
180+
let backing_devices = get_backing_devices(&target_root)
181+
.with_context(|| "looking for colocated bios_boot parts")?;
182+
183+
// now, look for all bios_boot parts on those devices
184+
let mut bios_boots = Vec::new();
185+
for parent_device in backing_devices {
186+
if let Some(bios) = get_bios_boot_partition(&parent_device)? {
187+
bios_boots.push(bios)
188+
}
189+
}
190+
log::debug!("Find bios_boot partitions: {bios_boots:?}");
191+
Ok(bios_boots)
192+
}
193+
194+
#[cfg(test)]
195+
mod tests {
196+
use super::*;
197+
198+
#[test]
199+
fn test_deserialize_lsblk_output() {
200+
let data = include_str!("../tests/fixtures/example-lsblk-output.json");
201+
let devices: BlockDevices =
202+
serde_json::from_str(&data).expect("JSON was not well-formatted");
203+
assert_eq!(devices.blockdevices.len(), 7);
204+
assert_eq!(devices.blockdevices[0].path, "/dev/sr0");
205+
assert!(devices.blockdevices[0].pttype.is_none());
206+
assert!(devices.blockdevices[0].parttypename.is_none());
207+
}
208+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Refs:
1818
mod backend;
1919
#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
2020
mod bios;
21+
mod blockdev;
2122
mod bootupd;
2223
mod cli;
2324
mod component;

0 commit comments

Comments
 (0)