Skip to content

Commit 6b37831

Browse files
committed
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
1 parent e891ea3 commit 6b37831

File tree

8 files changed

+177
-13
lines changed

8 files changed

+177
-13
lines changed

src/bios.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use anyhow::{bail, Result};
2+
use camino::Utf8PathBuf;
23
#[cfg(target_arch = "powerpc64")]
34
use std::borrow::Cow;
45
use std::io::prelude::*;
@@ -8,6 +9,7 @@ use std::process::Command;
89
use crate::blockdev;
910
use crate::bootupd::RootContext;
1011
use crate::component::*;
12+
use crate::grubconfigs;
1113
use crate::model::*;
1214
use crate::packagesystem;
1315

@@ -157,10 +159,66 @@ impl Component for Bios {
157159
crate::component::query_adopt_state()
158160
}
159161

162+
// Backup the current grub.cfg and install new static config
163+
// - Backup "/boot/loader/grub.cfg" to "/boot/grub2/grub.cfg.bak"
164+
// - Remove symlink "/boot/grub2/grub.cfg"
165+
// - Install new static grub.cfg
166+
// - Remove old "/boot/loader/grub.cfg"
167+
fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> {
168+
use anyhow::Context;
169+
use openat_ext::OpenatDirExt;
170+
171+
let grub_config_dir = Utf8PathBuf::from(sysroot_path).join("boot/grub2");
172+
let dirfd = destdir
173+
.sub_dir(grub_config_dir.as_std_path())
174+
.context("Opening /boot/grub2")?;
175+
176+
let grubconfig = grub_config_dir.join(grubconfigs::GRUBCONFIG);
177+
178+
// On BIOS, /boot/grub2/grub.cfg is symlink to /boot/loader/grub.cfg,
179+
// should backup it to /boot/grub2/grub.cfg.bak
180+
if !grubconfig.exists() {
181+
anyhow::bail!("Not found '{}'", grubconfig);
182+
} else if grubconfig.is_symlink() {
183+
let realconfig = dirfd.read_link(grubconfigs::GRUBCONFIG)?;
184+
let realconfig =
185+
Utf8PathBuf::from_path_buf(realconfig).expect("Path should be valid UTF-8");
186+
// Resolve symlink location
187+
let mut current_config = grub_config_dir.clone();
188+
current_config.push(realconfig);
189+
let backup_config = grub_config_dir.join(grubconfigs::BACKUP);
190+
191+
// Backup the current GRUB config which is hopefully working right now
192+
println!(
193+
"Creating a backup of the current GRUB config '{}' in '{}'...",
194+
current_config, backup_config
195+
);
196+
std::fs::copy(&current_config, &backup_config)
197+
.context("Failed to backup GRUB config")?;
198+
// Remove the symlink (on BIOS)
199+
dirfd.remove_file_optional(grubconfigs::GRUBCONFIG)?;
200+
crate::grubconfigs::install(&destdir, None, false)?;
201+
} else {
202+
anyhow::bail!("'{}' is not a symlink", grubconfig);
203+
}
204+
205+
// Remove the unused file, backuped as /boot/grub2/grub.cfg.bak
206+
destdir.remove_file_optional("boot/loader/grub.cfg")?;
207+
208+
// Create the stamp file
209+
dirfd
210+
.write_file(".grub2-static-migrated", 0o644)
211+
.context("Creating stamp file")?;
212+
// Synchronize the filesystem containing /boot/grub2 to disk.
213+
let _ = dirfd.syncfs();
214+
Ok(())
215+
}
216+
160217
fn adopt_update(
161218
&self,
162219
rootcxt: &RootContext,
163220
update: &ContentMetadata,
221+
with_static_config: bool,
164222
) -> Result<Option<InstalledContent>> {
165223
let bios_devices = blockdev::find_colocated_bios_boot(&rootcxt.devices)?;
166224
let Some(meta) = self.query_adopt(&bios_devices)? else {
@@ -177,8 +235,28 @@ impl Component for Bios {
177235
"Found multiple parent devices {parent} and {next}; not currently supported"
178236
);
179237
}
180-
self.run_grub_install(rootcxt.path.as_str(), &parent)?;
238+
239+
let root_path = rootcxt.path.as_str();
240+
self.run_grub_install(root_path, &parent)?;
181241
log::debug!("Installed grub modules on {parent}");
242+
243+
if with_static_config {
244+
// Installs the static config if the OSTree bootloader is either not set
245+
// or is set to a value other than `none`.
246+
if let Some(bootloader) = crate::ostreeutil::get_ostree_bootloader()? {
247+
if bootloader != "none" {
248+
println!(
249+
"ostree repo 'sysroot.bootloader' config option is: '{}'",
250+
bootloader
251+
);
252+
self.migrate_static_grub_config(root_path, &rootcxt.sysroot)?;
253+
} else {
254+
println!(
255+
"Already using a static GRUB config, skipped adopting the static config"
256+
);
257+
}
258+
}
259+
}
182260
Ok(Some(InstalledContent {
183261
meta: update.clone(),
184262
filetree: None,

src/bootupd.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ pub(crate) fn update(name: &str, rootcxt: &RootContext) -> Result<ComponentUpdat
266266
pub(crate) fn adopt_and_update(
267267
name: &str,
268268
rootcxt: &RootContext,
269+
with_static_config: bool,
269270
) -> Result<Option<ContentMetadata>> {
270271
let sysroot = &rootcxt.sysroot;
271272
let mut state = SavedState::load_from_disk("/")?.unwrap_or_default();
@@ -279,15 +280,25 @@ pub(crate) fn adopt_and_update(
279280
let Some(update) = component.query_update(sysroot)? else {
280281
anyhow::bail!("Component {} has no available update", name);
281282
};
283+
282284
let sysroot = sysroot.try_clone()?;
283285
let mut state_guard =
284286
SavedState::acquire_write_lock(sysroot).context("Failed to acquire write lock")?;
285287

286288
let inst = component
287-
.adopt_update(&rootcxt, &update)
289+
.adopt_update(&rootcxt, &update, with_static_config)
288290
.context("Failed adopt and update")?;
289291
if let Some(inst) = inst {
290292
state.installed.insert(component.name().into(), inst);
293+
// Set static_configs metadata and save
294+
if with_static_config {
295+
let meta = get_static_config_meta()?;
296+
state.static_configs = Some(meta);
297+
// Set bootloader to none
298+
ostreeutil::set_ostree_bootloader("none")?;
299+
300+
println!("Static GRUB configuration has been adopted successfully.");
301+
}
291302
state_guard.update_state(&state)?;
292303
return Ok(Some(update));
293304
} else {
@@ -505,7 +516,7 @@ pub(crate) fn client_run_update() -> Result<()> {
505516
}
506517
for (name, adoptable) in status.adoptable.iter() {
507518
if adoptable.confident {
508-
if let Some(r) = adopt_and_update(name, &rootcxt)? {
519+
if let Some(r) = adopt_and_update(name, &rootcxt, false)? {
509520
println!("Adopted and updated: {}: {}", name, r.version);
510521
updated = true;
511522
}
@@ -519,14 +530,14 @@ pub(crate) fn client_run_update() -> Result<()> {
519530
Ok(())
520531
}
521532

522-
pub(crate) fn client_run_adopt_and_update() -> Result<()> {
533+
pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> {
523534
let rootcxt = prep_before_update()?;
524535
let status: Status = status()?;
525536
if status.adoptable.is_empty() {
526537
println!("No components are adoptable.");
527538
} else {
528539
for (name, _) in status.adoptable.iter() {
529-
if let Some(r) = adopt_and_update(name, &rootcxt)? {
540+
if let Some(r) = adopt_and_update(name, &rootcxt, with_static_config)? {
530541
println!("Adopted and updated: {}: {}", name, r.version);
531542
}
532543
}

src/cli/bootupctl.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub enum CtlVerb {
5656
#[clap(name = "update", about = "Update all components")]
5757
Update,
5858
#[clap(name = "adopt-and-update", about = "Update all adoptable components")]
59-
AdoptAndUpdate,
59+
AdoptAndUpdate(AdoptAndUpdateOpts),
6060
#[clap(name = "validate", about = "Validate system state")]
6161
Validate,
6262
#[clap(
@@ -88,13 +88,20 @@ pub struct StatusOpts {
8888
json: bool,
8989
}
9090

91+
#[derive(Debug, Parser)]
92+
pub struct AdoptAndUpdateOpts {
93+
/// Install the static GRUB config files
94+
#[clap(long, action)]
95+
with_static_config: bool,
96+
}
97+
9198
impl CtlCommand {
9299
/// Run CLI application.
93100
pub fn run(self) -> Result<()> {
94101
match self.cmd {
95102
CtlVerb::Status(opts) => Self::run_status(opts),
96103
CtlVerb::Update => Self::run_update(),
97-
CtlVerb::AdoptAndUpdate => Self::run_adopt_and_update(),
104+
CtlVerb::AdoptAndUpdate(opts) => Self::run_adopt_and_update(opts),
98105
CtlVerb::Validate => Self::run_validate(),
99106
CtlVerb::Backend(CtlBackend::Generate(opts)) => {
100107
super::bootupd::DCommand::run_generate_meta(opts)
@@ -133,9 +140,9 @@ impl CtlCommand {
133140
}
134141

135142
/// Runner for `update` verb.
136-
fn run_adopt_and_update() -> Result<()> {
143+
fn run_adopt_and_update(opts: AdoptAndUpdateOpts) -> Result<()> {
137144
ensure_running_in_systemd()?;
138-
bootupd::client_run_adopt_and_update()
145+
bootupd::client_run_adopt_and_update(opts.with_static_config)
139146
}
140147

141148
/// Runner for `validate` verb.

src/component.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,15 @@ pub(crate) trait Component {
3131
/// and "synthesize" content metadata from it.
3232
fn query_adopt(&self, devices: &Option<Vec<String>>) -> Result<Option<Adoptable>>;
3333

34+
// Backup the current grub config, and install static grub config from tree
35+
fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()>;
36+
3437
/// Given an adoptable system and an update, perform the update.
3538
fn adopt_update(
3639
&self,
3740
rootcxt: &RootContext,
3841
update: &ContentMetadata,
42+
with_static_config: bool,
3943
) -> Result<Option<InstalledContent>>;
4044

4145
/// Implementation of `bootupd install` for a given component. This should

src/efi.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::bootupd::RootContext;
2323
use crate::model::*;
2424
use crate::ostreeutil;
2525
use crate::util::{self, CommandRunExt};
26-
use crate::{blockdev, filetree};
26+
use crate::{blockdev, filetree, grubconfigs};
2727
use crate::{component::*, packagesystem};
2828

2929
/// Well-known paths to the ESP that may have been mounted external to us.
@@ -245,11 +245,48 @@ impl Component for Efi {
245245
crate::component::query_adopt_state()
246246
}
247247

248+
// Backup "/boot/efi/EFI/{vendor}/grub.cfg" to "/boot/efi/EFI/{vendor}/grub.cfg.bak"
249+
// Install new static grub.cfg
250+
fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> {
251+
// On EFI, the current config is /boot/efi/EFI/{vendor}/grub.cfg
252+
let sysroot =
253+
openat::Dir::open(sysroot_path).with_context(|| format!("Opening {sysroot_path}"))?;
254+
let Some(vendor) = self.get_efi_vendor(&sysroot)? else {
255+
anyhow::bail!("Failed to find efi vendor");
256+
};
257+
258+
// destdir is /boot/efi/EFI
259+
let efidir = destdir
260+
.sub_dir(&vendor)
261+
.with_context(|| format!("Opening EFI/{}", vendor))?;
262+
println!("Creating a backup of the current GRUB config on EFI");
263+
efidir
264+
.copy_file(grubconfigs::GRUBCONFIG, grubconfigs::BACKUP)
265+
.context("Backuping grub.cfg")?;
266+
grubconfigs::install(&sysroot, Some(&vendor), false)?;
267+
// Synchronize the filesystem containing /boot/efi/EFI/{vendor} to disk.
268+
// (ignore failures)
269+
let _ = efidir.syncfs();
270+
271+
let dirfd = sysroot
272+
.sub_dir("boot/grub2")
273+
.context("Opening /boot/grub2")?;
274+
// Create the stamp file
275+
dirfd
276+
.write_file(".grub2-static-migrated", 0o644)
277+
.context("Creating stamp file")?;
278+
// Synchronize the filesystem containing /boot/grub2 to disk.
279+
let _ = dirfd.syncfs();
280+
281+
Ok(())
282+
}
283+
248284
/// Given an adoptable system and an update, perform the update.
249285
fn adopt_update(
250286
&self,
251287
rootcxt: &RootContext,
252288
updatemeta: &ContentMetadata,
289+
with_static_config: bool,
253290
) -> Result<Option<InstalledContent>> {
254291
let esp_devices = blockdev::find_colocated_esps(&rootcxt.devices)?;
255292
let Some(meta) = self.query_adopt(&esp_devices)? else {
@@ -282,6 +319,25 @@ impl Component for Efi {
282319
log::trace!("applying adoption diff: {}", &diff);
283320
filetree::apply_diff(&updated, &destdir, &diff, None)
284321
.context("applying filesystem changes")?;
322+
323+
// Backup current config and install static config
324+
if with_static_config {
325+
// Installs the static config if the OSTree bootloader is either not set
326+
// or is set to a value other than `none`.
327+
if let Some(bootloader) = ostreeutil::get_ostree_bootloader()? {
328+
if bootloader != "none" {
329+
println!(
330+
"ostree repo 'sysroot.bootloader' config option is: '{}'",
331+
bootloader
332+
);
333+
self.migrate_static_grub_config(rootcxt.path.as_str(), destdir)?;
334+
} else {
335+
println!(
336+
"Already using a static GRUB config, skipped adopting the static config"
337+
);
338+
}
339+
}
340+
}
285341
Ok(Some(InstalledContent {
286342
meta: updatemeta.clone(),
287343
filetree: Some(updatef),

src/filetree.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ use openat_ext::OpenatDirExt;
2828
target_arch = "riscv64"
2929
))]
3030
use openssl::hash::{Hasher, MessageDigest};
31+
#[cfg(any(
32+
target_arch = "x86_64",
33+
target_arch = "aarch64",
34+
target_arch = "riscv64"
35+
))]
3136
use rustix::fd::BorrowedFd;
3237
use serde::{Deserialize, Serialize};
3338
#[allow(unused_imports)]
@@ -39,8 +44,6 @@ use std::fmt::Display;
3944
target_arch = "riscv64"
4045
))]
4146
use std::os::unix::io::AsRawFd;
42-
use std::os::unix::process::CommandExt;
43-
use std::process::Command;
4447

4548
/// The prefix we apply to our temporary files.
4649
#[cfg(any(
@@ -355,6 +358,8 @@ pub(crate) fn syncfs(d: &openat::Dir) -> Result<()> {
355358
))]
356359
fn copy_dir(root: &openat::Dir, src: &str, dst: &str) -> Result<()> {
357360
use bootc_utils::CommandRunExt;
361+
use std::os::unix::process::CommandExt;
362+
use std::process::Command;
358363

359364
let rootfd = unsafe { BorrowedFd::borrow_raw(root.as_raw_fd()) };
360365
unsafe {

src/grubconfigs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use openat_ext::OpenatDirExt;
1111
const GRUB2DIR: &str = "grub2";
1212
const CONFIGDIR: &str = "/usr/lib/bootupd/grub2-static";
1313
const DROPINDIR: &str = "configs.d";
14+
// The related grub files
1415
const GRUBENV: &str = "grubenv";
16+
pub(crate) const GRUBCONFIG: &str = "grub.cfg";
17+
pub(crate) const BACKUP: &str = "grub.cfg.backup";
1518

1619
/// Install the static GRUB config files.
1720
#[context("Installing static GRUB configs")]

src/ostreeutil.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ pub(crate) fn get_ostree_bootloader() -> Result<Option<String>> {
7878
.context("Querying ostree sysroot.bootloader")?;
7979
if !result.status.success() {
8080
// ostree will exit with a none zero return code if the key does not exists
81-
return Ok(None);
81+
return Ok(Some(String::from("")));
8282
} else {
8383
let res = String::from_utf8(result.stdout)
8484
.with_context(|| "decoding as UTF-8 output of ostree command")?;

0 commit comments

Comments
 (0)