Skip to content

Commit d265493

Browse files
committed
update: Skip bootloader update when no block devices back the root
In environments without block-backed boot filesystems (virtiofs in bcvk ephemeral, NFS root, ISO boot, etc.) there is no on-disk bootloader to manage. Previously the update path would fail because list_dev_current_root() bailed when it could not find a block device from /boot or /sysroot. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 969d7c2 commit d265493

4 files changed

Lines changed: 78 additions & 14 deletions

File tree

.github/workflows/ci.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,19 @@ jobs:
114114
run: |
115115
set -xeuo pipefail
116116
sudo podman run --rm -v $PWD:/run/src -w /run/src --privileged localhost/bootupd:latest tests/tests/generate-update-metadata.sh
117+
118+
# Verify bootupd works in a bcvk ephemeral (virtiofs) environment.
119+
# This catches regressions where bootloader-update.service fails on
120+
# systems without a disk-backed bootloader (direct kernel boot).
121+
ephemeral:
122+
runs-on: ubuntu-24.04
123+
steps:
124+
- uses: actions/checkout@v6
125+
- uses: bootc-dev/actions/bootc-ubuntu-setup@main
126+
with:
127+
libvirt: true
128+
- name: Build container image
129+
run: sudo podman build --build-arg=base=quay.io/fedora/fedora-bootc:43 -t localhost/bootupd:latest -f Dockerfile .
130+
- name: Smoke test (bcvk ephemeral)
131+
timeout-minutes: 10
132+
run: sudo bcvk ephemeral run-ssh localhost/bootupd:latest -- /usr/libexec/bootupd-tests/ephemeral-test.sh

Dockerfile

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ EORUN
3838
# Remove /var/roothome as workaround
3939
RUN <<EORUN
4040
set -xeuo pipefail
41-
[ -d /var/roothome ] && rm -rf /var/roothome
41+
rm -rf /var/roothome
4242
EORUN
43-
# Sanity check this too
44-
RUN bootc container lint --fatal-warnings
43+
# Install CI test scripts (used by bcvk ephemeral smoke tests)
44+
COPY --from=build /build/ci/ephemeral-test.sh /usr/libexec/bootupd-tests/ephemeral-test.sh
45+
# Sanity check this too; don't use --fatal-warnings as some base images
46+
# have pre-existing warnings (e.g. /run/systemd content in Fedora).
47+
RUN bootc container lint
4548

ci/ephemeral-test.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
# Smoke test for bcvk ephemeral (virtiofs direct-boot) environments.
3+
# This runs *inside* the ephemeral VM and verifies that bootupd
4+
# handles the diskless virtiofs root gracefully.
5+
set -xeuo pipefail
6+
7+
# Verify we're actually on virtiofs — this test is meaningless otherwise.
8+
root_fstype=$(findmnt -n -o FSTYPE /)
9+
if [ "$root_fstype" != "virtiofs" ]; then
10+
echo "ERROR: expected root fstype 'virtiofs', got '${root_fstype}'" >&2
11+
exit 1
12+
fi
13+
echo "ok: root filesystem is virtiofs"
14+
15+
# The bootloader-update.service should have already run at boot (it's
16+
# enabled by preset on Fedora). Verify it succeeded rather than failed.
17+
systemctl is-active bootloader-update.service
18+
echo "ok: bootloader-update.service is active (ran successfully at boot)"
19+
20+
# Also verify a manual invocation skips cleanly.
21+
output=$(bootupctl update 2>&1)
22+
echo "$output"
23+
if ! echo "$output" | grep -qi 'skipping'; then
24+
echo "ERROR: expected skip message in output" >&2
25+
exit 1
26+
fi
27+
echo "ok: bootupctl update skipped cleanly on virtiofs"
28+
29+
echo "All ephemeral smoke tests passed."

src/bootupd.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -430,17 +430,19 @@ pub(crate) fn adopt_and_update(
430430
/// Get the block device backing the current root by trying `/boot` first,
431431
/// then falling back to `/sysroot`. This avoids issues with virtual
432432
/// filesystems like composefs that are mounted on `/`.
433-
#[context("Finding block device from boot or sysroot")]
434-
fn list_dev_current_root() -> Result<Device> {
433+
///
434+
/// Returns `Ok(None)` when no block-backed filesystem is found (e.g. virtiofs
435+
/// in bcvk ephemeral, NFS root, ISO boot), so callers can skip gracefully.
436+
fn list_dev_current_root() -> Result<Option<Device>> {
435437
let auth = cap_std::ambient_authority();
436438
for path in ["/boot", "/sysroot"] {
437439
if let Ok(dir) = Dir::open_ambient_dir(path, auth) {
438440
if let Ok(dev) = bootc_internal_blockdev::list_dev_by_dir(&dir) {
439-
return Ok(dev);
441+
return Ok(Some(dev));
440442
}
441443
}
442444
}
443-
anyhow::bail!("Failed to find block device from /boot or /sysroot")
445+
Ok(None)
444446
}
445447

446448
/// daemon implementation of component validate
@@ -450,7 +452,9 @@ pub(crate) fn validate(name: &str) -> Result<ValidationResult> {
450452
let Some(inst) = state.installed.get(name) else {
451453
anyhow::bail!("Component {} is not installed", name);
452454
};
453-
let device = list_dev_current_root()?;
455+
let Some(device) = list_dev_current_root()? else {
456+
return Ok(ValidationResult::Skip);
457+
};
454458
component.validate(inst, &device)
455459
}
456460

@@ -612,17 +616,27 @@ impl RootContext {
612616
}
613617
}
614618

615-
/// Initialize parent devices to prepare the update
616-
fn prep_before_update() -> Result<RootContext> {
619+
/// Initialize parent devices to prepare the update.
620+
///
621+
/// Returns `Ok(None)` when no block-backed boot filesystem is found,
622+
/// so the caller can skip the update gracefully.
623+
fn prep_before_update() -> Result<Option<RootContext>> {
617624
let path = "/";
618625
let sysroot = openat::Dir::open(path).context("Opening root dir")?;
619-
let device = list_dev_current_root()?;
620-
Ok(RootContext::new(sysroot, path, device))
626+
let Some(device) = list_dev_current_root()? else {
627+
println!(
628+
"No block-backed boot filesystem found; bootloader update is not applicable, skipping."
629+
);
630+
return Ok(None);
631+
};
632+
Ok(Some(RootContext::new(sysroot, path, device)))
621633
}
622634

623635
pub(crate) fn client_run_update() -> Result<()> {
624636
crate::try_fail_point!("update");
625-
let rootcxt = prep_before_update()?;
637+
let Some(rootcxt) = prep_before_update()? else {
638+
return Ok(());
639+
};
626640
let status: Status = status()?;
627641
if status.components.is_empty() && status.adoptable.is_empty() {
628642
println!("No components installed.");
@@ -677,7 +691,9 @@ pub(crate) fn client_run_update() -> Result<()> {
677691
}
678692

679693
pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> {
680-
let rootcxt = prep_before_update()?;
694+
let Some(rootcxt) = prep_before_update()? else {
695+
return Ok(());
696+
};
681697
let status: Status = status()?;
682698
if status.adoptable.is_empty() {
683699
println!("No components are adoptable.");

0 commit comments

Comments
 (0)