Skip to content

Commit ee9bef1

Browse files
committed
Support updating multiple EFIs on mirrored setups(RAID1)
The EFI System Partition is not mounted after booted, on systems configured with boot device mirroring, there are independent EFI partitions on each constituent disk, need to mount each disk and updates. But skip updating BIOS in this case. Xref to coreos#132
1 parent 15a964a commit ee9bef1

File tree

8 files changed

+394
-157
lines changed

8 files changed

+394
-157
lines changed

Diff for: src/bios.rs

+27-100
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,18 @@ 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");
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+
// skip bios update if has multiple devices
74+
if bios_boot_devices.len() > 1 {
75+
log::warn!("Find multiple devices which are currently not supported");
76+
return None;
13277
}
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-
}
78+
if !bios_boot_devices.is_empty() {
79+
return Some(bios_boot_devices);
14780
}
148-
Ok(None)
81+
None
14982
}
15083
}
15184

@@ -187,7 +120,7 @@ impl Component for Bios {
187120

188121
fn query_adopt(&self) -> Result<Option<Adoptable>> {
189122
#[cfg(target_arch = "x86_64")]
190-
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition()?.is_none() {
123+
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition().is_none() {
191124
log::debug!("Skip BIOS adopt");
192125
return Ok(None);
193126
}
@@ -199,9 +132,13 @@ impl Component for Bios {
199132
anyhow::bail!("Failed to find adoptable system")
200133
};
201134

202-
let device = self.get_device()?;
203-
let device = device.trim();
204-
self.run_grub_install("/", device)?;
135+
let target_root = "/";
136+
let devices = blockdev::get_backing_devices(&target_root)?
137+
.into_iter()
138+
.next();
139+
let dev = devices.unwrap();
140+
self.run_grub_install(target_root, &dev)?;
141+
log::debug!("Install grub2 on {dev}");
205142
Ok(InstalledContent {
206143
meta: update.clone(),
207144
filetree: None,
@@ -215,9 +152,14 @@ impl Component for Bios {
215152

216153
fn run_update(&self, sysroot: &openat::Dir, _: &InstalledContent) -> Result<InstalledContent> {
217154
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)?;
155+
let sysroot = sysroot.recover_path()?;
156+
let dest_root = sysroot.to_str().unwrap_or("/");
157+
let devices = blockdev::get_backing_devices(&dest_root)?
158+
.into_iter()
159+
.next();
160+
let dev = devices.unwrap();
161+
self.run_grub_install(dest_root, &dev)?;
162+
log::debug!("Install grub2 on {dev}");
221163

222164
let adopted_from = None;
223165
Ok(InstalledContent {
@@ -235,18 +177,3 @@ impl Component for Bios {
235177
Ok(None)
236178
}
237179
}
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

+206
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)