Skip to content
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

Add support for root=boot (with EFI) and writing UUID file #562

Merged
merged 1 commit into from
Nov 19, 2023
Merged
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
1 change: 1 addition & 0 deletions src/bios.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ impl Component for Bios {
src_root: &openat::Dir,
dest_root: &str,
device: &str,
_update_firmware: bool,
) -> Result<InstalledContent> {
let meta = if let Some(meta) = get_component_update(src_root, self)? {
meta
Expand Down
42 changes: 31 additions & 11 deletions src/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ pub(crate) enum ClientRequest {
Status,
}

pub(crate) enum ConfigMode {
None,
Static,
WithUUID,
}

impl ConfigMode {
pub(crate) fn enabled_with_uuid(&self) -> Option<bool> {
match self {
ConfigMode::None => None,
ConfigMode::Static => Some(false),
ConfigMode::WithUUID => Some(true),
}
}
}

pub(crate) fn install(
source_root: &str,
dest_root: &str,
device: Option<&str>,
with_static_configs: bool,
configs: ConfigMode,
update_firmware: bool,
target_components: Option<&[String]>,
auto_components: bool,
) -> Result<()> {
Expand Down Expand Up @@ -78,7 +95,7 @@ pub(crate) fn install(
}

let meta = component
.install(&source_root, dest_root, device)
.install(&source_root, dest_root, device, update_firmware)
.with_context(|| format!("installing component {}", component.name()))?;
log::info!("Installed {} {}", component.name(), meta.meta.version);
state.installed.insert(component.name().into(), meta);
Expand All @@ -89,14 +106,17 @@ pub(crate) fn install(
}
let sysroot = &openat::Dir::open(dest_root)?;

if with_static_configs {
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64"
))]
crate::grubconfigs::install(sysroot, installed_efi)?;
// On other architectures, assume that there's nothing to do.
match configs.enabled_with_uuid() {
Some(uuid) => {
#[cfg(any(
target_arch = "x86_64",
target_arch = "aarch64",
target_arch = "powerpc64"
))]
crate::grubconfigs::install(sysroot, installed_efi, uuid)?;
// On other architectures, assume that there's nothing to do.
}
None => {}
}

