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 13 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
2 changes: 2 additions & 0 deletions src/bios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ impl Bios {
}

// check bios_boot partition on gpt type disk
#[cfg(target_arch = "x86_64")]
fn get_bios_boot_partition(&self) -> Option<String> {
match blockdev::get_single_device("/") {
Ok(device) => {
Expand Down Expand Up @@ -148,6 +149,7 @@ impl Component for Bios {
}

fn query_adopt(&self) -> Result<Option<Adoptable>> {
// Skip BIOS adopt if booting with efi and none bios_boot part
#[cfg(target_arch = "x86_64")]
if crate::efi::is_efi_booted()? && self.get_bios_boot_partition().is_none() {
log::debug!("Skip BIOS adopt");
Expand Down
18 changes: 12 additions & 6 deletions src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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");
Expand All @@ -14,10 +14,18 @@ pub fn get_devices<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
}
let bootdir = openat::Dir::open(&bootdir)?;
// Run findmnt to get the source path of mount point boot
let fsinfo = crate::filesystem::inspect_filesystem(&bootdir, ".")?;
// If failed, change to sysroot
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)?;
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))?;
let parent_devices = bootc_blockdev::find_parent_devices(&source)
.with_context(|| format!("while looking for backing devices of {}", source))?;
log::debug!("Find parent devices: {parent_devices:?}");
Ok(parent_devices)
}
Expand All @@ -37,7 +45,6 @@ pub fn get_single_device<P: AsRef<Path>>(target_root: P) -> Result<String> {

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

/// Find all ESP partitions on the devices with mountpoint boot
#[allow(dead_code)]
pub fn find_colocated_esps<P: AsRef<Path>>(target_root: P) -> Result<Vec<String>> {
// first, get the parent device
let devices = get_devices(&target_root).with_context(|| "while looking for colocated ESPs")?;
Expand Down
169 changes: 110 additions & 59 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 @@ -57,28 +58,6 @@ pub(crate) struct Efi {
}

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 +72,22 @@ impl Efi {
return esp_device;
}

pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result<PathBuf> {
let mut mountpoint = self.mountpoint.borrow_mut();
// Get esp devices list on all devices
fn get_all_esp_devices(&self) -> Option<Vec<String>> {
let esp_devices = blockdev::find_colocated_esps("/").expect("get esp devices");
if !esp_devices.is_empty() {
return Some(esp_devices);
}
return None;
}

// Get mounted esp path
fn get_mounted_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 +99,24 @@ 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)
}

// Ensure mount the passed 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 esp_device = self
.get_esp_device()
.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?;
let root = root.as_ref();
for &mnt in ESP_MOUNTS.iter() {
let mnt = root.join(mnt);
if !mnt.exists() {
Expand All @@ -136,6 +137,7 @@ impl Efi {
fn unmount(&self) -> Result<()> {
if let Some(mount) = self.mountpoint.borrow_mut().take() {
Command::new("umount")
.arg("-l")
.arg(&mount)
.run()
.with_context(|| format!("Failed to unmount {mount:?}"))?;
Expand Down Expand Up @@ -243,8 +245,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 @@ -267,24 +268,40 @@ 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.get_mounted_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_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),
adopted_from: Some(meta.version),
})
}

// TODO: Remove dest_root; it was never actually used
// For installation, only support single ESP now
fn install(
&self,
src_root: &openat::Dir,
Expand All @@ -298,11 +315,19 @@ 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 destdir = if let Some(destdir) = self.get_mounted_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)
let destd = &openat::Dir::open(&destdir)
.with_context(|| format!("opening dest dir {}", destdir.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 @@ -339,12 +364,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.get_mounted_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_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 adopted_from = None;
Ok(InstalledContent {
meta: updatemeta,
Expand Down Expand Up @@ -392,24 +430,37 @@ 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() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we now have multiple places where we basically need to iterate over esp_devices and mount them and then do_some_work() I wish there was an easy way just to make that a funcion and then yield like we can in python so that we don't have to copy/paste this same code in many places.

Do you know of way to do that? I did some searching around rust book and the net and didn't find an easy answer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I can not find either.

let dest_path = if let Some(dest_path) = self.get_mounted_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 Expand Up @@ -447,7 +498,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
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"
Loading