Skip to content

Commit baa6c67

Browse files
committed
install: Enable installing to devices with multiple parents
Assisted-by: Claude Code (Opus 4) Signed-off-by: ckyrouac <ckyrouac@redhat.com>
1 parent adab93e commit baa6c67

7 files changed

Lines changed: 579 additions & 10 deletions

File tree

crates/lib/src/bootloader.rs

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,48 @@ pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
6767
Ok(r)
6868
}
6969

70+
/// Check whether the target bootupd supports `--filesystem`.
71+
///
72+
/// Runs `bootupctl backend install --help` and looks for `--filesystem` in the
73+
/// output. When `deployment_path` is set the command runs inside a bwrap
74+
/// container so we probe the binary from the target image.
75+
fn bootupd_supports_filesystem(rootfs: &Utf8Path, deployment_path: Option<&str>) -> Result<bool> {
76+
let help_args = ["bootupctl", "backend", "install", "--help"];
77+
let output = if let Some(deploy) = deployment_path {
78+
let target_root = rootfs.join(deploy);
79+
BwrapCmd::new(&target_root)
80+
.setenv(
81+
"PATH",
82+
"/bin:/usr/bin:/sbin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
83+
)
84+
.run_get_string(help_args)?
85+
} else {
86+
Command::new("bootupctl")
87+
.args(&help_args[1..])
88+
.log_debug()
89+
.run_get_string()?
90+
};
91+
92+
let use_filesystem = output.contains("--filesystem");
93+
94+
if use_filesystem {
95+
tracing::debug!("bootupd supports --filesystem");
96+
} else {
97+
tracing::debug!("bootupd does not support --filesystem, falling back to --device");
98+
}
99+
100+
Ok(use_filesystem)
101+
}
102+
103+
/// Install the bootloader via bootupd.
104+
///
105+
/// When the target bootupd supports `--filesystem` we pass it pointing at a
106+
/// block-backed mount so that bootupd can resolve the backing device(s) itself
107+
/// via `lsblk`. In the bwrap path we bind-mount the physical root at
108+
/// `/sysroot` to give `lsblk` a real block-backed path.
109+
///
110+
/// For older bootupd versions that lack `--filesystem` we fall back to the
111+
/// legacy `--device <device_path> <rootfs>` invocation.
70112
#[context("Installing bootloader")]
71113
pub(crate) fn install_via_bootupd(
72114
device: &bootc_blockdev::Device,
@@ -91,8 +133,6 @@ pub(crate) fn install_via_bootupd(
91133

92134
println!("Installing bootloader via bootupd");
93135

94-
let device_path = device.path();
95-
96136
// Build the bootupctl arguments
97137
let mut bootupd_args: Vec<&str> = vec!["backend", "install"];
98138
if configopts.bootupd_skip_boot_uuid {
@@ -107,26 +147,55 @@ pub(crate) fn install_via_bootupd(
107147
if let Some(ref opts) = bootupd_opts {
108148
bootupd_args.extend(opts.iter().copied());
109149
}
110-
bootupd_args.extend(["--device", &device_path, rootfs_mount]);
150+
151+
// When the target bootupd lacks --filesystem support, fall back to the
152+
// legacy --device flag. For --device we need the whole-disk device path
153+
// (e.g. /dev/vda), not a partition (e.g. /dev/vda3), so resolve the
154+
// parent via require_single_root(). (Older bootupd doesn't support
155+
// multiple backing devices anyway.)
156+
// Computed before building bootupd_args so the String lives long enough.
157+
let root_device_path = if bootupd_supports_filesystem(rootfs, deployment_path)
158+
.context("Probing bootupd --filesystem support")?
159+
{
160+
None
161+
} else {
162+
Some(device.require_single_root()?.path())
163+
};
164+
if let Some(ref dev) = root_device_path {
165+
tracing::debug!("bootupd does not support --filesystem, falling back to --device {dev}");
166+
bootupd_args.extend(["--device", dev]);
167+
bootupd_args.push(rootfs_mount);
168+
} else {
169+
tracing::debug!("bootupd supports --filesystem");
170+
bootupd_args.extend(["--filesystem", rootfs_mount]);
171+
bootupd_args.push(rootfs_mount);
172+
}
111173

112174
// Run inside a bwrap container. It takes care of mounting and creating
113175
// the necessary API filesystems in the target deployment and acts as
114176
// a nicer `chroot`.
115177
if let Some(deploy) = deployment_path {
116178
let target_root = rootfs.join(deploy);
117179
let boot_path = rootfs.join("boot");
180+
let rootfs_path = rootfs.to_path_buf();
118181

119182
tracing::debug!("Running bootupctl via bwrap in {}", target_root);
120183

121184
// Prepend "bootupctl" to the args for bwrap
122185
let mut bwrap_args = vec!["bootupctl"];
123186
bwrap_args.extend(bootupd_args);
124187

125-
let cmd = BwrapCmd::new(&target_root)
188+
let mut cmd = BwrapCmd::new(&target_root)
126189
// Bind mount /boot from the physical target root so bootupctl can find
127190
// the boot partition and install the bootloader there
128191
.bind(&boot_path, &"/boot");
129192

193+
// Only bind mount the physical root at /sysroot when using --filesystem;
194+
// bootupd needs it to resolve backing block devices via lsblk.
195+
if root_device_path.is_none() {
196+
cmd = cmd.bind(&rootfs_path, &"/sysroot");
197+
}
198+
130199
// The $PATH in the bwrap env is not complete enough for some images
131200
// so we inject a reasonnable default.
132201
// This is causing bootupctl and/or sfdisk binaries

crates/lib/src/install.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2564,9 +2564,8 @@ pub(crate) async fn install_to_filesystem(
25642564
// Find the real underlying backing device for the root. This is currently just required
25652565
// for GRUB (BIOS) and in the future zipl (I think).
25662566
let device_info = {
2567-
let dev =
2568-
bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?.require_single_root()?;
2569-
tracing::debug!("Backing device: {}", dev.path());
2567+
let dev = bootc_blockdev::list_dev(Utf8Path::new(&inspect.source))?;
2568+
tracing::debug!("Target filesystem backing device: {}", dev.path());
25702569
dev
25712570
};
25722571

crates/utils/src/bwrap.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ impl<'a> BwrapCmd<'a> {
5959
self
6060
}
6161

62-
/// Run the specified command inside the container.
63-
pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
62+
/// Build the bwrap `Command` with all bind mounts, env vars, and args.
63+
fn build_command<S: AsRef<OsStr>>(&self, args: impl IntoIterator<Item = S>) -> Command {
6464
let mut cmd = Command::new("bwrap");
6565

6666
// Bind the root filesystem
@@ -92,6 +92,21 @@ impl<'a> BwrapCmd<'a> {
9292
cmd.arg("--");
9393
cmd.args(args);
9494

95-
cmd.log_debug().run_inherited_with_cmd_context()
95+
cmd
96+
}
97+
98+
/// Run the specified command inside the container.
99+
pub fn run<S: AsRef<OsStr>>(self, args: impl IntoIterator<Item = S>) -> Result<()> {
100+
self.build_command(args)
101+
.log_debug()
102+
.run_inherited_with_cmd_context()
103+
}
104+
105+
/// Run the specified command inside the container and capture stdout as a string.
106+
pub fn run_get_string<S: AsRef<OsStr>>(
107+
self,
108+
args: impl IntoIterator<Item = S>,
109+
) -> Result<String> {
110+
self.build_command(args).log_debug().run_get_string()
96111
}
97112
}

hack/provision-derived.sh

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,48 @@ resize_rootfs: false
6969
CLOUDEOF
7070
fi
7171

72+
# Temporary: update bootupd from @CoreOS/continuous copr until
73+
# CentOS Stream 10 base image includes a version supporting --filesystem
74+
. /usr/lib/os-release
75+
case "${ID}-${VERSION_ID}" in
76+
"centos-10"|"rhel-10."*)
77+
case $ID in
78+
fedora) copr_distro="fedora" ;;
79+
*) copr_distro="centos-stream" ;;
80+
esac
81+
# Update bootc first from rhcontainerbot copr; the new bootupd
82+
# requires a newer bootc than what ships in the base image.
83+
cat >/etc/yum.repos.d/rhcontainerbot-bootc.repo <<REPOEOF
84+
[copr:copr.fedorainfracloud.org:rhcontainerbot:bootc]
85+
name=Copr repo for bootc owned by rhcontainerbot
86+
baseurl=https://download.copr.fedorainfracloud.org/results/rhcontainerbot/bootc/${copr_distro}-\$releasever-\$basearch/
87+
type=rpm-md
88+
skip_if_unavailable=True
89+
gpgcheck=1
90+
gpgkey=https://download.copr.fedorainfracloud.org/results/rhcontainerbot/bootc/pubkey.gpg
91+
repo_gpgcheck=0
92+
enabled=1
93+
enabled_metadata=1
94+
REPOEOF
95+
dnf -y update bootc
96+
rm -f /etc/yum.repos.d/rhcontainerbot-bootc.repo
97+
cat >/etc/yum.repos.d/coreos-continuous.repo <<REPOEOF
98+
[copr:copr.fedorainfracloud.org:group_CoreOS:continuous]
99+
name=Copr repo for continuous owned by @CoreOS
100+
baseurl=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/${copr_distro}-\$releasever-\$basearch/
101+
type=rpm-md
102+
skip_if_unavailable=True
103+
gpgcheck=1
104+
gpgkey=https://download.copr.fedorainfracloud.org/results/@CoreOS/continuous/pubkey.gpg
105+
repo_gpgcheck=0
106+
enabled=1
107+
enabled_metadata=1
108+
REPOEOF
109+
dnf -y install bootupd-0.2.32.41.gb788553
110+
rm -f /etc/yum.repos.d/coreos-continuous.repo
111+
;;
112+
esac
113+
72114
dnf clean all
73115
# Stock extra cleaning of logs and caches in general (mostly dnf)
74116
rm /var/log/* /var/cache /var/lib/{dnf,rpm-state,rhsm} -rf

tmt/plans/integration.fmf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,11 @@ execute:
222222
how: fmf
223223
test:
224224
- /tmt/tests/tests/test-38-install-bootloader-none
225+
226+
/plan-39-multi-device-esp:
227+
summary: Test multi-device ESP detection for to-existing-root
228+
discover:
229+
how: fmf
230+
test:
231+
- /tmt/tests/test-39-multi-device-esp
225232
# END GENERATED PLANS

0 commit comments

Comments
 (0)