Skip to content

Commit 1794593

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 1794593

7 files changed

Lines changed: 598 additions & 16 deletions

File tree

crates/lib/src/bootloader.rs

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ const BOOTUPD_UPDATES: &str = "usr/lib/bootupd/updates";
2323
// from: https://github.com/systemd/systemd/blob/26b2085d54ebbfca8637362eafcb4a8e3faf832f/man/systemd-boot.xml#L392
2424
const SYSTEMD_KEY_DIR: &str = "loader/keys";
2525

26-
/// Mount ESP part at /boot/efi
26+
/// Mount the first ESP found among backing devices at /boot/efi.
27+
///
28+
/// This is used by the install-alongside path to clean stale bootloader
29+
/// files before reinstallation. On multi-device setups only the first
30+
/// ESP is mounted and cleaned; stale files on additional ESPs are left
31+
/// in place (bootupd will overwrite them during installation).
32+
// TODO: clean all ESPs on multi-device setups
2733
pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool) -> Result<()> {
2834
let efi_path = Utf8Path::new("boot").join(crate::bootloader::EFI_DIR);
2935
let Some(esp_fd) = root
@@ -45,11 +51,14 @@ pub(crate) fn mount_esp_part(root: &Dir, root_path: &Utf8Path, is_ostree: bool)
4551
root
4652
};
4753

48-
let dev = bootc_blockdev::list_dev_by_dir(physical_root)?.require_single_root()?;
49-
if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) {
50-
let esp_path = esp_dev.path();
51-
bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
52-
tracing::debug!("Mounted {esp_path} at /boot/efi");
54+
let roots = bootc_blockdev::list_dev_by_dir(physical_root)?.find_all_roots()?;
55+
for dev in &roots {
56+
if let Some(esp_dev) = dev.find_partition_of_type(bootc_blockdev::ESP) {
57+
let esp_path = esp_dev.path();
58+
bootc_mount::mount(&esp_path, &root_path.join(&efi_path))?;
59+
tracing::debug!("Mounted {esp_path} at /boot/efi");
60+
return Ok(());
61+
}
5362
}
5463
Ok(())
5564
}
@@ -67,6 +76,48 @@ pub(crate) fn supports_bootupd(root: &Dir) -> Result<bool> {
6776
Ok(r)
6877
}
6978

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

92143
println!("Installing bootloader via bootupd");
93144

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

112183
// Run inside a bwrap container. It takes care of mounting and creating
113184
// the necessary API filesystems in the target deployment and acts as
114185
// a nicer `chroot`.
115186
if let Some(deploy) = deployment_path {
116187
let target_root = rootfs.join(deploy);
117188
let boot_path = rootfs.join("boot");
189+
let rootfs_path = rootfs.to_path_buf();
118190

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

121193
// Prepend "bootupctl" to the args for bwrap
122194
let mut bwrap_args = vec!["bootupctl"];
123195
bwrap_args.extend(bootupd_args);
124196

125-
let cmd = BwrapCmd::new(&target_root)
197+
let mut cmd = BwrapCmd::new(&target_root)
126198
// Bind mount /boot from the physical target root so bootupctl can find
127199
// the boot partition and install the bootloader there
128200
.bind(&boot_path, &"/boot");
129201

202+
// Only bind mount the physical root at /sysroot when using --filesystem;
203+
// bootupd needs it to resolve backing block devices via lsblk.
204+
if root_device_path.is_none() {
205+
cmd = cmd.bind(&rootfs_path, &"/sysroot");
206+
}
207+
130208
// The $PATH in the bwrap env is not complete enough for some images
131209
// so we inject a reasonnable default.
132210
// 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,15 @@ 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+
provision+:
229+
hardware:
230+
boot:
231+
method: uefi
232+
discover:
233+
how: fmf
234+
test:
235+
- /tmt/tests/test-39-multi-device-esp
225236
# END GENERATED PLANS

0 commit comments

Comments
 (0)