From 3bcae6b37fbaa92fe9bc58ba2ea649e9610b4a92 Mon Sep 17 00:00:00 2001 From: Huijing Hei Date: Thu, 26 Jun 2025 21:43:02 +0800 Subject: [PATCH 3/3] adopt: add tag to install static GRUB config from tree Add `bootupctl adopt-and-update --with-static-config` to migrate RHCOS system to use static GRUB config, for example 4.1/4.2 born RHCOS nodes. Fixes: https://issues.redhat.com/browse/OCPBUGS-52485 (cherry picked from commit 52ff0f987501a31951931add7bb8312b8934c3b0) --- src/bios.rs | 82 ++++++++++++++++++++++++++++++++++++++++++-- src/bootupd.rs | 19 +++++++--- src/cli/bootupctl.rs | 15 +++++--- src/component.rs | 4 +++ src/efi.rs | 49 ++++++++++++++++++++++++-- src/grubconfigs.rs | 5 +++ 6 files changed, 161 insertions(+), 13 deletions(-) diff --git a/src/bios.rs b/src/bios.rs index 6e528b8..0a65dcf 100644 --- a/src/bios.rs +++ b/src/bios.rs @@ -1,4 +1,6 @@ -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; +use camino::Utf8PathBuf; +use openat_ext::OpenatDirExt; #[cfg(target_arch = "powerpc64")] use std::borrow::Cow; use std::io::prelude::*; @@ -8,6 +10,7 @@ use std::process::Command; use crate::blockdev; use crate::component::*; +use crate::grubconfigs; use crate::model::*; use crate::packagesystem; @@ -156,7 +159,71 @@ impl Component for Bios { crate::component::query_adopt_state() } - fn adopt_update(&self, _: &openat::Dir, update: &ContentMetadata) -> Result { + // Backup the current grub.cfg and replace with new static config + // - Backup "/boot/loader/grub.cfg" to "/boot/grub2/grub.cfg.bak" + // - Remove symlink "/boot/grub2/grub.cfg" + // - Replace "/boot/grub2/grub.cfg" symlink with new static "grub.cfg" + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> { + let grub = "boot/grub2"; + // sysroot_path is /, destdir is Dir of / + let grub_config_path = Utf8PathBuf::from(sysroot_path).join(grub); + let grub_config_dir = destdir.sub_dir(grub).context("Opening boot/grub2")?; + + let grub_config = grub_config_path.join(grubconfigs::GRUBCONFIG); + + if !grub_config.exists() { + anyhow::bail!("Could not find '{}'", grub_config); + } + + let mut current_config; + // If /boot/grub2/grub.cfg is not symlink, we need to keep going + if !grub_config.is_symlink() { + println!("'{}' is not a symlink", grub_config); + current_config = grub_config.clone(); + } else { + // If /boot/grub2/grub.cfg is symlink to /boot/loader/grub.cfg, + // backup it to /boot/grub2/grub.cfg.bak + // Get real file for symlink /boot/grub2/grub.cfg + let real_config = grub_config_dir.read_link(grubconfigs::GRUBCONFIG)?; + let real_config = + Utf8PathBuf::from_path_buf(real_config).expect("Path should be valid UTF-8"); + // Resolve symlink location + current_config = grub_config_path.clone(); + current_config.push(real_config); + } + + let backup_config = grub_config_path.join(grubconfigs::GRUBCONFIG_BACKUP); + if !backup_config.exists() { + // Backup the current GRUB config which is hopefully working right now + println!( + "Creating a backup of the current GRUB config '{}' in '{}'...", + current_config, backup_config + ); + std::fs::copy(¤t_config, &backup_config) + .context("Failed to backup GRUB config")?; + } + + crate::grubconfigs::install(&destdir, None, true)?; + + // Remove the real config if it is symlink and will not + // if /boot/grub2/grub.cfg is file + if current_config != grub_config { + println!("Removing {}", current_config); + grub_config_dir.remove_file_optional(current_config.as_std_path())?; + } + + // Synchronize the filesystem containing /boot/grub2 to disk. + let _ = grub_config_dir.syncfs(); + + Ok(()) + } + + fn adopt_update( + &self, + _: &openat::Dir, + update: &ContentMetadata, + with_static_config: bool, + ) -> Result { let Some(meta) = self.query_adopt()? else { anyhow::bail!("Failed to find adoptable system") }; @@ -165,6 +232,17 @@ impl Component for Bios { let device = blockdev::get_single_device(&target_root)?; self.run_grub_install(target_root, &device)?; log::debug!("Install grub modules on {device}"); + if with_static_config { + // Install the static config if the OSTree bootloader is not set. + if let Some(bootloader) = crate::ostreeutil::get_ostree_bootloader()? { + println!( + "ostree repo 'sysroot.bootloader' config option is currently set to: '{bootloader}'", + ); + } else { + println!("ostree repo 'sysroot.bootloader' config option is not set yet"); + self.migrate_static_grub_config(target_root, &openat::Dir::open(target_root)?)?; + }; + } Ok(InstalledContent { meta: update.clone(), filetree: None, diff --git a/src/bootupd.rs b/src/bootupd.rs index 1213654..0e0a00e 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -256,7 +256,7 @@ pub(crate) fn update(name: &str) -> Result { } /// daemon implementation of component adoption -pub(crate) fn adopt_and_update(name: &str) -> Result { +pub(crate) fn adopt_and_update(name: &str, with_static_config: bool) -> Result { let sysroot = openat::Dir::open("/")?; let mut state = SavedState::load_from_disk("/")?.unwrap_or_default(); let component = component::new_from_name(name)?; @@ -273,10 +273,19 @@ pub(crate) fn adopt_and_update(name: &str) -> Result { SavedState::acquire_write_lock(sysroot).context("Failed to acquire write lock")?; let inst = component - .adopt_update(&state_guard.sysroot, &update) + .adopt_update(&state_guard.sysroot, &update, with_static_config) .context("Failed adopt and update")?; state.installed.insert(component.name().into(), inst); + // Set static_configs metadata and save + if with_static_config && state.static_configs.is_none() { + let meta = get_static_config_meta()?; + state.static_configs = Some(meta); + // Set bootloader to none + crate::ostreeutil::set_ostree_bootloader("none")?; + + println!("Static GRUB configuration has been adopted successfully."); + } state_guard.update_state(&state)?; Ok(update) } @@ -445,7 +454,7 @@ pub(crate) fn client_run_update() -> Result<()> { } for (name, adoptable) in status.adoptable.iter() { if adoptable.confident { - let r: ContentMetadata = adopt_and_update(name)?; + let r: ContentMetadata = adopt_and_update(name, false)?; println!("Adopted and updated: {}: {}", name, r.version); updated = true; } else { @@ -458,13 +467,13 @@ pub(crate) fn client_run_update() -> Result<()> { Ok(()) } -pub(crate) fn client_run_adopt_and_update() -> Result<()> { +pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> { let status: Status = status()?; if status.adoptable.is_empty() { println!("No components are adoptable."); } else { for (name, _) in status.adoptable.iter() { - let r: ContentMetadata = adopt_and_update(name)?; + let r: ContentMetadata = adopt_and_update(name, with_static_config)?; println!("Adopted and updated: {}: {}", name, r.version); } } diff --git a/src/cli/bootupctl.rs b/src/cli/bootupctl.rs index 0b79054..4e831fe 100644 --- a/src/cli/bootupctl.rs +++ b/src/cli/bootupctl.rs @@ -56,7 +56,7 @@ pub enum CtlVerb { #[clap(name = "update", about = "Update all components")] Update, #[clap(name = "adopt-and-update", about = "Update all adoptable components")] - AdoptAndUpdate, + AdoptAndUpdate(AdoptAndUpdateOpts), #[clap(name = "validate", about = "Validate system state")] Validate, #[clap( @@ -88,13 +88,20 @@ pub struct StatusOpts { json: bool, } +#[derive(Debug, Parser)] +pub struct AdoptAndUpdateOpts { + /// Install the static GRUB config files + #[clap(long, action)] + with_static_config: bool, +} + impl CtlCommand { /// Run CLI application. pub fn run(self) -> Result<()> { match self.cmd { CtlVerb::Status(opts) => Self::run_status(opts), CtlVerb::Update => Self::run_update(), - CtlVerb::AdoptAndUpdate => Self::run_adopt_and_update(), + CtlVerb::AdoptAndUpdate(opts) => Self::run_adopt_and_update(opts), CtlVerb::Validate => Self::run_validate(), CtlVerb::Backend(CtlBackend::Generate(opts)) => { super::bootupd::DCommand::run_generate_meta(opts) @@ -133,9 +140,9 @@ impl CtlCommand { } /// Runner for `update` verb. - fn run_adopt_and_update() -> Result<()> { + fn run_adopt_and_update(opts: AdoptAndUpdateOpts) -> Result<()> { ensure_running_in_systemd()?; - bootupd::client_run_adopt_and_update() + bootupd::client_run_adopt_and_update(opts.with_static_config) } /// Runner for `validate` verb. diff --git a/src/component.rs b/src/component.rs index dfff20d..cb3b355 100644 --- a/src/component.rs +++ b/src/component.rs @@ -31,11 +31,15 @@ pub(crate) trait Component { /// and "synthesize" content metadata from it. fn query_adopt(&self) -> Result>; + // Backup the current grub config, and install static grub config from tree + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()>; + /// Given an adoptable system and an update, perform the update. fn adopt_update( &self, sysroot: &openat::Dir, update: &ContentMetadata, + with_static_config: bool, ) -> Result; /// Implementation of `bootupd install` for a given component. This should diff --git a/src/efi.rs b/src/efi.rs index f7a0daa..bd94868 100644 --- a/src/efi.rs +++ b/src/efi.rs @@ -20,9 +20,10 @@ use walkdir::WalkDir; use widestring::U16CString; use crate::filetree; +use crate::grubconfigs; use crate::model::*; use crate::ostreeutil; -use crate::util::{self, CommandRunExt}; +use crate::util::*; use crate::{component::*, packagesystem}; /// Well-known paths to the ESP that may have been mounted external to us. @@ -108,7 +109,7 @@ impl Efi { if st.f_type != libc::MSDOS_SUPER_MAGIC { continue; } - util::ensure_writable_mount(&mnt)?; + ensure_writable_mount(&mnt)?; log::debug!("Reusing existing {mnt:?}"); return Ok(mnt); } @@ -257,11 +258,41 @@ impl Component for Efi { crate::component::query_adopt_state() } + // Backup "/boot/efi/EFI/{vendor}/grub.cfg" to "/boot/efi/EFI/{vendor}/grub.cfg.bak" + // Replace "/boot/efi/EFI/{vendor}/grub.cfg" with new static "grub.cfg" + fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> { + let sysroot = + openat::Dir::open(sysroot_path).with_context(|| format!("Opening {sysroot_path}"))?; + let Some(vendor) = self.get_efi_vendor(&sysroot)? else { + anyhow::bail!("Failed to find efi vendor"); + }; + + // destdir is /boot/efi/EFI + let efidir = destdir + .sub_dir(&vendor) + .with_context(|| format!("Opening EFI/{}", vendor))?; + + if !efidir.exists(grubconfigs::GRUBCONFIG_BACKUP)? { + println!("Creating a backup of the current GRUB config on EFI"); + efidir + .copy_file(grubconfigs::GRUBCONFIG, grubconfigs::GRUBCONFIG_BACKUP) + .context("Failed to backup GRUB config")?; + } + + grubconfigs::install(&sysroot, Some(&vendor), true)?; + // Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk. + // (ignore failures) + let _ = efidir.syncfs(); + + Ok(()) + } + /// Given an adoptable system and an update, perform the update. fn adopt_update( &self, sysroot: &openat::Dir, updatemeta: &ContentMetadata, + with_static_config: bool, ) -> Result { let Some(meta) = self.query_adopt()? else { anyhow::bail!("Failed to find adoptable system") @@ -277,6 +308,20 @@ impl Component for Efi { let diff = updatef.relative_diff_to(&esp)?; log::trace!("applying adoption diff: {}", &diff); filetree::apply_diff(&updated, &esp, &diff, None).context("applying filesystem changes")?; + + // Backup current config and install static config + if with_static_config { + // Install the static config if the OSTree bootloader is not set. + if let Some(bootloader) = crate::ostreeutil::get_ostree_bootloader()? { + println!( + "ostree repo 'sysroot.bootloader' config option is currently set to: '{bootloader}'", + ); + } else { + println!("ostree repo 'sysroot.bootloader' config option is not set yet"); + self.migrate_static_grub_config("/", &esp)?; + }; + } + Ok(InstalledContent { meta: updatemeta.clone(), filetree: Some(updatef), diff --git a/src/grubconfigs.rs b/src/grubconfigs.rs index 09aeebf..f3c70d6 100644 --- a/src/grubconfigs.rs +++ b/src/grubconfigs.rs @@ -9,6 +9,9 @@ use openat_ext::OpenatDirExt; const GRUB2DIR: &str = "grub2"; const CONFIGDIR: &str = "/usr/lib/bootupd/grub2-static"; const DROPINDIR: &str = "configs.d"; +// The related grub files +pub(crate) const GRUBCONFIG: &str = "grub.cfg"; +pub(crate) const GRUBCONFIG_BACKUP: &str = "grub.cfg.backup"; /// Install the static GRUB config files. #[context("Installing static GRUB configs")] @@ -100,6 +103,8 @@ pub(crate) fn install( .copy_file_at(uuid_path, &efidir, target) .context("Writing bootuuid.cfg to efi dir")?; } + } else { + println!("Could not find /boot/efi/EFI when installing {target:?}"); } } -- 2.49.0