Skip to content

Commit 0a75768

Browse files
jbtrystramcgwalters
authored andcommitted
install/bootupd: chroot to deployment
When `--src-imgref` is passed, the deployed systemd does not match the running environnement. In this case, let's run bootupd from inside the deployment. This makes sure we are using the binaries shipped in the image (and relevant config files such as grub fragements). We use bwrap to set up the chroot for a easier handling of the API filesystems. We could do that in all cases but i kept it behind the `--src-imgref` option since when using the target container as the buildroot it will have no impact, and we expect this scenario to be the most common. In CoreOS we have a specific test that checks if the bootloader was installed with the `grub2-install` of the image. Fixes #1559 Also see #1455 Assisted-by: OpenCode (Opus 4.5) Signed-off-by: jbtrystram <jbtrystram@redhat.com>
1 parent 216d720 commit 0a75768

6 files changed

Lines changed: 183 additions & 20 deletions

File tree

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ jobs:
9292
# Install tests
9393
sudo bootc-integration-tests install-alongside localhost/bootc-install
9494
95+
# inspect system state after the install tests.
96+
sudo lsblk
97+
sudo mount
98+
9599
# system-reinstall-bootc tests
96100
cargo build --release -p system-reinstall-bootc
97101

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/lib/src/bootloader.rs

Lines changed: 63 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::fs::create_dir_all;
22
use std::process::Command;
33

