Skip to content

Commit 33d1285

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. Xref to #132
1 parent 30e304a commit 33d1285

File tree

6 files changed

+170
-58
lines changed

6 files changed

+170
-58
lines changed

src/efi.rs

Lines changed: 117 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;
@@ -54,31 +55,10 @@ pub(crate) fn is_efi_booted() -> Result<bool> {
5455
#[derive(Default)]
5556
pub(crate) struct Efi {
5657
mountpoint: RefCell<Option<PathBuf>>,
58+
esps: RefCell<Option<Vec<String>>>,
5759
}
5860

5961
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-
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,32 @@ 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+
fn get_all_esp_devices(&self) -> Option<Vec<String>> {
77+
let mut esps = self.esps.borrow_mut();
78+
if let Some(esp_devs) = esps.as_deref() {
79+
log::debug!("Reusing existing esps {esp_devs:?}");
80+
return Some(esp_devs.to_owned());
81+
}
82+
83+
let mut esp_devices = vec![];
84+
if let Some(esp_device) = self.get_esp_device() {
85+
esp_devices.push(esp_device.to_string_lossy().into_owned());
86+
} else {
87+
esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices");
88+
};
89+
if !esp_devices.is_empty() {
90+
*esps = Some(esp_devices.clone());
91+
return Some(esp_devices);
92+
}
93+
return None;
94+
}
95+
96+
fn check_existing_esp<P: AsRef<Path>>(&self, root: P) -> Result<Option<PathBuf>> {
97+
let mountpoint = self.mountpoint.borrow_mut();
9898
if let Some(mountpoint) = mountpoint.as_deref() {
99-
return Ok(mountpoint.to_owned());
99+
return Ok(Some(mountpoint.to_owned()));
100100
}
101+
let root = root.as_ref();
101102
for &mnt in ESP_MOUNTS {
102103
let mnt = root.join(mnt);
103104
if !mnt.exists() {
@@ -109,13 +110,23 @@ impl Efi {
109110
continue;
110111
}
111112
util::ensure_writable_mount(&mnt)?;
112-
log::debug!("Reusing existing {mnt:?}");
113-
return Ok(mnt);
113+
log::debug!("Reusing existing mount point {mnt:?}");
114+
return Ok(Some(mnt));
114115
}
116+
Ok(None)
117+
}
115118

116-
let esp_device = self
117-
.get_esp_device()
118-
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
119+
pub(crate) fn ensure_mounted_esp<P: AsRef<Path>>(
120+
&self,
121+
root: P,
122+
esp_device: &str,
123+
) -> Result<PathBuf> {
124+
let mut mountpoint = self.mountpoint.borrow_mut();
125+
if let Some(mountpoint) = mountpoint.as_deref() {
126+
return Ok(mountpoint.to_owned());
127+
}
128+
129+
let root = root.as_ref();
119130
for &mnt in ESP_MOUNTS.iter() {
120131
let mnt = root.join(mnt);
121132
if !mnt.exists() {
@@ -134,10 +145,9 @@ impl Efi {
134145
}
135146
Ok(mountpoint.as_deref().unwrap().to_owned())
136147
}
137-
138148
fn unmount(&self) -> Result<()> {
139149
if let Some(mount) = self.mountpoint.borrow_mut().take() {
140-
let status = Command::new("umount").arg(&mount).status()?;
150+
let status = Command::new("umount").arg("-l").arg(&mount).status()?;
141151
if !status.success() {
142152
anyhow::bail!("Failed to unmount {mount:?}: {status:?}");
143153
}
@@ -245,8 +255,7 @@ impl Component for Efi {
245255
}
246256

247257
fn query_adopt(&self) -> Result<Option<Adoptable>> {
248-
let esp = self.open_esp_optional()?;
249-
if esp.is_none() {
258+
if self.get_all_esp_devices().is_none() {
250259
log::trace!("No ESP detected");
251260
return Ok(None);
252261
};
@@ -269,16 +278,32 @@ impl Component for Efi {
269278
anyhow::bail!("Failed to find adoptable system")
270279
};
271280

272-
let esp = self.open_esp()?;
273-
validate_esp(&esp)?;
274281
let updated = sysroot
275282
.sub_dir(&component_updatedirname(self))
276283
.context("opening update dir")?;
277284
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")?;
285+
let esp_devices = self
286+
.get_all_esp_devices()
287+
.expect("get esp devices before adopt");
288+
let sysroot = sysroot.recover_path()?;
289+
290+
for esp_dev in esp_devices {
291+
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
292+
dest_path.join("EFI")
293+
} else {
294+
self.ensure_mounted_esp(&sysroot, &esp_dev)?.join("EFI")
295+
};
296+
297+
let esp = openat::Dir::open(&dest_path).context("opening EFI dir")?;
298+
validate_esp(&esp)?;
299+
300+
// For adoption, we should only touch files that we know about.
301+
let diff = updatef.relative_diff_to(&esp)?;
302+
log::trace!("applying adoption diff: {}", &diff);
303+
filetree::apply_diff(&updated, &esp, &diff, None)
304+
.context("applying filesystem changes")?;
305+
self.unmount().context("unmount after adopt")?;
306+
}
282307
Ok(InstalledContent {
283308
meta: updatemeta.clone(),
284309
filetree: Some(updatef),
@@ -300,9 +325,18 @@ impl Component for Efi {
300325
log::debug!("Found metadata {}", meta.version);
301326
let srcdir_name = component_updatedirname(self);
302327
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))?;
304328

305-
let destd = &openat::Dir::open(destdir)
329+
let destdir = if let Some(destdir) = self.check_existing_esp(dest_root)? {
330+
destdir
331+
} else {
332+
let esp_device = self
333+
.get_esp_device()
334+
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
335+
let esp_device = esp_device.to_str().unwrap();
336+
self.ensure_mounted_esp(dest_root, esp_device)?
337+
};
338+
339+
let destd = &openat::Dir::open(&destdir)
306340
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
307341
validate_esp(destd)?;
308342

@@ -344,12 +378,25 @@ impl Component for Efi {
344378
.context("opening update dir")?;
345379
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
346380
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")?;
381+
let esp_devices = self
382+
.get_all_esp_devices()
383+
.context("get esp devices when running update")?;
384+
let sysroot = sysroot.recover_path()?;
385+
386+
for esp in esp_devices {
387+
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
388+
dest_path.join("EFI")
389+
} else {
390+
self.ensure_mounted_esp(&sysroot, &esp)?.join("EFI")
391+
};
392+
393+
let destdir = openat::Dir::open(&dest_path).context("opening EFI dir")?;
394+
validate_esp(&destdir)?;
395+
log::trace!("applying diff: {}", &diff);
396+
filetree::apply_diff(&updated, &destdir, &diff, None)
397+
.context("applying filesystem changes")?;
398+
self.unmount().context("unmount after update")?;
399+
}
353400
let adopted_from = None;
354401
Ok(InstalledContent {
355402
meta: updatemeta,
@@ -397,24 +444,36 @@ impl Component for Efi {
397444
}
398445

399446
fn validate(&self, current: &InstalledContent) -> Result<ValidationResult> {
400-
if !is_efi_booted()? && self.get_esp_device().is_none() {
447+
let esp_devices = self.get_all_esp_devices();
448+
if !is_efi_booted()? && esp_devices.is_none() {
401449
return Ok(ValidationResult::Skip);
402450
}
403451
let currentf = current
404452
.filetree
405453
.as_ref()
406454
.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)?;
410455
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));
456+
let esps = esp_devices.ok_or_else(|| anyhow::anyhow!("No esp device found!"))?;
457+
let dest_root = Path::new("/");
458+
for esp_dev in esps.iter() {
459+
let dest_path = if let Some(dest_path) = self.check_existing_esp(dest_root)? {
460+
dest_path.join("EFI")
461+
} else {
462+
self.ensure_mounted_esp(dest_root, &esp_dev)?.join("EFI")
463+
};
464+
465+
let efidir = openat::Dir::open(dest_path.as_path())?;
466+
let diff = currentf.relative_diff_to(&efidir)?;
467+
468+
for f in diff.changes.iter() {
469+
errs.push(format!("Changed: {}", f));
470+
}
471+
for f in diff.removals.iter() {
472+
errs.push(format!("Removed: {}", f));
473+
}
474+
assert_eq!(diff.additions.len(), 0);
475+
self.unmount().context("unmount after validate")?;
416476
}
417-
assert_eq!(diff.additions.len(), 0);
418477
if !errs.is_empty() {
419478
Ok(ValidationResult::Errors(errs))
420479
} else {

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Refs:
1818
mod backend;
1919
#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
2020
mod bios;
21+
mod blockdev;
2122
mod bootupd;
2223
mod cli;
2324
mod component;

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)