Skip to content

Commit

Permalink
migrate-static-grub-config: Add GRUB static migration subcommand
Browse files Browse the repository at this point in the history
Add a hidden subcommand that migrates existing systems using a dynamic
GRUB config to a static one.

This command is expected to be run after a successful bootloader update.
One way to do that is to add it as a droppin unit config for the
`bootloader-update.service` unit included in this repo:

```
$ cat /usr/lib/systremd/system/bootloader-update.service.d/migrate-static-grub-config.conf
[Service]
ExecStart=/usr/bin/bootupctl migrate-static-grub-config
```

This will be used on Atomic Desktops & IoT systems to migrate systems to
a static GRUB config before enabling composefs as GRUB curently does not
interact well with it [1].

[1] https://bugzilla.redhat.com/show_bug.cgi?id=2308594

See: https://gitlab.com/fedora/ostree/sig/-/issues/35
See: https://pagure.io/workstation-ostree-config/pull-request/591
See: https://fedoraproject.org/wiki/Changes/ComposefsAtomicDesktops
Fixes: #789
  • Loading branch information
travier committed Jan 27, 2025
1 parent d94820b commit c4eaadb
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 1 deletion.
152 changes: 151 additions & 1 deletion src/bootupd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedSt
use crate::util;
use anyhow::{anyhow, Context, Result};
use clap::crate_version;
use fn_error_context::context;
use libc::mode_t;
use libc::{S_IRGRP, S_IROTH, S_IRUSR, S_IWUSR};
use openat_ext::OpenatDirExt;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
use std::fs::{self, File};
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};

pub(crate) enum ConfigMode {
None,
Expand Down Expand Up @@ -489,6 +495,150 @@ pub(crate) fn client_run_validate() -> Result<()> {
Ok(())
}

#[context("Migrating to a static GRUB config")]
pub(crate) fn client_run_migrate_static_grub_config() -> Result<()> {
// Did we already complete the migration?
let mut cmd = std::process::Command::new("ostree");
let result = cmd
.args([
"config",
"--repo=/sysroot/ostree/repo",
"get",
"sysroot.bootloader",
])
.output()
.context("Querying ostree sysroot.bootloader")?;
if !result.status.success() {
// ostree will exit with a non zero return code if the key does not exists
println!("ostree repo 'sysroot.bootloader' config option is not set yet");
} else {
let res = String::from_utf8(result.stdout)
.with_context(|| "decoding as UTF-8 output of ostree command")?;
let bootloader = res.trim_end();
if bootloader == "none" {
println!("Already using a static GRUB config");
return Ok(());
}
println!(
"ostree repo 'sysroot.bootloader' config option is currently set to: '{}'",
bootloader
);
}

// Remount /boot read write just for this unit (we are called in a slave mount namespace by systemd)
ensure_writable_boot()?;

let grub_config_dir = PathBuf::from("/boot/grub2");
let dirfd = openat::Dir::open(&grub_config_dir).context("Opening /boot/grub2")?;

// We mark the bootloader as BLS capable to disable the ostree-grub2 logic.
// We can do that as we know that we are run after the bootloader has been
// updated and all recent GRUB2 versions support reading BLS configs.
// Ignore errors as this is not critical. This is a safety net if a user
// manually overwrites the (soon) static GRUB config by calling `grub2-mkconfig`.
// We need this until we can rely on ostree-grub2 being removed from the image.
println!("Marking bootloader as BLS capable...");
_ = File::create("/boot/grub2/.grub2-blscfg-supported");

// Migrate /boot/grub2/grub.cfg to a static GRUB config if it is a symlink
let grub_config_filename = PathBuf::from("/boot/grub2/grub.cfg");
match dirfd.read_link("grub.cfg") {
Err(_) => {
println!(
"'{}' is not a symlink, nothing to migrate",
grub_config_filename.display()
);
}
Ok(path) => {
println!("Migrating to a static GRUB config...");

// Resolve symlink location
let mut current_config = grub_config_dir.clone();
current_config.push(path);

// Backup the current GRUB config which is hopefully working right now
let backup_config = PathBuf::from("/boot/grub2/grub.cfg.backup");
println!(
"Creating a backup of the current GRUB config '{}' in '{}'...",
current_config.display(),
backup_config.display()
);
fs::copy(&current_config, &backup_config).context("Failed to backup GRUB config")?;

// Read the current config, strip the ostree generated GRUB entries and
// write the result to a temporary file
println!("Stripping ostree generated entries from GRUB config...");
let current_config_file =
File::open(current_config).context("Could not open current GRUB config")?;
let stripped_config = String::from("grub.cfg.stripped");
// mode = -rw-r--r-- (644)
let mut writer = BufWriter::new(
dirfd
.write_file(
&stripped_config,
(S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) as mode_t,
)
.context("Failed to open temporary GRUB config")?,
);
let mut skip = false;
for line in BufReader::new(current_config_file).lines() {
let line = line.context("Failed to read line from GRUB config")?;
if line == "### END /etc/grub.d/15_ostree ###" {
skip = false;
}
if skip {
continue;
}
if line == "### BEGIN /etc/grub.d/15_ostree ###" {
skip = true;
}
writer
.write_all(&line.as_bytes())
.context("Failed to write stripped GRUB config")?;
writer
.write_all(b"\n")
.context("Failed to write stripped GRUB config")?;
}
writer
.flush()
.context("Failed to write stripped GRUB config")?;

// Sync changes to the filesystem (ignore failures)
let _ = dirfd.syncfs();

// Atomically exchange the configs
dirfd
.local_exchange(&stripped_config, "grub.cfg")
.context("Failed to exchange symlink with current GRUB config")?;

// Sync changes to the filesystem (ignore failures)
let _ = dirfd.syncfs();

println!("GRUB config symlink successfully replaced with the current config");

// Remove the now unused symlink (optional cleanup, ignore any failures)
_ = dirfd.remove_file(&stripped_config);
}
};

println!("Setting 'sysroot.bootloader' to 'none' in ostree repo config...");
let status = std::process::Command::new("ostree")
.args([
"config",
"--repo=/sysroot/ostree/repo",
"set",
"sysroot.bootloader",
"none",
])
.status()?;
if !status.success() {
anyhow::bail!("Failed to set 'sysroot.bootloader' to 'none' in ostree repo config");
}

println!("Static GRUB config migration completed successfully");
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
13 changes: 13 additions & 0 deletions src/cli/bootupctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ pub enum CtlVerb {
AdoptAndUpdate,
#[clap(name = "validate", about = "Validate system state")]
Validate,
#[clap(
name = "migrate-static-grub-config",
hide = true,
about = "Migrate a system to a static GRUB config"
)]
MigrateStaticGrubConfig,
}

#[derive(Debug, Parser)]
Expand Down Expand Up @@ -96,6 +102,7 @@ impl CtlCommand {
CtlVerb::Backend(CtlBackend::Install(opts)) => {
super::bootupd::DCommand::run_install(opts)
}
CtlVerb::MigrateStaticGrubConfig => Self::run_migrate_static_grub_config(),
}
}

Expand Down Expand Up @@ -136,6 +143,12 @@ impl CtlCommand {
ensure_running_in_systemd()?;
bootupd::client_run_validate()
}

/// Runner for `migrate-static-grub-config` verb.
fn run_migrate_static_grub_config() -> Result<()> {
ensure_running_in_systemd()?;
bootupd::client_run_migrate_static_grub_config()
}
}

/// Checks if the current process is (apparently at least)
Expand Down

0 comments on commit c4eaadb

Please sign in to comment.