Skip to content

Commit d4bdbe1

Browse files
committed
efi: support updating multiple EFIs in mirrored setups (RAID1)
The prep PR is #824 Fixes #132
1 parent ee0aabd commit d4bdbe1

File tree

6 files changed

+159
-61
lines changed

6 files changed

+159
-61
lines changed

src/bios.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,16 @@ impl Bios {
9797
}
9898

9999
// check bios_boot partition on gpt type disk
100+
#[cfg(target_arch = "x86_64")]
100101
fn get_bios_boot_partition(&self) -> Option<String> {
101102
match blockdev::get_single_device("/") {
102103
Ok(device) => {
103104
let bios_boot_part =
104105
blockdev::get_bios_boot_partition(&device).expect("get bios_boot part");
105106
return bios_boot_part;
106107
}
107-
Err(e) => log::warn!("Get error: {}", e),
108+
Err(e) => log::warn!("Get single device: {}", e),
108109
}
109-
log::debug!("Not found any bios_boot partition");
110110
None
111111
}
112112
}
@@ -149,7 +149,7 @@ impl Component for Bios {
149149

150150
fn query_adopt(&self) -> Result<Option<Adoptable>> {
151151
#[cfg(target_arch = "x86_64")]
152-
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition().is_none() {
152+
if self.get_bios_boot_partition().is_none() {
153153
log::debug!("Skip BIOS adopt");
154154
return Ok(None);
155155
}

src/blockdev.rs

-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ pub fn get_single_device<P: AsRef<Path>>(target_root: P) -> Result<String> {
3737

3838
/// Find esp partition on the same device
3939
/// using sfdisk to get partitiontable
40-
#[allow(dead_code)]
4140
pub fn get_esp_partition(device: &str) -> Result<Option<String>> {
4241
const ESP_TYPE_GUID: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B";
4342
let device_info: PartitionTable = bootc_blockdev::partitions_of(Utf8Path::new(device))?;
@@ -52,7 +51,6 @@ pub fn get_esp_partition(device: &str) -> Result<Option<String>> {
5251
}
5352

5453
/// Find all ESP partitions on the devices with mountpoint boot
55-
#[allow(dead_code)]
5654
pub fn find_colocated_esps<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
5755
// first, get the parent device
5856
let devices = get_devices(&target_root).with_context(|| "while looking for colocated ESPs")?;

src/efi.rs

+111-56
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use rustix::fd::BorrowedFd;
1919
use walkdir::WalkDir;
2020
use widestring::U16CString;
2121

22+
use crate::blockdev;
2223
use crate::filetree;
2324
use crate::model::*;
2425
use crate::ostreeutil;
@@ -57,28 +58,7 @@ pub(crate) struct Efi {
5758
}
5859

5960
impl Efi {
60-
fn esp_path(&self) -> Result<PathBuf> {
61-
self.ensure_mounted_esp(Path::new("/"))
62-
.map(|v| v.join("EFI"))
63-
}
64-
65-
fn open_esp_optional(&self) -> Result<Option<openat::Dir>> {
66-
if !is_efi_booted()? && self.get_esp_device().is_none() {
67-
log::debug!("Skip EFI");
68-
return Ok(None);
69-
}
70-
let sysroot = openat::Dir::open("/")?;
71-
let esp = sysroot.sub_dir_optional(&self.esp_path()?)?;
72-
Ok(esp)
73-
}
74-
75-
fn open_esp(&self) -> Result<openat::Dir> {
76-
self.ensure_mounted_esp(Path::new("/"))?;
77-
let sysroot = openat::Dir::open("/")?;
78-
let esp = sysroot.sub_dir(&self.esp_path()?)?;
79-
Ok(esp)
80-
}
81-
61+
// Get esp device via legacy
8262
fn get_esp_device(&self) -> Option<PathBuf> {
8363
let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL]
8464
.into_iter()
@@ -93,11 +73,26 @@ impl Efi {
9373
return esp_device;
9474
}
9575

96-
pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result<PathBuf> {
97-
let mut mountpoint = self.mountpoint.borrow_mut();
76+
// Get esp device list on all devices
77+
fn get_esp_devices(&self) -> Option<Vec<String>> {
78+
let mut esp_devices = vec![];
79+
if let Some(esp_device) = self.get_esp_device() {
80+
esp_devices.push(esp_device.to_string_lossy().into_owned());
81+
} else {
82+
esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices");
83+
};
84+
if !esp_devices.is_empty() {
85+
return Some(esp_devices);
86+
}
87+
return None;
88+
}
89+
90+
fn check_mounted_esp<P: AsRef<Path>>(&self, root: P) -> Result<Option<PathBuf>> {
91+
let mountpoint = self.mountpoint.borrow_mut();
9892
if let Some(mountpoint) = mountpoint.as_deref() {
99-
return Ok(mountpoint.to_owned());
93+
return Ok(Some(mountpoint.to_owned()));
10094
}
95+
let root = root.as_ref();
10196
for &mnt in ESP_MOUNTS {
10297
let mnt = root.join(mnt);
10398
if !mnt.exists() {
@@ -109,13 +104,23 @@ impl Efi {
109104
continue;
110105
}
111106
util::ensure_writable_mount(&mnt)?;
112-
log::debug!("Reusing existing {mnt:?}");
113-
return Ok(mnt);
107+
log::debug!("Reusing existing mount point {mnt:?}");
108+
return Ok(Some(mnt));
109+
}
110+
Ok(None)
111+
}
112+
113+
pub(crate) fn ensure_mounted_esp<P: AsRef<Path>>(
114+
&self,
115+
root: P,
116+
esp_device: &str,
117+
) -> Result<PathBuf> {
118+
let mut mountpoint = self.mountpoint.borrow_mut();
119+
if let Some(mountpoint) = mountpoint.as_deref() {
120+
return Ok(mountpoint.to_owned());
114121
}
115122

116-
let esp_device = self
117-
.get_esp_device()
118-
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
123+
let root = root.as_ref();
119124
for &mnt in ESP_MOUNTS.iter() {
120125
let mnt = root.join(mnt);
121126
if !mnt.exists() {
@@ -137,6 +142,7 @@ impl Efi {
137142
if let Some(mount) = self.mountpoint.borrow_mut().take() {
138143
Command::new("umount")
139144
.arg(&mount)
145+
.arg("-l")
140146
.run()
141147
.with_context(|| format!("Failed to unmount {mount:?}"))?;
142148
log::trace!("Unmounted");
@@ -243,8 +249,7 @@ impl Component for Efi {
243249
}
244250

245251
fn query_adopt(&self) -> Result<Option<Adoptable>> {
246-
let esp = self.open_esp_optional()?;
247-
if esp.is_none() {
252+
if self.get_esp_devices().is_none() {
248253
log::trace!("No ESP detected");
249254
return Ok(None);
250255
};
@@ -267,16 +272,32 @@ impl Component for Efi {
267272
anyhow::bail!("Failed to find adoptable system")
268273
};
269274

270-
let esp = self.open_esp()?;
271-
validate_esp(&esp)?;
272275
let updated = sysroot
273276
.sub_dir(&component_updatedirname(self))
274277
.context("opening update dir")?;
275278
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
276-
// For adoption, we should only touch files that we know about.
277-
let diff = updatef.relative_diff_to(&esp)?;
278-
log::trace!("applying adoption diff: {}", &diff);
279-
filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?;
279+
let esp_devices = self
280+
.get_esp_devices()
281+
.expect("get esp devices before adopt");
282+
let sysroot = sysroot.recover_path()?;
283+
284+
for esp_dev in esp_devices {
285+
let dest_path = if let Some(dest_path) = self.check_mounted_esp(&sysroot)? {
286+
dest_path.join("EFI")
287+
} else {
288+
self.ensure_mounted_esp(&sysroot, &esp_dev)?.join("EFI")
289+
};
290+
291+
let esp = openat::Dir::open(&dest_path).context("opening EFI dir")?;
292+
validate_esp(&esp)?;
293+
294+
// For adoption, we should only touch files that we know about.
295+
let diff = updatef.relative_diff_to(&esp)?;
296+
log::trace!("applying adoption diff: {}", &diff);
297+
filetree::apply_diff(&updated, &esp, &diff, None)
298+
.context("applying filesystem changes")?;
299+
self.unmount().context("unmount after adopt")?;
300+
}
280301
Ok(InstalledContent {
281302
meta: updatemeta.clone(),
282303
filetree: Some(updatef),
@@ -298,9 +319,17 @@ impl Component for Efi {
298319
log::debug!("Found metadata {}", meta.version);
299320
let srcdir_name = component_updatedirname(self);
300321
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
301-
let destdir = &self.ensure_mounted_esp(Path::new(dest_root))?;
322+
let destdir = if let Some(destdir) = self.check_mounted_esp(dest_root)? {
323+
destdir
324+
} else {
325+
let esp_device = self
326+
.get_esp_device()
327+
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
328+
let esp_device = esp_device.to_str().unwrap();
329+
self.ensure_mounted_esp(dest_root, esp_device)?
330+
};
302331

303-
let destd = &openat::Dir::open(destdir)
332+
let destd = &openat::Dir::open(&destdir)
304333
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
305334
validate_esp(destd)?;
306335

@@ -339,12 +368,25 @@ impl Component for Efi {
339368
.context("opening update dir")?;
340369
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
341370
let diff = currentf.diff(&updatef)?;
342-
self.ensure_mounted_esp(Path::new("/"))?;
343-
let destdir = self.open_esp().context("opening EFI dir")?;
344-
validate_esp(&destdir)?;
345-
log::trace!("applying diff: {}", &diff);
346-
filetree::apply_diff(&updated, &destdir, &diff, None)
347-
.context("applying filesystem changes")?;
371+
let esp_devices = self
372+
.get_esp_devices()
373+
.context("get esp devices when running update")?;
374+
let sysroot = sysroot.recover_path()?;
375+
376+
for esp in esp_devices {
377+
let dest_path = if let Some(dest_path) = self.check_mounted_esp(&sysroot)? {
378+
dest_path.join("EFI")
379+
} else {
380+
self.ensure_mounted_esp(&sysroot, &esp)?.join("EFI")
381+
};
382+
383+
let destdir = openat::Dir::open(&dest_path).context("opening EFI dir")?;
384+
validate_esp(&destdir)?;
385+
log::trace!("applying diff: {}", &diff);
386+
filetree::apply_diff(&updated, &destdir, &diff, None)
387+
.context("applying filesystem changes")?;
388+
self.unmount().context("unmount after update")?;
389+
}
348390
let adopted_from = None;
349391
Ok(InstalledContent {
350392
meta: updatemeta,
@@ -392,24 +434,37 @@ impl Component for Efi {
392434
}
393435

394436
fn validate(&self, current: &InstalledContent) -> Result<ValidationResult> {
395-
if !is_efi_booted()? && self.get_esp_device().is_none() {
437+
let esp_devices = self.get_esp_devices();
438+
if !is_efi_booted()? && esp_devices.is_none() {
396439
return Ok(ValidationResult::Skip);
397440
}
398441
let currentf = current
399442
.filetree
400443
.as_ref()
401444
.ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;
402-
self.ensure_mounted_esp(Path::new("/"))?;
403-
let efidir = self.open_esp()?;
404-
let diff = currentf.relative_diff_to(&efidir)?;
445+
405446
let mut errs = Vec::new();
406-
for f in diff.changes.iter() {
407-
errs.push(format!("Changed: {}", f));
408-
}
409-
for f in diff.removals.iter() {
410-
errs.push(format!("Removed: {}", f));
447+
let esps = esp_devices.ok_or_else(|| anyhow::anyhow!("No esp device found!"))?;
448+
let dest_root = Path::new("/");
449+
for esp_dev in esps.iter() {
450+
let dest_path = if let Some(dest_path) = self.check_mounted_esp(dest_root)? {
451+
dest_path.join("EFI")
452+
} else {
453+
self.ensure_mounted_esp(dest_root, &esp_dev)?.join("EFI")
454+
};
455+
456+
let efidir = openat::Dir::open(dest_path.as_path())?;
457+
let diff = currentf.relative_diff_to(&efidir)?;
458+
459+
for f in diff.changes.iter() {
460+
errs.push(format!("Changed: {}", f));
461+
}
462+
for f in diff.removals.iter() {
463+
errs.push(format!("Removed: {}", f));
464+
}
465+
assert_eq!(diff.additions.len(), 0);
466+
self.unmount().context("unmount after validate")?;
411467
}
412-
assert_eq!(diff.additions.len(), 0);
413468
if !errs.is_empty() {
414469
Ok(ValidationResult::Errors(errs))
415470
} else {

tests/kola/raid1/config.bu

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
variant: fcos
2+
version: 1.5.0
3+
boot_device:
4+
mirror:
5+
devices:
6+
- /dev/vda
7+
- /dev/vdb

tests/kola/raid1/data/libtest.sh

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../data/libtest.sh

tests/kola/raid1/test.sh

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/bin/bash
2+
## kola:
3+
## # additionalDisks is only supported on qemu.
4+
## platforms: qemu
5+
## # Root reprovisioning requires at least 4GiB of memory.
6+
## minMemory: 4096
7+
## # Linear RAID is setup on these disks.
8+
## additionalDisks: ["10G"]
9+
## # This test includes a lot of disk I/O and needs a higher
10+
## # timeout value than the default.
11+
## timeoutMin: 15
12+
## description: Verify updating multiple EFIs with RAID 1 works.
13+
14+
set -xeuo pipefail
15+
16+
# shellcheck disable=SC1091
17+
. "$KOLA_EXT_DATA/libtest.sh"
18+
19+
srcdev=$(findmnt -nvr /sysroot -o SOURCE)
20+
[[ ${srcdev} == "/dev/md126" ]]
21+
22+
blktype=$(lsblk -o TYPE "${srcdev}" --noheadings)
23+
[[ ${blktype} == "raid1" ]]
24+
25+
fstype=$(findmnt -nvr /sysroot -o FSTYPE)
26+
[[ ${fstype} == "xfs" ]]
27+
ok "source is XFS on RAID1 device"
28+
29+
30+
mount -o remount,rw /boot
31+
32+
rm -f -v /boot/bootupd-state.json
33+
34+
bootupctl adopt-and-update | grep "Adopted and updated: EFI"
35+
36+
bootupctl status | grep "Component EFI"
37+
ok "bootupctl adopt-and-update supports multiple EFIs on RAID1"

0 commit comments

Comments
 (0)