Skip to content

Commit 96242b0

Browse files
committed
adopt: add tag to install static GRUB config from tree
And add `bootloader-migrate.service` to migrate a RHCOS system to use a static GRUB config for 4.1/4.2 born RHCOS nodes. Fixes: https://issues.redhat.com/browse/OCPBUGS-52485
1 parent e891ea3 commit 96242b0

File tree

9 files changed

+181
-12
lines changed

9 files changed

+181
-12
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ install-grub-static:
3737
.PHONY: install-systemd-unit
3838
install-systemd-unit:
3939
install -m 644 -D -t "${DESTDIR}$(PREFIX)/lib/systemd/system/" systemd/bootloader-update.service
40+
install -m 644 -D -t "${DESTDIR}$(PREFIX)/lib/systemd/system/" systemd/bootloader-migrate.service
4041

4142
.PHONY: install-all
4243
install-all: install install-grub-static install-systemd-unit

src/bios.rs

Lines changed: 63 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,65 @@ impl Component for Bios {
157159
crate::component::query_adopt_state()
158160
}
159161

162+
// Backup "/boot/loader/grub.cfg" to "/boot/grub2/grub.cfg.bak"
163+
// Remove symlink "/boot/grub2/grub.cfg"
164+
// Install new static grub.cfg
165+
// Remove old "/boot/loader/grub.cfg"
166+
fn migrate_static_grub_config(&self, sysroot_path: &str, destdir: &openat::Dir) -> Result<()> {
167+
use anyhow::Context;
168+
use openat_ext::OpenatDirExt;
169+
170+
let grub_config_dir = Utf8PathBuf::from(sysroot_path).join("boot/grub2");
171+
let dirfd = destdir
172+
.sub_dir(grub_config_dir.as_std_path())
173+
.context("Opening /boot/grub2")?;
174+
175+
let grubconfig = grub_config_dir.join(grubconfigs::GRUBCONFIG);
176+
177+
// On BIOS, /boot/grub2/grub.cfg is symlink to /boot/loader/grub.cfg,
178+
// should backup it to /boot/grub2/grub.cfg.bak
179+
if !grubconfig.exists() {
180+
anyhow::bail!("Not found '{}'", grubconfig);
181+
} else if grubconfig.is_symlink() {
182+
let realconfig = dirfd.read_link(grubconfigs::GRUBCONFIG)?;
183+
let realconfig =
184+
Utf8PathBuf::from_path_buf(realconfig).expect("Path should be valid UTF-8");
185+
// Resolve symlink location
186+
let mut current_config = grub_config_dir.clone();
187+
current_config.push(realconfig);
188+
let backup_config = grub_config_dir.join(grubconfigs::BACKUP);
189+
190+
// Backup the current GRUB config which is hopefully working right now
191+
println!(
192+
"Creating a backup of the current GRUB config '{}' in '{}'...",
193+
current_config, backup_config
194+
);
195+
std::fs::copy(&current_config, &backup_config)
196+
.context("Failed to backup GRUB config")?;
197+
// Remove the symlink (on BIOS)
198+
dirfd.remove_file_optional(grubconfigs::GRUBCONFIG)?;
199+
crate::grubconfigs::install(&destdir, None, false)?;
200+
} else {
201+
anyhow::bail!("'{}' is not a symlink", grubconfig);
202+
}
203+
204+
// Remove the unused file, backuped as /boot/grub2/grub.cfg.bak
205+
destdir.remove_file_optional("boot/loader/grub.cfg")?;
206+
207+
// Create the stamp file
208+
dirfd
209+
.write_file(".grub2-static-migrated", 0o644)
210+
.context("Creating stamp file")?;
211+
// Synchronize the filesystem containing /boot/grub2 to disk.
212+
let _ = dirfd.syncfs();
213+
Ok(())
214+
}
215+
160216
fn adopt_update(
161217
&self,
162218
rootcxt: &RootContext,
163219
update: &ContentMetadata,
220+
with_static_config: bool,
164221
) -> Result<Option<InstalledContent>> {
165222
let bios_devices = blockdev::find_colocated_bios_boot(&rootcxt.devices)?;
166223
let Some(meta) = self.query_adopt(&bios_devices)? else {
@@ -177,8 +234,13 @@ impl Component for Bios {
177234
"Found multiple parent devices {parent} and {next}; not currently supported"
178235
);
179236
}
180-
self.run_grub_install(rootcxt.path.as_str(), &parent)?;
237+
238+
let root_path = rootcxt.path.as_str();
239+
self.run_grub_install(root_path, &parent)?;
181240
log::debug!("Installed grub modules on {parent}");
241+
if with_static_config {
242+
self.migrate_static_grub_config(root_path, &rootcxt.sysroot)?;
243+
}
182244
Ok(Some(InstalledContent {
183245
meta: update.clone(),
184246
filetree: None,

src/bootupd.rs

Lines changed: 30 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+
mut 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,40 @@ 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+
// Installs the static configuration if the OSTree bootloader is either not set
284+
// or is set to a value other than `none`.
285+
if with_static_config {
286+
if let Some(bootloader) = ostreeutil::get_ostree_bootloader()? {
287+
if bootloader == "none" {
288+
println!("Already using a static GRUB config, skipped adopting the static config");
289+
with_static_config = false;
290+
}
291+
println!(
292+
"ostree repo 'sysroot.bootloader' config option is currently set to: '{}'",
293+
bootloader
294+
);
295+
} else {
296+
println!("ostree repo 'sysroot.bootloader' config option is not set yet");
297+
}
298+
}
282299
let sysroot = sysroot.try_clone()?;
283300
let mut state_guard =
284301
SavedState::acquire_write_lock(sysroot).context("Failed to acquire write lock")?;
285302

286303
let inst = component
287-
.adopt_update(&rootcxt, &update)
304+
.adopt_update(&rootcxt, &update, with_static_config)
288305
.context("Failed adopt and update")?;
289306
if let Some(inst) = inst {
290307
state.installed.insert(component.name().into(), inst);
308+
// Set static_configs metadata and save
309+
if with_static_config {
310+
let meta = get_static_config_meta()?;
311+
state.static_configs = Some(meta);
312+
// Set bootloader to none
313+
ostreeutil::set_ostree_bootloader("none")?;
314+
315+
println!("Static GRUB configuration has been adopted successfully.");
316+
}
291317
state_guard.update_state(&state)?;
292318
return Ok(Some(update));
293319
} else {
@@ -505,7 +531,7 @@ pub(crate) fn client_run_update() -> Result<()> {
505531
}
506532
for (name, adoptable) in status.adoptable.iter() {
507533
if adoptable.confident {
508-
if let Some(r) = adopt_and_update(name, &rootcxt)? {
534+
if let Some(r) = adopt_and_update(name, &rootcxt, false)? {
509535
println!("Adopted and updated: {}: {}", name, r.version);
510536
updated = true;
511537
}
@@ -519,14 +545,14 @@ pub(crate) fn client_run_update() -> Result<()> {
519545
Ok(())
520546
}
521547

522-
pub(crate) fn client_run_adopt_and_update() -> Result<()> {
548+
pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> {
523549
let rootcxt = prep_before_update()?;
524550
let status: Status = status()?;
525551
if status.adoptable.is_empty() {
526552
println!("No components are adoptable.");
527553
} else {
528554
for (name, _) in status.adoptable.iter() {
529-
if let Some(r) = adopt_and_update(name, &rootcxt)? {
555+
if let Some(r) = adopt_and_update(name, &rootcxt, with_static_config)? {
530556
println!("Adopted and updated: {}: {}", name, r.version);
531557
}
532558
}

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: 43 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,11 @@ 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+
self.migrate_static_grub_config(rootcxt.path.as_str(), destdir)?;
326+
}
285327
Ok(Some(InstalledContent {
286328
meta: updatemeta.clone(),
287329
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")]

systemd/bootloader-migrate.service

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[Unit]
2+
# e.g. OpenShift RHCOS 4.1/4.2 nodes
3+
Description=Static GRUB config migration
4+
ConditionArchitecture=!s390x
5+
ConditionPathExists=!/boot/grub2/.grub2-static-migrated
6+
RequiresMountsFor=/sysroot /boot
7+
8+
[Service]
9+
Type=oneshot
10+
ExecStart=/usr/bin/bootupctl adopt-and-update --with-static-config
11+
RemainAfterExit=yes
12+
# Keep this stuff in sync with SYSTEMD_ARGS_BOOTUPD in general
13+
PrivateNetwork=yes
14+
ProtectHome=yes
15+
KillMode=mixed
16+
MountFlags=slave
17+
18+
[Install]
19+
WantedBy=multi-user.target

0 commit comments

Comments
 (0)