use std::fmt::Write; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use bootc_internal_utils::CommandRunExt; use fn_error_context::context; use openat_ext::OpenatDirExt; use crate::freezethaw::fsfreeze_thaw_cycle; /// The subdirectory of /boot we use const GRUB2DIR: &str = "grub2"; const CONFIGDIR: &str = "/usr/lib/bootupd/grub2-static"; const DROPINDIR: &str = "configs.d"; // The related grub files const GRUBENV: &str = "grubenv"; pub(crate) const GRUBCONFIG: &str = "grub.cfg"; pub(crate) const GRUBCONFIG_BACKUP: &str = "grub.cfg.backup"; // File mode for /boot/grub2/grub.config // https://github.com/coreos/bootupd/issues/952 const GRUBCONFIG_FILE_MODE: u32 = 0o600; /// Install the static GRUB config files. #[context("Installing static GRUB configs")] pub(crate) fn install( target_root: &openat::Dir, installed_efi_vendor: Option<&str>, write_uuid: bool, ) -> Result<()> { let bootdir = &target_root.sub_dir("boot").context("Opening /boot")?; let boot_is_mount = { let root_dev = target_root.self_metadata()?.stat().st_dev; let boot_dev = bootdir.self_metadata()?.stat().st_dev; log::debug!("root_dev={root_dev} boot_dev={boot_dev}"); root_dev != boot_dev }; if !bootdir.exists(GRUB2DIR)? { bootdir.create_dir(GRUB2DIR, 0o700)?; } let mut config = String::from("# Generated by bootupd / do not edit\n\n"); let pre = std::fs::read_to_string(Path::new(CONFIGDIR).join("grub-static-pre.cfg"))?; config.push_str(pre.as_str()); let dropindir = openat::Dir::open(&Path::new(CONFIGDIR).join(DROPINDIR))?; // Sort the files for reproducibility let mut entries = dropindir .list_dir(".")? .map(|e| e.map_err(anyhow::Error::msg)) .collect::>>()?; entries.sort_by(|a, b| a.file_name().cmp(b.file_name())); for ent in entries { 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; } writeln!(config, "\n### BEGIN {name} ###")?; let dropin = std::fs::read_to_string(Path::new(CONFIGDIR).join(DROPINDIR).join(name))?; config.push_str(dropin.as_str()); writeln!(config, "### END {name} ###")?; println!("Added {name}"); } let grub2dir = bootdir.sub_dir(GRUB2DIR)?; grub2dir .write_file_contents("grub.cfg", GRUBCONFIG_FILE_MODE, config.as_bytes()) .context("Copying grub-static.cfg")?; println!("Installed: grub.cfg"); write_grubenv(&bootdir).context("Create grubenv")?; let uuid_path = if write_uuid { let target_fs = if boot_is_mount { bootdir } else { target_root }; let bootfs_meta = crate::filesystem::inspect_filesystem(target_fs, ".")?; let bootfs_uuid = bootfs_meta .uuid .ok_or_else(|| anyhow::anyhow!("Failed to find UUID for boot"))?; let grub2_uuid_contents = format!("set BOOT_UUID=\"{bootfs_uuid}\"\n"); let uuid_path = "bootuuid.cfg"; grub2dir .write_file_contents(uuid_path, 0o644, grub2_uuid_contents) .context("Writing bootuuid.cfg")?; println!("Installed: bootuuid.cfg"); Some(uuid_path) } else { None }; fsfreeze_thaw_cycle(grub2dir.open_file(".")?)?; if let Some(vendordir) = installed_efi_vendor { log::debug!("vendordir={:?}", &vendordir); let vendor = PathBuf::from(vendordir); let target = &vendor.join("grub.cfg"); let dest_efidir = target_root .sub_dir_optional("boot/efi/EFI") .context("Opening /boot/efi/EFI")?; if let Some(efidir) = dest_efidir { efidir .copy_file(&Path::new(CONFIGDIR).join("grub-static-efi.cfg"), target) .context("Copying static EFI")?; println!("Installed: {target:?}"); if let Some(uuid_path) = uuid_path { let target = &vendor.join(uuid_path); grub2dir .copy_file_at(uuid_path, &efidir, target) .context("Writing bootuuid.cfg to efi dir")?; println!("Installed: {target:?}"); } fsfreeze_thaw_cycle(efidir.open_file(".")?)?; } else { println!("Could not find /boot/efi/EFI when installing {target:?}"); } } Ok(()) } #[context("Create file boot/grub2/grubenv")] fn write_grubenv(bootdir: &openat::Dir) -> Result<()> { let grubdir = &bootdir.sub_dir(GRUB2DIR).context("Opening boot/grub2")?; if grubdir.exists(GRUBENV)? { return Ok(()); } let editenv = Path::new("/usr/bin/grub2-editenv"); if !editenv.exists() { anyhow::bail!("Failed to find {:?}", editenv); } std::process::Command::new(editenv) .args([GRUBENV, "create"]) .current_dir(format!("/proc/self/fd/{}", grubdir.as_raw_fd())) .run_with_cmd_context() } #[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, Some("fedora"), false).unwrap(); assert!(td.exists("boot/grub2/grub.cfg")?); assert!(td.exists("boot/efi/EFI/fedora/grub.cfg")?); Ok(()) } #[test] fn test_write_grubenv() -> Result<()> { // Skip this test if grub2-editenv is not installed let editenv = Path::new("/usr/bin/grub2-editenv"); if !editenv.try_exists()? { return Ok(()); } let td = tempfile::tempdir()?; let tdp = td.path(); std::fs::create_dir_all(tdp.join("boot/grub2"))?; let td = openat::Dir::open(&tdp.join("boot"))?; write_grubenv(&td)?; assert!(td.exists("grub2/grubenv")?); Ok(()) } }