Skip to content

Commit 5fb2e96

Browse files
committed
Support updating multiple EFIs in mirrored setups(RAID1)
Xref to coreos#132
1 parent 15a964a commit 5fb2e96

File tree

5 files changed

+343
-159
lines changed

5 files changed

+343
-159
lines changed

Diff for: src/bios.rs

+21-101
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,19 @@ use std::io::prelude::*;
22
use std::path::Path;
33
use std::process::Command;
44

5+
use crate::blockdev;
56
use crate::component::*;
67
use crate::model::*;
78
use crate::packagesystem;
89
use anyhow::{bail, Result};
910

10-
use crate::util;
11-
use serde::{Deserialize, Serialize};
12-
1311
// grub2-install file path
1412
pub(crate) const GRUB_BIN: &str = "usr/sbin/grub2-install";
1513

16-
#[derive(Serialize, Deserialize, Debug)]
17-
struct BlockDevice {
18-
path: String,
19-
pttype: Option<String>,
20-
parttypename: Option<String>,
21-
}
22-
23-
#[derive(Serialize, Deserialize, Debug)]
24-
struct Devices {
25-
blockdevices: Vec<BlockDevice>,
26-
}
27-
2814
#[derive(Default)]
2915
pub(crate) struct Bios {}
3016

3117
impl Bios {
32-
// get target device for running update
33-
fn get_device(&self) -> Result<String> {
34-
let mut cmd: Command;
35-
#[cfg(target_arch = "x86_64")]
36-
{
37-
// find /boot partition
38-
cmd = Command::new("findmnt");
39-
cmd.arg("--noheadings")
40-
.arg("--nofsroot")
41-
.arg("--output")
42-
.arg("SOURCE")
43-
.arg("/boot");
44-
let partition = util::cmd_output(&mut cmd)?;
45-
46-
// lsblk to find parent device
47-
cmd = Command::new("lsblk");
48-
cmd.arg("--paths")
49-
.arg("--noheadings")
50-
.arg("--output")
51-
.arg("PKNAME")
52-
.arg(partition.trim());
53-
}
54-
55-
#[cfg(target_arch = "powerpc64")]
56-
{
57-
// get PowerPC-PReP-boot partition
58-
cmd = Command::new("realpath");
59-
cmd.arg("/dev/disk/by-partlabel/PowerPC-PReP-boot");
60-
}
61-
62-
let device = util::cmd_output(&mut cmd)?;
63-
Ok(device)
64-
}
65-
6618
// Return `true` if grub2-modules installed
6719
fn check_grub_modules(&self) -> Result<bool> {
6820
let usr_path = Path::new("/usr/lib/grub");
@@ -115,37 +67,13 @@ impl Bios {
11567
}
11668

11769
// check bios_boot partition on gpt type disk
118-
fn get_bios_boot_partition(&self) -> Result<Option<String>> {
119-
let target = self.get_device()?;
120-
// lsblk to list children with bios_boot
121-
let output = Command::new("lsblk")
122-
.args([
123-
"--json",
124-
"--output",
125-
"PATH,PTTYPE,PARTTYPENAME",
126-
target.trim(),
127-
])
128-
.output()?;
129-
if !output.status.success() {
130-
std::io::stderr().write_all(&output.stderr)?;
131-
bail!("Failed to run lsblk");
132-
}
133-
134-
let output = String::from_utf8(output.stdout)?;
135-
// Parse the JSON string into the `Devices` struct
136-
let Ok(devices) = serde_json::from_str::<Devices>(&output) else {
137-
bail!("Could not deserialize JSON output from lsblk");
138-
};
139-
140-
// Find the device with the parttypename "BIOS boot"
141-
for device in devices.blockdevices {
142-
if let Some(parttypename) = &device.parttypename {
143-
if parttypename == "BIOS boot" && device.pttype.as_deref() == Some("gpt") {
144-
return Ok(Some(device.path));
145-
}
146-
}
70+
fn get_bios_boot_partition(&self) -> Option<Vec<String>> {
71+
let bios_boot_devices =
72+
blockdev::find_colocated_bios_boot("/").expect("get bios_boot devices");
73+
if !bios_boot_devices.is_empty() {
74+
return Some(bios_boot_devices);
14775
}
148-
Ok(None)
76+
return None;
14977
}
15078
}
15179

@@ -187,7 +115,7 @@ impl Component for Bios {
187115

188116
fn query_adopt(&self) -> Result<Option<Adoptable>> {
189117
#[cfg(target_arch = "x86_64")]
190-
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition()?.is_none() {
118+
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition().is_none() {
191119
log::debug!("Skip BIOS adopt");
192120
return Ok(None);
193121
}
@@ -199,9 +127,12 @@ impl Component for Bios {
199127
anyhow::bail!("Failed to find adoptable system")
200128
};
201129

202-
let device = self.get_device()?;
203-
let device = device.trim();
204-
self.run_grub_install("/", device)?;
130+
let target_root = "/";
131+
let devices = blockdev::get_backing_devices(&target_root)?;
132+
for dev in devices.iter() {
133+
self.run_grub_install(target_root, dev)?;
134+
log::debug!("Install grub2 on {dev}");
135+
}
205136
Ok(InstalledContent {
206137
meta: update.clone(),
207138
filetree: None,
@@ -215,9 +146,13 @@ impl Component for Bios {
215146

216147
fn run_update(&self, sysroot: &openat::Dir, _: &InstalledContent) -> Result<InstalledContent> {
217148
let updatemeta = self.query_update(sysroot)?.expect("update available");
218-
let device = self.get_device()?;
219-
let device = device.trim();
220-
self.run_grub_install("/", device)?;
149+
let sysroot = sysroot.recover_path()?;
150+
let dest_root = sysroot.to_str().unwrap_or("/");
151+
let devices = blockdev::get_backing_devices(&dest_root)?;
152+
for dev in devices.iter() {
153+
self.run_grub_install(dest_root, dev)?;
154+
log::debug!("Install grub2 on {dev}");
155+
}
221156

222157
let adopted_from = None;
223158
Ok(InstalledContent {
@@ -235,18 +170,3 @@ impl Component for Bios {
235170
Ok(None)
236171
}
237172
}
238-
239-
#[cfg(test)]
240-
mod tests {
241-
use super::*;
242-
243-
#[test]
244-
fn test_deserialize_lsblk_output() {
245-
let data = include_str!("../tests/fixtures/example-lsblk-output.json");
246-
let devices: Devices = serde_json::from_str(&data).expect("JSON was not well-formatted");
247-
assert_eq!(devices.blockdevices.len(), 7);
248-
assert_eq!(devices.blockdevices[0].path, "/dev/sr0");
249-
assert!(devices.blockdevices[0].pttype.is_none());
250-
assert!(devices.blockdevices[0].parttypename.is_none());
251-
}
252-
}

Diff for: src/blockdev.rs

+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
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+
pub fn find_colocated_bios_boot<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
176+
// first, get the parent device
177+
let backing_devices = get_backing_devices(&target_root)
178+
.with_context(|| "looking for colocated bios_boot parts")?;
179+
180+
// now, look for all bios_boot parts on those devices
181+
let mut bios_boots = Vec::new();
182+
for parent_device in backing_devices {
183+
if let Some(bios) = get_bios_boot_partition(&parent_device)? {
184+
bios_boots.push(bios)
185+
}
186+
}
187+
log::debug!("Find bios_boot partitions: {bios_boots:?}");
188+
Ok(bios_boots)
189+
}
190+
191+
#[cfg(test)]
192+
mod tests {
193+
use super::*;
194+
195+
#[test]
196+
fn test_deserialize_lsblk_output() {
197+
let data = include_str!("../tests/fixtures/example-lsblk-output.json");
198+
let devices: BlockDevices =
199+
serde_json::from_str(&data).expect("JSON was not well-formatted");
200+
assert_eq!(devices.blockdevices.len(), 7);
201+
assert_eq!(devices.blockdevices[0].path, "/dev/sr0");
202+
assert!(devices.blockdevices[0].pttype.is_none());
203+
assert!(devices.blockdevices[0].parttypename.is_none());
204+
}
205+
}

0 commit comments

Comments
 (0)