Skip to content

efi: support updating multiple EFIs in mirrored setups (RAID1) #855

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,29 @@ use anyhow::{bail, Context, Result};
use bootc_blockdev::PartitionTable;
use fn_error_context::context;

#[context("get parent devices from mount point boot")]
#[context("get parent devices from mount point boot or sysroot")]
pub fn get_devices<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
let target_root = target_root.as_ref();
let bootdir = target_root.join("boot");
if !bootdir.exists() {
bail!("{} does not exist", bootdir.display());
}
let bootdir = openat::Dir::open(&bootdir)?;
// Run findmnt to get the source path of mount point boot
let fsinfo = crate::filesystem::inspect_filesystem(&bootdir, ".")?;
// Attempt to get the source path of the /boot mount point using findmnt
// Fallback to /sysroot if the command fails
let source = if let Ok(fsinfo) = crate::filesystem::inspect_filesystem(&bootdir, ".") {
fsinfo.source
} else {
let sysroot = target_root.join("sysroot");
let sysrootdir = openat::Dir::open(&sysroot)
.with_context(|| format!("Opening sysroot {}", sysroot.display()))?;
let fsinfo = crate::filesystem::inspect_filesystem(&sysrootdir, ".")?;
fsinfo.source
};
// Find the parent devices of the source path
let parent_devices = bootc_blockdev::find_parent_devices(&fsinfo.source)
.with_context(|| format!("while looking for backing devices of {}", fsinfo.source))?;
log::debug!("Find parent devices: {parent_devices:?}");
let parent_devices = bootc_blockdev::find_parent_devices(&source)
.with_context(|| format!("While looking for backing devices of {}", source))?;
log::debug!("Found parent devices: {parent_devices:?}");
Ok(parent_devices)
}

Expand Down
112 changes: 51 additions & 61 deletions src/efi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::path::{Path, PathBuf};
use std::process::Command;

use anyhow::{bail, Context, Result};
use bootc_utils::CommandRunExt;
use cap_std::fs::Dir;
use cap_std_ext::cap_std;
use fn_error_context::context;
Expand All @@ -22,7 +23,7 @@ use widestring::U16CString;
use crate::bootupd::RootContext;
use crate::model::*;
use crate::ostreeutil;
use crate::util::{self, CommandRunExt};
use crate::util;
use crate::{blockdev, filetree};
use crate::{component::*, packagesystem};

Expand Down Expand Up @@ -123,6 +124,14 @@ impl Efi {

fn unmount(&self) -> Result<()> {
if let Some(mount) = self.mountpoint.borrow_mut().take() {
// To safely unmount `/boot/efi`, first ensure
// all pending writes are flushed to disk using `sync`.
Command::new("sync")
.arg("--file-system")
.arg(&mount)
.run_with_cmd_context()
.with_context(|| format!("Failed to sync before unmounting {mount:?}"))?;

Command::new("umount")
.arg(&mount)
.run()
Expand Down Expand Up @@ -256,32 +265,26 @@ impl Component for Efi {
anyhow::bail!("Failed to find adoptable system")
};

let esp_devices = esp_devices.unwrap_or_default();
let mut devices = esp_devices.iter();
let Some(esp) = devices.next() else {
anyhow::bail!("Failed to find esp device");
};

if let Some(next_esp) = devices.next() {
anyhow::bail!(
"Found multiple esp devices {esp} and {next_esp}; not currently supported"
);
}
let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?;

let destdir = &openat::Dir::open(&destpath.join("EFI"))
.with_context(|| format!("opening EFI dir {}", destpath.display()))?;
validate_esp(&destdir)?;
let updated = rootcxt
.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(&destdir)?;
log::trace!("applying adoption diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;

let esp_devices = esp_devices.unwrap_or_default();
for esp in esp_devices {
let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?;

let esp = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?;
validate_esp_fstype(&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 Down Expand Up @@ -312,7 +315,7 @@ impl Component for Efi {

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

// TODO - add some sort of API that allows directly setting the working
// directory to a file descriptor.
Expand Down Expand Up @@ -354,24 +357,17 @@ impl Component for Efi {
let Some(esp_devices) = blockdev::find_colocated_esps(&rootcxt.devices)? else {
anyhow::bail!("Failed to find all esp devices");
};
let mut devices = esp_devices.iter();
let Some(esp) = devices.next() else {
anyhow::bail!("Failed to find esp device");
};

if let Some(next_esp) = devices.next() {
anyhow::bail!(
"Found multiple esp devices {esp} and {next_esp}; not currently supported"
);
for esp in esp_devices {
let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?;
let destdir = openat::Dir::open(&destpath.join("EFI")).context("opening EFI dir")?;
validate_esp_fstype(&destdir)?;
log::trace!("applying diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;
self.unmount().context("unmount after update")?;
}
let destpath = &self.ensure_mounted_esp(rootcxt.path.as_ref(), Path::new(&esp))?;

let destdir = &openat::Dir::open(&destpath.join("EFI"))
.with_context(|| format!("opening EFI dir {}", destpath.display()))?;
validate_esp(&destdir)?;
log::trace!("applying diff: {}", &diff);
filetree::apply_diff(&updated, &destdir, &diff, None)
.context("applying filesystem changes")?;

let adopted_from = None;
Ok(InstalledContent {
meta: updatemeta,
Expand Down Expand Up @@ -430,31 +426,25 @@ impl Component for Efi {
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No filetree for installed EFI found!"))?;

let mut errs = Vec::new();
let esp_devices = esp_devices.unwrap_or_default();
let mut devices = esp_devices.iter();
let Some(esp) = devices.next() else {
anyhow::bail!("Failed to find esp device");
};

if let Some(next_esp) = devices.next() {
anyhow::bail!(
"Found multiple esp devices {esp} and {next_esp}; not currently supported"
);
}
let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp))?;
for esp in esp_devices.iter() {
let destpath = &self.ensure_mounted_esp(Path::new("/"), Path::new(&esp))?;

let efidir = &openat::Dir::open(&destpath.join("EFI"))
.with_context(|| format!("opening EFI dir {}", destpath.display()))?;
let efidir = &openat::Dir::open(&destpath.join("EFI"))
.with_context(|| format!("opening EFI dir {}", destpath.display()))?;
let diff = currentf.relative_diff_to(&efidir)?;

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));
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 Expand Up @@ -492,7 +482,7 @@ impl Drop for Efi {
}
}

fn validate_esp(dir: &openat::Dir) -> Result<()> {
fn validate_esp_fstype(dir: &openat::Dir) -> Result<()> {
let dir = unsafe { BorrowedFd::borrow_raw(dir.as_raw_fd()) };
let stat = rustix::fs::fstatfs(&dir)?;
if stat.f_type != libc::MSDOS_SUPER_MAGIC {
Expand Down
1 change: 1 addition & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::process::Command;
use anyhow::{bail, Context, Result};
use openat_ext::OpenatDirExt;

#[allow(dead_code)]
pub(crate) trait CommandRunExt {
fn run(&mut self) -> Result<()>;
}
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
35 changes: 35 additions & 0 deletions tests/kola/raid1/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/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 using 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"
Loading