Skip to content

Commit 624f424

Browse files
committed
efi.rs: support updating multiple EFIs in 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 #132
1 parent 848ce44 commit 624f424

File tree

5 files changed

+161
-58
lines changed

5 files changed

+161
-58
lines changed

src/efi.rs

Lines changed: 109 additions & 58 deletions
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,6 @@ 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-
8261
fn get_esp_device(&self) -> Option<PathBuf> {
8362
let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL]
8463
.into_iter()
@@ -93,11 +72,25 @@ impl Efi {
9372
return esp_device;
9473
}
9574

96-
pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result<PathBuf> {
97-
let mut mountpoint = self.mountpoint.borrow_mut();
75+
fn get_all_esp_devices(&self) -> Option<Vec<String>> {
76+
let mut esp_devices = vec![];
77+
if let Some(esp_device) = self.get_esp_device() {
78+
esp_devices.push(esp_device.to_string_lossy().into_owned());
79+
} else {
80+
esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices");
81+
};
82+
if !esp_devices.is_empty() {
83+
return Some(esp_devices);
84+
}
85+
return None;
86+
}
87+
88+
fn check_existing_esp<P: AsRef<Path>>(&self, root: P) -> Result<Option<PathBuf>> {
89+
let mountpoint = self.mountpoint.borrow_mut();
9890
if let Some(mountpoint) = mountpoint.as_deref() {
99-
return Ok(mountpoint.to_owned());
91+
return Ok(Some(mountpoint.to_owned()));
10092
}
93+
let root = root.as_ref();
10194
for &mnt in ESP_MOUNTS {
10295
let mnt = root.join(mnt);
10396
if !mnt.exists() {
@@ -109,13 +102,23 @@ impl Efi {
109102
continue;
110103
}
111104
util::ensure_writable_mount(&mnt)?;
112-
log::debug!("Reusing existing {mnt:?}");
113-
return Ok(mnt);
105+
log::debug!("Reusing existing mount point {mnt:?}");
106+
return Ok(Some(mnt));
114107
}
108+
Ok(None)
109+
}
115110

116-
let esp_device = self
117-
.get_esp_device()
118-
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
111+
pub(crate) fn ensure_mounted_esp<P: AsRef<Path>>(
112+
&self,
113+
root: P,
114+
esp_device: &str,
115+
) -> Result<PathBuf> {
116+
let mut mountpoint = self.mountpoint.borrow_mut();
117+
if let Some(mountpoint) = mountpoint.as_deref() {
118+
return Ok(mountpoint.to_owned());
119+
}
120+
121+
let root = root.as_ref();
119122
for &mnt in ESP_MOUNTS.iter() {
120123
let mnt = root.join(mnt);
121124
if !mnt.exists() {
@@ -134,10 +137,9 @@ impl Efi {
134137
}
135138
Ok(mountpoint.as_deref().unwrap().to_owned())
136139
}
137-
138140
fn unmount(&self) -> Result<()> {
139141
if let Some(mount) = self.mountpoint.borrow_mut().take() {
140-
let status = Command::new("umount").arg(&mount).status()?;
142+
let status = Command::new("umount").arg("-l").arg(&mount).status()?;
141143
if !status.success() {
142144
anyhow::bail!("Failed to unmount {mount:?}: {status:?}");
143145
}
@@ -245,8 +247,7 @@ impl Component for Efi {
245247
}
246248

247249
fn query_adopt(&self) -> Result<Option<Adoptable>> {
248-
let esp = self.open_esp_optional()?;
249-
if esp.is_none() {
250+
if self.get_all_esp_devices().is_none() {
250251
log::trace!("No ESP detected");
251252
return Ok(None);
252253
};
@@ -269,16 +270,32 @@ impl Component for Efi {
269270
anyhow::bail!("Failed to find adoptable system")
270271
};
271272

272-
let esp = self.open_esp()?;
273-
validate_esp(&esp)?;
274273
let updated = sysroot
275274
.sub_dir(&component_updatedirname(self))
276275
.context("opening update dir")?;
277276
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
278-
// For adoption, we should only touch files that we know about.
279-
let diff = updatef.relative_diff_to(&esp)?;
280-
log::trace!("applying adoption diff: {}", &diff);
281-
filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?;
277+
let esp_devices = self
278+
.get_all_esp_devices()
279+
.expect("get esp devices before adopt");
280+
let sysroot = sysroot.recover_path()?;
281+
282+
for esp_dev in esp_devices {
283+
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
284+
dest_path.join("EFI")
285+
} else {
286+
self.ensure_mounted_esp(&sysroot, &esp_dev)?.join("EFI")
287+
};
288+
289+
let esp = openat::Dir::open(&dest_path).context("opening EFI dir")?;
290+
validate_esp(&esp)?;
291+
292+
// For adoption, we should only touch files that we know about.
293+
let diff = updatef.relative_diff_to(&esp)?;
294+
log::trace!("applying adoption diff: {}", &diff);
295+
filetree::apply_diff(&updated, &esp, &diff, None)
296+
.context("applying filesystem changes")?;
297+
self.unmount().context("unmount after adopt")?;
298+
}
282299
Ok(InstalledContent {
283300
meta: updatemeta.clone(),
284301
filetree: Some(updatef),
@@ -300,9 +317,18 @@ impl Component for Efi {
300317
log::debug!("Found metadata {}", meta.version);
301318
let srcdir_name = component_updatedirname(self);
302319
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
303-
let destdir = &self.ensure_mounted_esp(Path::new(dest_root))?;
304320

305-
let destd = &openat::Dir::open(destdir)
321+
let destdir = if let Some(destdir) = self.check_existing_esp(dest_root)? {
322+
destdir
323+
} else {
324+
let esp_device = self
325+
.get_esp_device()
326+
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
327+
let esp_device = esp_device.to_str().unwrap();
328+
self.ensure_mounted_esp(dest_root, esp_device)?
329+
};
330+
331+
let destd = &openat::Dir::open(&destdir)
306332
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
307333
validate_esp(destd)?;
308334

@@ -344,12 +370,25 @@ impl Component for Efi {
344370
.context("opening update dir")?;
345371
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
346372
let diff = currentf.diff(&updatef)?;
347-
self.ensure_mounted_esp(Path::new("/"))?;
348-
let destdir = self.open_esp().context("opening EFI dir")?;
349-
validate_esp(&destdir)?;
350-
log::trace!("applying diff: {}", &diff);
351-
filetree::apply_diff(&updated, &destdir, &diff, None)
352-
.context("applying filesystem changes")?;
373+
let esp_devices = self
374+
.get_all_esp_devices()
375+
.context("get esp devices when running update")?;
376+
let sysroot = sysroot.recover_path()?;
377+
378+
for esp in esp_devices {
379+
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
380+
dest_path.join("EFI")
381+
} else {
382+
self.ensure_mounted_esp(&sysroot, &esp)?.join("EFI")
383+
};
384+
385+
let destdir = openat::Dir::open(&dest_path).context("opening EFI dir")?;
386+
validate_esp(&destdir)?;
387+
log::trace!("applying diff: {}", &diff);
388+
filetree::apply_diff(&updated, &destdir, &diff, None)
389+
.context("applying filesystem changes")?;
390+
self.unmount().context("unmount after update")?;
391+
}
353392
let adopted_from = None;
354393
Ok(InstalledContent {
355394
meta: updatemeta,
@@ -397,24 +436,36 @@ impl Component for Efi {
397436
}
398437

399438
fn validate(&self, current: &InstalledContent) -> Result<ValidationResult> {
400-
if !is_efi_booted()? && self.get_esp_device().is_none() {
439+
let esp_devices = self.get_all_esp_devices();
440+
if !is_efi_booted()? && esp_devices.is_none() {
401441
return Ok(ValidationResult::Skip);
402442
}
403443
let currentf = current
404444
.filetree
405445
.as_ref()
406446
.ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;
407-
self.ensure_mounted_esp(Path::new("/"))?;
408-
let efidir = self.open_esp()?;
409-
let diff = currentf.relative_diff_to(&efidir)?;
410447
let mut errs = Vec::new();
411-
for f in diff.changes.iter() {
412-
errs.push(format!("Changed: {}", f));
413-
}
414-
for f in diff.removals.iter() {
415-
errs.push(format!("Removed: {}", f));
448+
let esps = esp_devices.ok_or_else(|| anyhow::anyhow!("No esp device found!"))?;
449+
let dest_root = Path::new("/");
450+
for esp_dev in esps.iter() {
451+
let dest_path = if let Some(dest_path) = self.check_existing_esp(dest_root)? {
452+
dest_path.join("EFI")
453+
} else {
454+
self.ensure_mounted_esp(dest_root, &esp_dev)?.join("EFI")
455+
};
456+
457+
let efidir = openat::Dir::open(dest_path.as_path())?;
458+
let diff = currentf.relative_diff_to(&efidir)?;
459+
460+
for f in diff.changes.iter() {
461+
errs.push(format!("Changed: {}", f));
462+
}
463+
for f in diff.removals.iter() {
464+
errs.push(format!("Removed: {}", f));
465+
}
466+
assert_eq!(diff.additions.len(), 0);
467+
self.unmount().context("unmount after validate")?;
416468
}
417-
assert_eq!(diff.additions.len(), 0);
418469
if !errs.is_empty() {
419470
Ok(ValidationResult::Errors(errs))
420471
} else {

tests/fixtures/example-lsblk-output.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,37 @@
33
{
44
"path": "/dev/sr0",
55
"pttype": null,
6+
"parttype": null,
67
"parttypename": null
78
},{
89
"path": "/dev/zram0",
910
"pttype": null,
11+
"parttype": null,
1012
"parttypename": null
1113
},{
1214
"path": "/dev/vda",
1315
"pttype": "gpt",
16+
"parttype": null,
1417
"parttypename": null
1518
},{
1619
"path": "/dev/vda1",
1720
"pttype": "gpt",
21+
"parttype": null,
1822
"parttypename": "EFI System"
1923
},{
2024
"path": "/dev/vda2",
2125
"pttype": "gpt",
26+
"parttype": null,
2227
"parttypename": "Linux extended boot"
2328
},{
2429
"path": "/dev/vda3",
2530
"pttype": "gpt",
31+
"parttype": null,
2632
"parttypename": "Linux filesystem"
2733
},{
2834
"path": "/dev/mapper/luks-df2d5f95-5725-44dd-83e1-81bc4cdc49b8",
2935
"pttype": null,
36+
"parttype": null,
3037
"parttypename": null
3138
}
3239
]

tests/kola/raid1/config.bu

Lines changed: 7 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../data/libtest.sh

tests/kola/raid1/test.sh

Lines changed: 37 additions & 0 deletions
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)