Skip to content

Commit

Permalink
Support updating multiple EFIs on mirrored setups(RAID1)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
HuijingHei committed Jan 16, 2025
1 parent 30e304a commit 33d1285
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 58 deletions.
175 changes: 117 additions & 58 deletions src/efi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use rustix::fd::BorrowedFd;
use walkdir::WalkDir;
use widestring::U16CString;

use crate::blockdev;
use crate::filetree;
use crate::model::*;
use crate::ostreeutil;
Expand Down Expand Up @@ -54,31 +55,10 @@ pub(crate) fn is_efi_booted() -> Result<bool> {
#[derive(Default)]
pub(crate) struct Efi {
mountpoint: RefCell<Option<PathBuf>>,
esps: RefCell<Option<Vec<String>>>,
}

impl Efi {
fn esp_path(&self) -> Result<PathBuf> {
self.ensure_mounted_esp(Path::new("/"))
.map(|v| v.join("EFI"))
}

fn open_esp_optional(&self) -> Result<Option<openat::Dir>> {
if !is_efi_booted()? && self.get_esp_device().is_none() {
log::debug!("Skip EFI");
return Ok(None);
}
let sysroot = openat::Dir::open("/")?;
let esp = sysroot.sub_dir_optional(&self.esp_path()?)?;
Ok(esp)
}

fn open_esp(&self) -> Result<openat::Dir> {
self.ensure_mounted_esp(Path::new("/"))?;
let sysroot = openat::Dir::open("/")?;
let esp = sysroot.sub_dir(&self.esp_path()?)?;
Ok(esp)
}

fn get_esp_device(&self) -> Option<PathBuf> {
let esp_devices = [COREOS_ESP_PART_LABEL, ANACONDA_ESP_PART_LABEL]
.into_iter()
Expand All @@ -93,11 +73,32 @@ impl Efi {
return esp_device;
}

pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result<PathBuf> {
let mut mountpoint = self.mountpoint.borrow_mut();
fn get_all_esp_devices(&self) -> Option<Vec<String>> {
let mut esps = self.esps.borrow_mut();
if let Some(esp_devs) = esps.as_deref() {
log::debug!("Reusing existing esps {esp_devs:?}");
return Some(esp_devs.to_owned());
}

let mut esp_devices = vec![];
if let Some(esp_device) = self.get_esp_device() {
esp_devices.push(esp_device.to_string_lossy().into_owned());
} else {
esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices");
};
if !esp_devices.is_empty() {
*esps = Some(esp_devices.clone());
return Some(esp_devices);
}
return None;
}

fn check_existing_esp<P: AsRef<Path>>(&self, root: P) -> Result<Option<PathBuf>> {
let mountpoint = self.mountpoint.borrow_mut();
if let Some(mountpoint) = mountpoint.as_deref() {
return Ok(mountpoint.to_owned());
return Ok(Some(mountpoint.to_owned()));
}
let root = root.as_ref();
for &mnt in ESP_MOUNTS {
let mnt = root.join(mnt);
if !mnt.exists() {
Expand All @@ -109,13 +110,23 @@ impl Efi {
continue;
}
util::ensure_writable_mount(&mnt)?;
log::debug!("Reusing existing {mnt:?}");
return Ok(mnt);
log::debug!("Reusing existing mount point {mnt:?}");
return Ok(Some(mnt));
}
Ok(None)
}

let esp_device = self
.get_esp_device()
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
pub(crate) fn ensure_mounted_esp<P: AsRef<Path>>(
&self,
root: P,
esp_device: &str,
) -> Result<PathBuf> {
let mut mountpoint = self.mountpoint.borrow_mut();
if let Some(mountpoint) = mountpoint.as_deref() {
return Ok(mountpoint.to_owned());
}

let root = root.as_ref();
for &mnt in ESP_MOUNTS.iter() {
let mnt = root.join(mnt);
if !mnt.exists() {
Expand All @@ -134,10 +145,9 @@ impl Efi {
}
Ok(mountpoint.as_deref().unwrap().to_owned())
}

fn unmount(&self) -> Result<()> {
if let Some(mount) = self.mountpoint.borrow_mut().take() {
let status = Command::new("umount").arg(&mount).status()?;
let status = Command::new("umount").arg("-l").arg(&mount).status()?;
if !status.success() {
anyhow::bail!("Failed to unmount {mount:?}: {status:?}");
}
Expand Down Expand Up @@ -245,8 +255,7 @@ impl Component for Efi {
}

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

let esp = self.open_esp()?;
validate_esp(&esp)?;
let updated = sysroot
.sub_dir(&component_updatedirname(self))
.context("opening update dir")?;
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
// For adoption, we should only touch files that we know about.
let diff = updatef.relative_diff_to(&esp)?;
log::trace!("applying adoption diff: {}", &diff);
filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?;
let esp_devices = self
.get_all_esp_devices()
.expect("get esp devices before adopt");
let sysroot = sysroot.recover_path()?;

for esp_dev in esp_devices {
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
dest_path.join("EFI")
} else {
self.ensure_mounted_esp(&sysroot, &esp_dev)?.join("EFI")
};

let esp = openat::Dir::open(&dest_path).context("opening EFI dir")?;
validate_esp(&esp)?;

// For adoption, we should only touch files that we know about.
let diff = updatef.relative_diff_to(&esp)?;
log::trace!("applying adoption diff: {}", &diff);
filetree::apply_diff(&updated, &esp, &diff, None)
.context("applying filesystem changes")?;
self.unmount().context("unmount after adopt")?;
}
Ok(InstalledContent {
meta: updatemeta.clone(),
filetree: Some(updatef),
Expand All @@ -300,9 +325,18 @@ impl Component for Efi {
log::debug!("Found metadata {}", meta.version);
let srcdir_name = component_updatedirname(self);
let ft = crate::filetree::FileTree::new_from_dir(&src_root.sub_dir(&srcdir_name)?)?;
let destdir = &self.ensure_mounted_esp(Path::new(dest_root))?;

let destd = &openat::Dir::open(destdir)
let destdir = if let Some(destdir) = self.check_existing_esp(dest_root)? {
destdir
} else {
let esp_device = self
.get_esp_device()
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
let esp_device = esp_device.to_str().unwrap();
self.ensure_mounted_esp(dest_root, esp_device)?
};

let destd = &openat::Dir::open(&destdir)
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
validate_esp(destd)?;

Expand Down Expand Up @@ -344,12 +378,25 @@ impl Component for Efi {
.context("opening update dir")?;
let updatef = filetree::FileTree::new_from_dir(&updated).context("reading update dir")?;
let diff = currentf.diff(&updatef)?;
self.ensure_mounted_esp(Path::new("/"))?;
let destdir = self.open_esp().context("opening EFI dir")?;
validate_esp(&destdir)?;
log::trace!("applying diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;
let esp_devices = self
.get_all_esp_devices()
.context("get esp devices when running update")?;
let sysroot = sysroot.recover_path()?;

for esp in esp_devices {
let dest_path = if let Some(dest_path) = self.check_existing_esp(&sysroot)? {
dest_path.join("EFI")
} else {
self.ensure_mounted_esp(&sysroot, &esp)?.join("EFI")
};

let destdir = openat::Dir::open(&dest_path).context("opening EFI dir")?;
validate_esp(&destdir)?;
log::trace!("applying diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;
self.unmount().context("unmount after update")?;
}
let adopted_from = None;
Ok(InstalledContent {
meta: updatemeta,
Expand Down Expand Up @@ -397,24 +444,36 @@ impl Component for Efi {
}

fn validate(&self, current: &InstalledContent) -> Result<ValidationResult> {
if !is_efi_booted()? && self.get_esp_device().is_none() {
let esp_devices = self.get_all_esp_devices();
if !is_efi_booted()? && esp_devices.is_none() {
return Ok(ValidationResult::Skip);
}
let currentf = current
.filetree
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;
self.ensure_mounted_esp(Path::new("/"))?;
let efidir = self.open_esp()?;
let diff = currentf.relative_diff_to(&efidir)?;
let mut errs = Vec::new();
for f in diff.changes.iter() {
errs.push(format!("Changed: {}", f));
}
for f in diff.removals.iter() {
errs.push(format!("Removed: {}", f));
let esps = esp_devices.ok_or_else(|| anyhow::anyhow!("No esp device found!"))?;
let dest_root = Path::new("/");
for esp_dev in esps.iter() {
let dest_path = if let Some(dest_path) = self.check_existing_esp(dest_root)? {
dest_path.join("EFI")
} else {
self.ensure_mounted_esp(dest_root, &esp_dev)?.join("EFI")
};

let efidir = openat::Dir::open(dest_path.as_path())?;
let diff = currentf.relative_diff_to(&efidir)?;

for f in diff.changes.iter() {
errs.push(format!("Changed: {}", f));
}
for f in diff.removals.iter() {
errs.push(format!("Removed: {}", f));
}
assert_eq!(diff.additions.len(), 0);
self.unmount().context("unmount after validate")?;
}
assert_eq!(diff.additions.len(), 0);
if !errs.is_empty() {
Ok(ValidationResult::Errors(errs))
} else {
Expand Down
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Refs:
mod backend;
#[cfg(any(target_arch = "x86_64", target_arch = "powerpc64"))]
mod bios;
mod blockdev;
mod bootupd;
mod cli;
mod component;
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/example-lsblk-output.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,37 @@
{
"path": "/dev/sr0",
"pttype": null,
"parttype": null,
"parttypename": null
},{
"path": "/dev/zram0",
"pttype": null,
"parttype": null,
"parttypename": null
},{
"path": "/dev/vda",
"pttype": "gpt",
"parttype": null,
"parttypename": null
},{
"path": "/dev/vda1",
"pttype": "gpt",
"parttype": null,
"parttypename": "EFI System"
},{
"path": "/dev/vda2",
"pttype": "gpt",
"parttype": null,
"parttypename": "Linux extended boot"
},{
"path": "/dev/vda3",
"pttype": "gpt",
"parttype": null,
"parttypename": "Linux filesystem"
},{
"path": "/dev/mapper/luks-df2d5f95-5725-44dd-83e1-81bc4cdc49b8",
"pttype": null,
"parttype": null,
"parttypename": null
}
]
Expand Down
7 changes: 7 additions & 0 deletions tests/kola/raid1/config.bu
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
variant: fcos
version: 1.5.0
boot_device:
mirror:
devices:
- /dev/vda
- /dev/vdb
1 change: 1 addition & 0 deletions tests/kola/raid1/data/libtest.sh
37 changes: 37 additions & 0 deletions tests/kola/raid1/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
## kola:
## # additionalDisks is only supported on qemu.
## platforms: qemu
## # Root reprovisioning requires at least 4GiB of memory.
## minMemory: 4096
## # Linear RAID is setup on these disks.
## additionalDisks: ["10G"]
## # This test includes a lot of disk I/O and needs a higher
## # timeout value than the default.
## timeoutMin: 15
## description: Verify updating multiple EFIs with RAID 1 works.

set -xeuo pipefail

# shellcheck disable=SC1091
. "$KOLA_EXT_DATA/libtest.sh"

srcdev=$(findmnt -nvr /sysroot -o SOURCE)
[[ ${srcdev} == "/dev/md126" ]]

blktype=$(lsblk -o TYPE "${srcdev}" --noheadings)
[[ ${blktype} == raid1 ]]

fstype=$(findmnt -nvr /sysroot -o FSTYPE)
[[ ${fstype} == xfs ]]
ok "source is XFS on RAID1 device"


mount -o remount,rw /boot

rm -f -v /boot/bootupd-state.json

bootupctl adopt-and-update | grep "Adopted and updated: EFI"

bootupctl status | grep "Component EFI"
ok "bootupctl adopt-and-update supports multiple EFIs on RAID1"

0 comments on commit 33d1285

Please sign in to comment.