deb-bootupd/bootupd/src/grubconfigs.rs

180 lines
6.2 KiB
Rust
Executable file

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::<Result<Vec<_>>>()?;
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(())
}
}