From f9ce09cca11c9442b2469362c9d5f16f0e719dfd Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Sat, 30 Sep 2023 17:52:54 -0400 Subject: [PATCH] Add support for installing static grub configs Currently these are duplicated in osbuild and coreos-assembler. We will aim to deduplicate them here. Ideally we'd add support for "day 2" updates of these; I started on a patch for that but it's sadly messy. This is an incremental improvement. Signed-off-by: Colin Walters --- Makefile | 3 +- src/bootupd.rs | 22 +++- src/cli/bootupd.rs | 5 + src/efi.rs | 28 +++-- src/grub2/configs.d/README.md | 4 + src/grub2/grub-static-post.cfg | 17 +++ .../{grub-static.cfg => grub-static-pre.cfg} | 23 +--- src/grubconfigs.rs | 105 ++++++++++++++++++ src/main.rs | 6 + 9 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 src/grub2/configs.d/README.md create mode 100644 src/grub2/grub-static-post.cfg rename src/grub2/{grub-static.cfg => grub-static-pre.cfg} (79%) create mode 100644 src/grubconfigs.rs diff --git a/Makefile b/Makefile index 28d1a527..1e28ae7c 100644 --- a/Makefile +++ b/Makefile @@ -46,4 +46,5 @@ install: install-units ln -s ../bootupd.socket "${DESTDIR}$(PREFIX)/lib/systemd/system/multi-user.target.wants" install-grub-static: - install -D -t ${DESTDIR}$(PREFIX)/lib/bootupd/grub2-static src/grub2/*.cfg \ No newline at end of file + install -D -t ${DESTDIR}$(PREFIX)/lib/bootupd/grub2-static src/grub2/*.cfg + install -d ${DESTDIR}$(PREFIX)/lib/bootupd/grub2-static/configs.d diff --git a/src/bootupd.rs b/src/bootupd.rs index 285adfec..b7a92ffa 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -30,12 +30,13 @@ pub(crate) fn install( source_root: &str, dest_root: &str, device: Option<&str>, + with_static_configs: bool, target_components: Option<&[String]>, ) -> Result<()> { // TODO: Change this to an Option<&str>; though this probably balloons into having // DeviceComponent and FileBasedComponent let device = device.unwrap_or(""); - let source_root = openat::Dir::open(source_root)?; + let source_root = openat::Dir::open(source_root).context("Opening source root")?; SavedState::ensure_not_present(dest_root) .context("failed to install, invalid re-install attempted")?; @@ -62,7 +63,8 @@ pub(crate) fn install( } let mut state = SavedState::default(); - for component in target_components { + let mut installed_efi = false; + for &component in target_components.iter() { // skip for BIOS if device is empty if component.name() == "BIOS" && device.is_empty() { println!( @@ -77,10 +79,22 @@ pub(crate) fn install( .with_context(|| format!("installing component {}", component.name()))?; log::info!("Installed {} {}", component.name(), meta.meta.version); state.installed.insert(component.name().into(), meta); + // Yes this is a hack...the Component thing just turns out to be too generic. + if component.name() == "EFI" { + installed_efi = true; + } + } + let sysroot = &openat::Dir::open(dest_root)?; + + if with_static_configs { + crate::grubconfigs::install(sysroot, installed_efi)?; } - let sysroot = openat::Dir::open(dest_root)?; - let mut state_guard = SavedState::unlocked(sysroot).context("failed to acquire write lock")?; + // Unmount the ESP, etc. + drop(target_components); + + let mut state_guard = + SavedState::unlocked(sysroot.try_clone()?).context("failed to acquire write lock")?; state_guard .update_state(&state) .context("failed to update state")?; diff --git a/src/cli/bootupd.rs b/src/cli/bootupd.rs index 1e5384f7..f7b08678 100644 --- a/src/cli/bootupd.rs +++ b/src/cli/bootupd.rs @@ -52,6 +52,10 @@ pub struct InstallOpts { #[clap(long)] device: Option, + /// Enable installation of the built-in static config files + #[clap(long)] + with_static_configs: bool, + #[clap(long = "component")] /// Only install these components components: Option>, @@ -90,6 +94,7 @@ impl DCommand { &opts.src_root, &opts.dest_root, opts.device.as_deref(), + opts.with_static_configs, opts.components.as_deref(), ) .context("boot data installation failed")?; diff --git a/src/efi.rs b/src/efi.rs index b6d04ba4..9737f59b 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -20,7 +20,7 @@ use crate::util::CommandRunExt; use crate::{component::*, packagesystem}; /// Well-known paths to the ESP that may have been mounted external to us. -pub(crate) const ESP_MOUNTS: &[&str] = &["boot", "boot/efi", "efi"]; +pub(crate) const ESP_MOUNTS: &[&str] = &["boot/efi", "efi", "boot"]; /// The ESP partition label on Fedora CoreOS derivatives pub(crate) const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM"; @@ -54,7 +54,7 @@ impl Efi { Ok(esp) } - fn ensure_mounted_esp(&self, root: &Path) -> Result { + pub(crate) fn ensure_mounted_esp(&self, root: &Path) -> Result { let mut mountpoint = self.mountpoint.borrow_mut(); if let Some(mountpoint) = mountpoint.as_deref() { return Ok(mountpoint.to_owned()); @@ -84,16 +84,21 @@ impl Efi { } } let esp_device = esp_device.ok_or_else(|| anyhow::anyhow!("Failed to find ESP device"))?; - let tmppath = tempfile::tempdir_in("/tmp")?.into_path(); - let status = std::process::Command::new("mount") - .arg(&esp_device) - .arg(&tmppath) - .status()?; - if !status.success() { - anyhow::bail!("Failed to mount {:?}", esp_device); + for &mnt in ESP_MOUNTS.iter() { + let mnt = root.join(mnt); + if !mnt.exists() { + continue; + } + let status = std::process::Command::new("mount") + .arg(&esp_device) + .arg(&mnt) + .status()?; + if !status.success() { + anyhow::bail!("Failed to mount {:?}", esp_device); + } + log::debug!("Mounted at {mnt:?}"); + *mountpoint = Some(mnt); } - log::debug!("Mounted at {tmppath:?}"); - *mountpoint = Some(tmppath); Ok(mountpoint.as_deref().unwrap().to_owned()) } @@ -380,6 +385,7 @@ impl Component for Efi { impl Drop for Efi { fn drop(&mut self) { + log::debug!("Unmounting"); let _ = self.unmount(); } } diff --git a/src/grub2/configs.d/README.md b/src/grub2/configs.d/README.md new file mode 100644 index 00000000..a278f521 --- /dev/null +++ b/src/grub2/configs.d/README.md @@ -0,0 +1,4 @@ +Add drop-in grub fragments into this directory to have +them be installed into the final config. + +The filenames must end in `.cfg`. diff --git a/src/grub2/grub-static-post.cfg b/src/grub2/grub-static-post.cfg new file mode 100644 index 00000000..e426e390 --- /dev/null +++ b/src/grub2/grub-static-post.cfg @@ -0,0 +1,17 @@ +if [ x$feature_timeout_style = xy ] ; then + set timeout_style=menu + set timeout=1 +# Fallback normal timeout code in case the timeout_style feature is +# unavailable. +else + set timeout=1 +fi + +# Import user defined configuration +# tracker: https://github.com/coreos/fedora-coreos-tracker/issues/805 +if [ -f $prefix/user.cfg ]; then + source $prefix/user.cfg +fi + +blscfg + diff --git a/src/grub2/grub-static.cfg b/src/grub2/grub-static-pre.cfg similarity index 79% rename from src/grub2/grub-static.cfg rename to src/grub2/grub-static-pre.cfg index 878357d1..8ebab325 100644 --- a/src/grub2/grub-static.cfg +++ b/src/grub2/grub-static-pre.cfg @@ -59,25 +59,4 @@ function load_video { fi } -# tracker: https://github.com/coreos/fedora-coreos-tracker/issues/805 -if [ -f $prefix/platform.cfg ]; then - source $prefix/platform.cfg -fi - -if [ x$feature_timeout_style = xy ] ; then - set timeout_style=menu - set timeout=1 -# Fallback normal timeout code in case the timeout_style feature is -# unavailable. -else - set timeout=1 -fi - -# Import user defined configuration -# tracker: https://github.com/coreos/fedora-coreos-tracker/issues/805 -if [ -f $prefix/user.cfg ]; then - source $prefix/user.cfg -fi - -blscfg - +# Other package code will be injected from here diff --git a/src/grubconfigs.rs b/src/grubconfigs.rs new file mode 100644 index 00000000..c7d8b865 --- /dev/null +++ b/src/grubconfigs.rs @@ -0,0 +1,105 @@ +use std::fmt::Write; +use std::os::unix::prelude::OsStrExt; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use fn_error_context::context; +use openat_ext::OpenatDirExt; + +/// The subdirectory of /boot we use +const GRUB2DIR: &str = "grub2"; +const CONFIGDIR: &str = "/usr/lib/bootupd/grub2-static"; +const DROPINDIR: &str = "configs.d"; + +#[context("Locating EFI vendordir")] +pub(crate) fn find_efi_vendordir(efidir: &openat::Dir) -> Result { + for d in efidir.list_dir(".")? { + let d = d?; + if d.file_name().as_bytes() == b"BOOT" { + continue; + } + let meta = efidir.metadata(d.file_name())?; + if !meta.is_dir() { + continue; + } + return Ok(d.file_name().into()); + } + anyhow::bail!("Failed to find EFI vendor dir") +} + +/// Install the static GRUB config files. +#[context("Installing static GRUB configs")] +pub(crate) fn install(target_root: &openat::Dir, efi: bool) -> Result<()> { + let bootdir = &target_root.sub_dir("boot").context("Opening /boot")?; + + let mut config = std::fs::read_to_string(Path::new(CONFIGDIR).join("grub-static-pre.cfg"))?; + + let dropindir = openat::Dir::open(&Path::new(CONFIGDIR).join(DROPINDIR))?; + for ent in dropindir.list_dir(".")? { + let ent = ent?; + let name = ent.file_name(); + let name = name + .to_str() + .ok_or_else(|| anyhow!("Invalid UTF-8: {name:?}"))?; + if !name.ends_with(".cfg") { + log::debug!("Ignoring {name}"); + continue; + } + // SAFETY: Cannot fail + writeln!(config, "source $prefix/{name}").unwrap(); + dropindir.copy_file_at(name, bootdir, format!("{GRUB2DIR}/{name}"))?; + println!("Installed {name}"); + } + + { + let post = std::fs::read_to_string(Path::new(CONFIGDIR).join("grub-static-post.cfg"))?; + config.push_str(post.as_str()); + } + + bootdir + .write_file_contents(format!("{GRUB2DIR}/grub.cfg"), 0o644, config.as_bytes()) + .context("Copying grub-static.cfg")?; + println!("Installed: grub.cfg"); + + let efidir = efi + .then(|| { + target_root + .sub_dir_optional("boot/efi/EFI") + .context("Opening /boot/efi/EFI") + }) + .transpose()? + .flatten(); + if let Some(efidir) = efidir.as_ref() { + let vendordir = find_efi_vendordir(efidir)?; + log::debug!("vendordir={:?}", &vendordir); + let target = &vendordir.join("grub.cfg"); + efidir + .copy_file(&Path::new(CONFIGDIR).join("grub-static-efi.cfg"), target) + .context("Copying static EFI")?; + println!("Installed: {target:?}"); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[ignore] + fn test_install() -> Result<()> { + env_logger::init(); + let td = tempfile::tempdir()?; + let tdp = td.path(); + let td = openat::Dir::open(tdp)?; + std::fs::create_dir_all(tdp.join("boot/grub2"))?; + std::fs::create_dir_all(tdp.join("boot/efi/EFI/BOOT"))?; + std::fs::create_dir_all(tdp.join("boot/efi/EFI/fedora"))?; + install(&td, true).unwrap(); + + assert!(td.exists("boot/grub2/grub.cfg")?); + assert!(td.exists("boot/efi/EFI/fedora/grub.cfg")?); + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 46d676a8..61b73ff8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,12 @@ mod daemon; #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))] mod efi; mod filetree; +#[cfg(any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "powerpc64" +))] +mod grubconfigs; mod ipc; mod model; mod model_legacy;