Skip to content

Commit 0436363

Browse files
committed
WIP: Import GRUB static migration code
1 parent 942aeb8 commit 0436363

2 files changed

Lines changed: 156 additions & 1 deletion

File tree

src/bootupd.rs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ use crate::model::{ComponentStatus, ComponentUpdatable, ContentMetadata, SavedSt
99
use crate::util;
1010
use anyhow::{anyhow, Context, Result};
1111
use clap::crate_version;
12+
use openat_ext::OpenatDirExt;
13+
use rustix::fs::link;
1214
use serde::{Deserialize, Serialize};
1315
use std::borrow::Cow;
1416
use std::collections::BTreeMap;
15-
use std::path::Path;
17+
use std::fs::{self, File};
18+
use std::path::{Path, PathBuf};
19+
use std::thread::current;
1620

1721
pub(crate) enum ConfigMode {
1822
None,
@@ -489,6 +493,148 @@ pub(crate) fn client_run_validate() -> Result<()> {
489493
Ok(())
490494
}
491495

496+
pub(crate) fn client_run_migrate() -> Result<()> {
497+
// Used to condition execution of this unit at the systemd level
498+
let stamp_file = "/boot/.bootupd-static-migration-complete";
499+
500+
// Did we already complete the migration?
501+
let mut ostree_cmd = std::process::Command::new("ostree");
502+
let result = ostree_cmd
503+
.args([
504+
"config",
505+
"--repo=/sysroot/ostree/repo",
506+
"get",
507+
"sysroot.bootloader",
508+
])
509+
.output()
510+
.with_context(|| "failed to call ostree command. Not performing migration")?;
511+
if !result.status.success() {
512+
// ostree will exit with a non zero return code if the key does not exists
513+
println!("ostree repo 'sysroot.bootloader' config option not set yet.");
514+
} else {
515+
let bootloader = String::from_utf8(result.stdout)
516+
.with_context(|| "decoding as UTF-8 output of ostree command")?;
517+
if bootloader.trim_end() == "none" {
518+
println!("ostree repo 'sysroot.bootloader' config option already set to 'none'.");
519+
println!("Assuming that the migration is already complete.");
520+
File::create(stamp_file)?;
521+
return Ok(());
522+
} else {
523+
println!(
524+
"ostree repo 'sysroot.bootloader' config currently set to: {}",
525+
bootloader.trim_end()
526+
);
527+
}
528+
}
529+
530+
// Remount /boot read write just for this unit (we are called in a slave mount namespace by systemd)
531+
ensure_writable_boot()?;
532+
533+
let grub_config_dir = PathBuf::from("/boot/grub2");
534+
let Ok(dirfd) = openat::Dir::open(&grub_config_dir).with_context(|| "a") else {
535+
anyhow::bail!(
536+
"Could not open {}. Is /boot mounted? Not performing migration.",
537+
grub_config_dir.display()
538+
);
539+
};
540+
541+
// Migrate /boot/grub2/grub.cfg to a static GRUB config if it is a symlink
542+
let grub_config_filename = PathBuf::from("/boot/grub2/grub.cfg");
543+
match dirfd.read_link("grub.cfg") {
544+
Err(_) => {
545+
println!(
546+
"'{}' is not a symlink. Nothing to migrate.",
547+
grub_config_filename.display()
548+
);
549+
}
550+
Ok(path) => {
551+
println!("Migrating to a static GRUB config...");
552+
553+
let mut current_config = grub_config_dir.clone();
554+
current_config.push(path);
555+
let backup_config = PathBuf::from("/boot/grub2/grub.cfg.backup");
556+
let current_config_copy = PathBuf::from("/boot/grub2/grub.cfg.current");
557+
558+
// Backup the current GRUB config which is hopefully working right now
559+
println!(
560+
"Creating a backup of the current GRUB config '{}' in '{}'...",
561+
current_config.display(),
562+
backup_config.display()
563+
);
564+
fs::copy(&current_config, &backup_config).map_err(|e| {
565+
anyhow!(
566+
"Could not copy the current GRUB config: {}. Not performing migration.",
567+
e
568+
)
569+
})?;
570+
571+
// Copy it again alongside the current symlink
572+
fs::copy(&current_config, &current_config_copy).map_err(|e| {
573+
anyhow!(
574+
"Could not copy the current GRUB config: {}. Not performing migration.",
575+
e
576+
)
577+
})?;
578+
579+
// Atomically exchange the configs
580+
dirfd.local_exchange("grub.cfg.current", "grub.cfg").map_err(|e| {
581+
anyhow!(
582+
"Could not exchange the symlink with the current GRUB config: {}. Not performing migration.",
583+
e
584+
)
585+
})?;
586+
587+
// Remove the now unused symlink (optional cleanup, ignore any failures)
588+
dirfd.remove_file("grub.cfg.current").unwrap_or_else(|e| {
589+
println!(
590+
"Could not remove now unused GRUB config symlink: {}. Ignoring error.",
591+
e
592+
)
593+
});
594+
595+
println!("GRUB config symlink successfully replaced with the current config.");
596+
}
597+
};
598+
599+
// If /etc/default/grub exists then we have to force the regeneration of the
600+
// GRUB config to remove the ostree entries that duplicates the BLS ones
601+
let grub_default = PathBuf::from("/etc/default/grub");
602+
if grub_default.exists() {
603+
println!("Marking bootloader as BLS capable...");
604+
File::create("/boot/grub2/.grub2-blscfg-supported")?;
605+
606+
println!("Regenerating GRUB config with only BLS configs...");
607+
// grub2-mkconfig -o /boot/grub2/grub.cfg
608+
let status = std::process::Command::new("grub2-mkconfig")
609+
.arg("-o")
610+
.arg(grub_config_filename)
611+
.status()?;
612+
if !status.success() {
613+
anyhow::bail!("Failed to regenerate the GRUB config");
614+
}
615+
}
616+
617+
println!("Setting up 'sysroot.bootloader' to 'none' in ostree repo config...");
618+
let status = std::process::Command::new("ostree")
619+
.args([
620+
"config",
621+
"--repo=/sysroot/ostree/repo",
622+
"set",
623+
"sysroot.bootloader",
624+
"none",
625+
])
626+
.status()?;
627+
if !status.success() {
628+
anyhow::bail!("Failed to set 'sysroot.bootloader' to 'none' in ostree repo config");
629+
}
630+
631+
// Migration complete, let's write the stamp file
632+
File::create(stamp_file)?;
633+
634+
println!("Static GRUB config migration completed successfully!");
635+
Ok(())
636+
}
637+
492638
#[cfg(test)]
493639
mod tests {
494640
use super::*;

src/cli/bootupctl.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ pub enum CtlVerb {
5858
AdoptAndUpdate,
5959
#[clap(name = "validate", about = "Validate system state")]
6060
Validate,
61+
#[clap(name = "migrate", about = "Migrate a system to static a GRUB config")]
62+
Migrate,
6163
}
6264

6365
#[derive(Debug, Parser)]
@@ -95,6 +97,7 @@ impl CtlCommand {
9597
CtlVerb::Backend(CtlBackend::Install(opts)) => {
9698
super::bootupd::DCommand::run_install(opts)
9799
}
100+
CtlVerb::Migrate => Self::run_migrate(),
98101
}
99102
}
100103

@@ -132,6 +135,12 @@ impl CtlCommand {
132135
ensure_running_in_systemd()?;
133136
bootupd::client_run_validate()
134137
}
138+
139+
/// Runner for `migrate` verb.
140+
fn run_migrate() -> Result<()> {
141+
ensure_running_in_systemd()?;
142+
bootupd::client_run_migrate()
143+
}
135144
}
136145

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

0 commit comments

Comments
 (0)