44
use anyhow::{Context, Result, anyhow, bail};
5-
use bootc_utils::CommandRunExt;
5+
use bootc_utils::{BwrapCmd, CommandRunExt};
66
use camino::Utf8Path;
77
use cap_std_ext::cap_std::fs::Dir;
88
use cap_std_ext::dirext::CapStdExtDirExt;
@@ -91,22 +91,71 @@ pub(crate) fn install_via_bootupd(
9191
// bootc defaults to only targeting the platform boot method.
9292
let bootupd_opts = (!configopts.generic_image).then_some(["--update-firmware", "--auto"]);
9393

94-
let abs_deployment_path = deployment_path.map(|v| rootfs.join(v));
95-
let src_root_arg = if let Some(p) = abs_deployment_path.as_deref() {
96-
vec!["--src-root", p.as_str()]
94+
// When not running inside the target container (through `--src-imgref`) we use
95+
// will bwrap as a chroot to run bootupctl from the deployment.
96+
// This makes sure we use binaries from the target image rather than the buildroot.
97+
// In that case, the target rootfs is replaced with `/` because this is just used by
98+
// bootupd to find the backing device.
99+
let rootfs_mount = if deployment_path.is_none() {
100+
rootfs.as_str()
97101
} else {
98-
vec![]
102+
"/"
99103
};
100-
let devpath = device.path();
104+
101105
println!("Installing bootloader via bootupd");
102-
Command::new("bootupctl")
103-
.args(["backend", "install", "--write-uuid"])
104-
.args(verbose)
105-
.args(bootupd_opts.iter().copied().flatten())
106-
.args(src_root_arg)
107-
.args(["--device", devpath.as_str(), rootfs.as_str()])
108-
.log_debug()
109-
.run_inherited_with_cmd_context()
106+
107+
// Build the bootupctl arguments
108+
let mut bootupd_args: Vec<&str> = vec!["backend", "install", "--write-uuid"];
109+
if let Some(v) = verbose {
110+
bootupd_args.push(v);
111+
}
112+
113+
if let Some(ref opts) = bootupd_opts {
114+
bootupd_args.extend(opts.iter().copied());
115+
}
116+
bootupd_args.extend(["--device", device.path().as_str(), rootfs_mount]);
117+
118+
// Run inside a bwrap container. It takes care of mounting and creating
119+
// the necessary API filesystems in the target deployment and acts as
120+
// a nicer `chroot`.
121+
if let Some(deploy) = deployment_path {
122+
let target_root = rootfs.join(deploy);
123+
let boot_path = rootfs.join("boot");
124+
125+
tracing::debug!("Running bootupctl via bwrap in {}", target_root);
126+
127+
// Prepend "bootupctl" to the args for bwrap
128+
let mut bwrap_args = vec!["bootupctl"];
129+
bwrap_args.extend(bootupd_args);
130+
131+
let mut cmd = BwrapCmd::new(&target_root)
132+
// Bind mount /boot from the physical target root so bootupctl can find
133+
// the boot partition and install the bootloader there
134+
.bind(&boot_path, &"/boot")
135+
// Bind the target block device inside the bwrap container so bootupctl can access it
136+
.bind_device(device.path().as_str());
137+
138+
// Also bind all partitions of the tafet block device
139+
for partition in &device.partitions {
140+
cmd = cmd.bind_device(&partition.node);
141+
}
142+
143+
// The $PATH in the bwrap env is not complete enough for some images
144+
// so we inject a reasonnable default.
145+
// This is causing bootupctl and/or sfdisk binaries
146+
// to be not found with fedora 43.
147+
cmd.setenv(
148+
"PATH",
149+
"/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
150+
)
151+
.run(bwrap_args)
152+
} else {
153+
// Running directly without chroot
154+
Command::new("bootupctl")
155+
.args(&bootupd_args)
156+
.log_debug()
157+
.run_inherited_with_cmd_context()
158+
}
110159
}
111160

112161
#[context("Installing bootloader")]

crates/utils/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ repository = "https://github.com/bootc-dev/bootc"
1010
# Workspace dependencies
1111
anstream = { workspace = true }
1212
anyhow = { workspace = true }
13+
cap-std-ext = {workspace = true, features = ["fs_utf8"] }
1314
chrono = { workspace = true, features = ["std"] }
1415
owo-colors = { workspace = true }
1516
rustix = { workspace = true }

crates/utils/src/bwrap.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/// Builder for running commands inside a target os tree using bubblewrap (bwrap).
2+
use std::borrow::Cow;
3+
use std::ffi::OsStr;
4+
use std::os::fd::AsRawFd;
5+
use std::process::Command;
6+
7+
use anyhow::Result;
8+
use cap_std_ext::camino::{Utf8Path, Utf8PathBuf};
9+
use cap_std_ext::cap_std::fs::Dir;
10+
11+
use crate::CommandRunExt;
12+
13+
/// Builder for running commands inside a target directory using bwrap.
14+
#[derive(Debug)]
15+
pub struct BwrapCmd<'a> {
16+
/// The target directory to use as root for the container
17+
chroot_path: Cow<'a, Utf8Path>,
18+
/// Bind mounts in format (source, target)
19+
bind_mounts: Vec<(&'a str, &'a str)>,
20+
/// Device nodes to bind into the container
21+
devices: Vec<&'a str>,
22+
/// Environment variables to set
23+
env_vars: Vec<(&'a str, &'a str)>,
24+
}
25+
26+
impl<'a> BwrapCmd<'a> {
27+
/// Create a new BwrapCmd builder with a root directory as a File Descriptor.
28+
#[allow(dead_code)]
29+
pub fn new_with_dir(path: &'a Dir) -> Self {
30+
let fd_path: String = format!("/proc/self/fd/{}", path.as_raw_fd());
31+
Self {
32+
chroot_path: Cow::Owned(Utf8PathBuf::from(&fd_path)),
33+
bind_mounts: Vec::new(),
34+
devices: Vec::new(),
35+
env_vars: Vec::new(),
36+
}
37+
}
38+
39+
/// Create a new BwrapCmd builder with a root directory
40+
pub fn new(path: &'a Utf8Path) -> Self {
41+
Self {
42+
chroot_path: Cow::Borrowed(path),
43+
bind_mounts: Vec::new(),
44+
devices: Vec::new(),
45+
env_vars: Vec::new(),
46+
}
47+
}
48+
49+
/// Add a bind mount from source to target inside the container.
50+
pub fn bind(
51+
mut self,
52+
source: &'a impl AsRef<Utf8Path>,
53+
target: &'a impl AsRef<Utf8Path>,
54+
) -> Self {
55+
self.bind_mounts
56+
.push((source.as_ref().as_str(), target.as_ref().as_str()));
57+
self
58+
}
59+
60+
/// Bind a device node into the container.
61+
pub fn bind_device(mut self, device: &'a str) -> Self {
62+
self.devices.push(device);
63+
self
64+
}
65+
66+
/// Set an environment variable for the command.
67+
pub fn setenv(mut self, key: &'a str, value: &'a str) -> Self {
68+
self.env_vars.push((key, value));
69+
self
70+
}
71+
72+
/// Run the specified command inside the container.
73+
pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
74+
let mut cmd = Command::new("bwrap");
75+
76+
// Bind the root filesystem
77+
cmd.args(["--bind", self.chroot_path.as_str(), "/"]);
78+
79+
// Setup API filesystems
80+
// See https://systemd.io/API_FILE_SYSTEMS/
81+
cmd.args(["--proc", "/proc"]);
82+
cmd.args(["--dev", "/dev"]);
83+
cmd.args(["--ro-bind", "/sys", "/sys"]);
84+
85+
// Add bind mounts
86+
for (source, target) in &self.bind_mounts {
87+
cmd.args(["--bind", source, target]);
88+
}
89+
90+
// Add device bind mounts
91+
for device in self.devices {
92+
cmd.args(["--dev-bind", device, device]);
93+
}
94+
95+
// Add environment variables
96+
for (key, value) in &self.env_vars {
97+
cmd.args(["--setenv", key, value]);
98+
}
99+
100+
// Command to run
101+
cmd.arg("--");
102+
cmd.args(args);
103+
104+
cmd.log_debug().run_inherited_with_cmd_context()
105+
}
106+
}

crates/utils/src/lib.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@
22
//! things here that only depend on the standard library and
33
//! "core" crates.
44
//!
5+
mod bwrap;
6+
pub use bwrap::*;
57
mod command;
68
pub use command::*;
7-
mod path;
8-
pub use path::*;
99
mod iterators;
1010
pub use iterators::*;
11-
mod timestamp;
12-
pub use timestamp::*;
13-
mod tracing_util;
14-
pub use tracing_util::*;
11+
mod path;
12+
pub use path::*;
1513
/// Re-execute the current process
1614
pub mod reexec;
1715
mod result_ext;
1816
pub use result_ext::*;
17+
mod timestamp;
18+
pub use timestamp::*;
19+
mod tracing_util;
20+
pub use tracing_util::*;
1921

2022
/// The name of our binary
2123
pub const NAME: &str = "bootc";

0 commit comments

Comments
 (0)