Skip to content

Commit 4a620a2

Browse files
committed
Add blockdev.rs to get block device
1 parent 4eb09b1 commit 4a620a2

File tree

1 file changed

+206
-0
lines changed

1 file changed

+206
-0
lines changed

src/blockdev.rs

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

0 commit comments

Comments
 (0)