Skip to content

Commit

Permalink
Merge pull request #562 from cgwalters/write-uuid-install
Browse files Browse the repository at this point in the history
Add support for root=boot (with EFI) and writing UUID file
  • Loading branch information
cgwalters authored Nov 19, 2023
2 parents 9d5d9d5 + 581102a commit eff31e9
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 23 deletions.
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

0 comments on commit eff31e9

Please sign in to comment.