// Unmount the ESP, etc.
Expand Down Expand Up @@ -126,7 +146,7 @@ pub(crate) fn get_components_impl(auto: bool) -> Components {
#[cfg(target_arch = "x86_64")]
{
if auto {
let is_efi_booted = Path::new("/sys/firmware/efi").exists();
let is_efi_booted = crate::efi::is_efi_booted().unwrap();
log::info!(
"System boot method: {}",
if is_efi_booted { "EFI" } else { "BIOS" }
Expand Down
21 changes: 19 additions & 2 deletions src/cli/bootupd.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::bootupd;
use crate::bootupd::{self, ConfigMode};
use anyhow::{Context, Result};
use clap::Parser;
use log::LevelFilter;
Expand Down Expand Up @@ -56,6 +56,15 @@ pub struct InstallOpts {
#[clap(long)]
with_static_configs: bool,

/// Implies `--with-static-configs`. When present, this also writes a
/// file with the UUID of the target filesystems.
#[clap(long)]
write_uuid: bool,

/// On EFI systems, invoke `efibootmgr` to update the firmware.
#[clap(long)]
update_firmware: bool,

#[clap(long = "component", conflicts_with = "auto")]
/// Only install these components
components: Option<Vec<String>>,
Expand Down Expand Up @@ -97,11 +106,19 @@ impl DCommand {

/// Runner for `install` verb.
pub(crate) fn run_install(opts: InstallOpts) -> Result<()> {
let configmode = if opts.write_uuid {
ConfigMode::WithUUID
} else if opts.with_static_configs {
ConfigMode::Static
} else {
ConfigMode::None
};
bootupd::install(
&opts.src_root,
&opts.dest_root,
opts.device.as_deref(),
opts.with_static_configs,
configmode,
opts.update_firmware,
opts.components.as_deref(),
opts.auto,
)
Expand Down
1 change: 1 addition & 0 deletions src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub(crate) trait Component {
src_root: &openat::Dir,
dest_root: &str,
device: &str,
update_firmware: bool,
) -> Result<InstalledContent>;

/// Implementation of `bootupd generate-update-metadata` for a given component.
Expand Down
105 changes: 99 additions & 6 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 fn_error_context::context;
use openat_ext::OpenatDirExt;
use widestring::U16CString;

Expand All @@ -22,6 +23,10 @@ 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/efi", "efi", "boot"];

/// The binary to change EFI boot ordering
const EFIBOOTMGR: &str = "efibootmgr";
const SHIM: &str = "shimx64.efi";

/// The ESP partition label on Fedora CoreOS derivatives
pub(crate) const COREOS_ESP_PART_LABEL: &str = "EFI-SYSTEM";
pub(crate) const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition";
Expand All @@ -30,6 +35,13 @@ pub(crate) const ANACONDA_ESP_PART_LABEL: &str = "EFI\\x20System\\x20Partition";
const LOADER_INFO_VAR_STR: &str = "LoaderInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";
const STUB_INFO_VAR_STR: &str = "StubInfo-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f";

/// Return `true` if the system is booted via EFI
pub(crate) fn is_efi_booted() -> Result<bool> {
Path::new("/sys/firmware/efi")
.try_exists()
.map_err(Into::into)
}

#[derive(Default)]
pub(crate) struct Efi {
mountpoint: RefCell<Option<PathBuf>>,
Expand Down Expand Up @@ -113,6 +125,17 @@ impl Efi {
}
Ok(())
}

#[context("Updating EFI firmware variables")]
fn update_firmware(&self, device: &str, espdir: &openat::Dir) -> Result<()> {
let efidir = &espdir.sub_dir("EFI").context("Opening EFI")?;
let vendordir = super::grubconfigs::find_efi_vendordir(efidir)?;
let vendordir = vendordir
.to_str()
.ok_or_else(|| anyhow::anyhow!("Invalid non-UTF-8 vendordir"))?;
clear_efi_current()?;
set_efi_current(device, espdir, vendordir)
}
}

/// Convert a nul-terminated UTF-16 byte array to a String.
Expand Down Expand Up @@ -259,7 +282,8 @@ impl Component for Efi {
&self,
src_root: &openat::Dir,
dest_root: &str,
_: &str,
device: &str,
update_firmware: bool,
) -> Result<InstalledContent> {
let meta = if let Some(meta) = get_component_update(src_root, self)? {
meta
Expand All @@ -270,11 +294,11 @@ impl Component for Efi {
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 destd = openat::Dir::open(destdir)
.with_context(|| format!("opening dest dir {}", destdir.display()))?;
validate_esp(&destd)?;
}

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

// TODO - add some sort of API that allows directly setting the working
// directory to a file descriptor.
let r = std::process::Command::new("cp")
Expand All @@ -286,6 +310,9 @@ impl Component for Efi {
if !r.success() {
anyhow::bail!("Failed to copy");
}
if update_firmware {
self.update_firmware(device, destd)?
}
Ok(InstalledContent {
meta,
filetree: Some(ft),
Expand Down Expand Up @@ -399,3 +426,69 @@ fn validate_esp(dir: &openat::Dir) -> Result<()> {
};
Ok(())
}

#[context("Clearing current EFI boot entry")]
pub(crate) fn clear_efi_current() -> Result<()> {
const BOOTCURRENT: &str = "BootCurrent";
if !crate::efi::is_efi_booted()? {
log::debug!("System is not booted via EFI");
return Ok(());
}
let output = Command::new(EFIBOOTMGR).output()?;
if !output.status.success() {
anyhow::bail!("Failed to invoke {EFIBOOTMGR}")
}
let output = String::from_utf8(output.stdout)?;
let current = output
.lines()
.filter_map(|l| l.split_once(':'))
.filter_map(|(k, v)| (k == BOOTCURRENT).then_some(v.trim()))
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to find BootCurrent"))?;
let output = Command::new(EFIBOOTMGR)
.args(["-b", current, "-B"])
.output()?;
if !output.status.success() {
std::io::copy(
&mut std::io::Cursor::new(output.stderr),
&mut std::io::stderr().lock(),
)?;
anyhow::bail!("Failed to invoke {EFIBOOTMGR}");
}
anyhow::Ok(())
}

#[context("Adding new EFI boot entry")]
pub(crate) fn set_efi_current(device: &str, espdir: &openat::Dir, vendordir: &str) -> Result<()> {
let fsinfo = crate::filesystem::inspect_filesystem(espdir, ".")?;
let source = fsinfo.source;
let devname = source
.rsplit_once('/')
.ok_or_else(|| anyhow::anyhow!("Failed to parse {source}"))?
.1;
let partition_path = format!("/sys/class/block/{devname}/partition");
let partition_number = std::fs::read_to_string(&partition_path)
.with_context(|| format!("Failed to read {partition_path}"))?;
let shim = format!("{vendordir}/{SHIM}");
if espdir.exists(&shim)? {
anyhow::bail!("Failed to find {SHIM}");
}
let loader = format!("\\EFI\\{}\\shimx64.efi", vendordir);
let st = Command::new(EFIBOOTMGR)
.args([
"--create",
"--disk",
device,
"--part",
partition_number.as_str(),
"--loader",
loader.as_str(),
"--label",
vendordir,
])
.status()?;
if !st.success() {
anyhow::bail!("Failed to invoke {EFIBOOTMGR}")
}
anyhow::Ok(())
}
44 changes: 44 additions & 0 deletions src/filesystem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::os::fd::AsRawFd;
use std::os::unix::process::CommandExt;
use std::process::Command;

use anyhow::{Context, Result};
use fn_error_context::context;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub(crate) struct Filesystem {
pub(crate) source: String,
pub(crate) fstype: String,
pub(crate) options: String,
pub(crate) uuid: Option<String>,
}

#[derive(Deserialize, Debug)]
pub(crate) struct Findmnt {
pub(crate) filesystems: Vec<Filesystem>,
}

#[context("Inspecting filesystem {path:?}")]
pub(crate) fn inspect_filesystem(root: &openat::Dir, path: &str) -> Result<Filesystem> {
let rootfd = root.as_raw_fd();
// SAFETY: This is unsafe just for the pre_exec, when we port to cap-std we can use cap-std-ext
let o = unsafe {
Command::new("findmnt")
.args(["-J", "-v", "--output-all", path])
.pre_exec(move || nix::unistd::fchdir(rootfd).map_err(Into::into))
.output()?
};
let st = o.status;
if !st.success() {
anyhow::bail!("findmnt failed: {st:?}");
}
let o: Findmnt = serde_json::from_reader(std::io::Cursor::new(&o.stdout))
.context("Parsing findmnt output")?;
o.filesystems
.into_iter()
.next()
.ok_or_else(|| anyhow::anyhow!("findmnt returned no data"))
}
9 changes: 7 additions & 2 deletions src/grub2/grub-static-efi.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ else
search --label boot --set prefix --no-floppy
fi
fi
set prefix=($prefix)/grub2
configfile $prefix/grub.cfg
if [ -d ($prefix)/grub2 ]; then
set prefix=($prefix)/grub2
configfile $prefix/grub.cfg
else
set prefix=($prefix)/boot/grub2
configfile $prefix/grub.cfg
fi
boot

Loading
Loading