diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15ecf24c..10874995 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,19 @@ jobs: run: | set -xeuo pipefail sudo podman run --rm -v $PWD:/run/src -w /run/src --privileged localhost/bootupd:latest tests/tests/generate-update-metadata.sh + + # Verify bootupd works in a bcvk ephemeral (virtiofs) environment. + # This catches regressions where bootloader-update.service fails on + # systems without a disk-backed bootloader (direct kernel boot). + ephemeral: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - uses: bootc-dev/actions/bootc-ubuntu-setup@main + with: + libvirt: true + - name: Build container image + run: sudo podman build --build-arg=base=quay.io/fedora/fedora-bootc:43 -t localhost/bootupd:latest -f Dockerfile . + - name: Smoke test (bcvk ephemeral) + timeout-minutes: 10 + run: sudo bcvk ephemeral run-ssh localhost/bootupd:latest -- /usr/libexec/bootupd-tests/ephemeral-test.sh diff --git a/Dockerfile b/Dockerfile index a0a75e4c..013f9c9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,8 @@ COPY --from=build /out/ / RUN </dev/null || true dnf -y copr enable rhcontainerbot/bootc centos-stream-9-x86_64 dnf -y install bootc dnf clean all @@ -38,8 +40,11 @@ EORUN # Remove /var/roothome as workaround RUN <&2 + exit 1 +fi +echo "ok: root filesystem is virtiofs" + +# The bootloader-update.service should have already run at boot (it's +# enabled by preset on Fedora). Verify it succeeded rather than failed. +systemctl is-active bootloader-update.service +echo "ok: bootloader-update.service is active (ran successfully at boot)" + +# Also verify a manual invocation skips cleanly. +output=$(bootupctl update 2>&1) +echo "$output" +if ! echo "$output" | grep -qi 'skipping'; then + echo "ERROR: expected skip message in output" >&2 + exit 1 +fi +echo "ok: bootupctl update skipped cleanly on virtiofs" + +echo "All ephemeral smoke tests passed." diff --git a/src/bootupd.rs b/src/bootupd.rs index 99558be9..60390e9b 100644 --- a/src/bootupd.rs +++ b/src/bootupd.rs @@ -430,17 +430,19 @@ pub(crate) fn adopt_and_update( /// Get the block device backing the current root by trying `/boot` first, /// then falling back to `/sysroot`. This avoids issues with virtual /// filesystems like composefs that are mounted on `/`. -#[context("Finding block device from boot or sysroot")] -fn list_dev_current_root() -> Result { +/// +/// Returns `Ok(None)` when no block-backed filesystem is found (e.g. virtiofs +/// in bcvk ephemeral, NFS root, ISO boot), so callers can skip gracefully. +fn list_dev_current_root() -> Result> { let auth = cap_std::ambient_authority(); for path in ["/boot", "/sysroot"] { if let Ok(dir) = Dir::open_ambient_dir(path, auth) { if let Ok(dev) = bootc_internal_blockdev::list_dev_by_dir(&dir) { - return Ok(dev); + return Ok(Some(dev)); } } } - anyhow::bail!("Failed to find block device from /boot or /sysroot") + Ok(None) } /// daemon implementation of component validate @@ -450,7 +452,9 @@ pub(crate) fn validate(name: &str) -> Result { let Some(inst) = state.installed.get(name) else { anyhow::bail!("Component {} is not installed", name); }; - let device = list_dev_current_root()?; + let Some(device) = list_dev_current_root()? else { + return Ok(ValidationResult::Skip); + }; component.validate(inst, &device) } @@ -612,17 +616,27 @@ impl RootContext { } } -/// Initialize parent devices to prepare the update -fn prep_before_update() -> Result { +/// Initialize parent devices to prepare the update. +/// +/// Returns `Ok(None)` when no block-backed boot filesystem is found, +/// so the caller can skip the update gracefully. +fn prep_before_update() -> Result> { let path = "/"; let sysroot = openat::Dir::open(path).context("Opening root dir")?; - let device = list_dev_current_root()?; - Ok(RootContext::new(sysroot, path, device)) + let Some(device) = list_dev_current_root()? else { + println!( + "No block-backed boot filesystem found; bootloader update is not applicable, skipping." + ); + return Ok(None); + }; + Ok(Some(RootContext::new(sysroot, path, device))) } pub(crate) fn client_run_update() -> Result<()> { crate::try_fail_point!("update"); - let rootcxt = prep_before_update()?; + let Some(rootcxt) = prep_before_update()? else { + return Ok(()); + }; let status: Status = status()?; if status.components.is_empty() && status.adoptable.is_empty() { println!("No components installed."); @@ -677,7 +691,9 @@ pub(crate) fn client_run_update() -> Result<()> { } pub(crate) fn client_run_adopt_and_update(with_static_config: bool) -> Result<()> { - let rootcxt = prep_before_update()?; + let Some(rootcxt) = prep_before_update()? else { + return Ok(()); + }; let status: Status = status()?; if status.adoptable.is_empty() { println!("No components are adoptable.");