Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 46 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ jobs:
|| echo "$LABELS" | jq -e 'index("ci/merge")' > /dev/null; }; then
# Full suite: all OSes
echo 'package_os_matrix=["fedora-44","fedora-45","fedora-46","centos-9","centos-10"]' >> "$GITHUB_OUTPUT"
echo 'integration_os_matrix=["fedora-44","centos-9","centos-10"]' >> "$GITHUB_OUTPUT"
# fedora-45 and fedora-46 only run the systemd-shim leg, see the excludes below
echo 'integration_os_matrix=["fedora-44","fedora-45","fedora-46","centos-9","centos-10"]' >> "$GITHUB_OUTPUT"
echo 'upgrade_os_matrix=["fedora-44","centos-10"]' >> "$GITHUB_OUTPUT"
echo 'run_heavy=true' >> "$GITHUB_OUTPUT"
elif [[ "$DOCS_ONLY" != "true" ]] && echo "$LABELS" | jq -e 'index("ci/tier-1")' > /dev/null; then
Expand Down Expand Up @@ -301,7 +302,7 @@ jobs:
test_os: ${{ fromJson(needs.compute-ci-level.outputs.integration_os_matrix) }}
variant: [ostree, composefs]
filesystem: ["ext4", "xfs"]
bootloader: ["grub", "grub-cc", "systemd"]
bootloader: ["grub", "grub-cc", "systemd", "systemd-shim"]
boot_type: ["bls", "uki"]
seal_state: ["sealed", "unsealed"]

Expand Down Expand Up @@ -349,7 +350,38 @@ jobs:
- bootloader: grub-cc
seal_state: sealed

# systemd-boot behind shim, installed through bootupd: one composefs
# leg, unsealed BLS on ext4, on Fedora 45 and rawhide only, the
# tested OSes that ship a signed systemd-boot laid out as a bootupd
# component (rawhide is allowed to fail, as in the package job).
- variant: ostree
bootloader: systemd-shim
- bootloader: systemd-shim
seal_state: sealed
- bootloader: systemd-shim
boot_type: uki
- bootloader: systemd-shim
filesystem: xfs
- test_os: fedora-44
bootloader: systemd-shim
- test_os: centos-9
bootloader: systemd-shim
- test_os: centos-10
bootloader: systemd-shim
# and those two run nothing else
- test_os: fedora-45
bootloader: grub
- test_os: fedora-45
bootloader: systemd
- test_os: fedora-46
bootloader: grub
- test_os: fedora-46
bootloader: grub-cc
- test_os: fedora-46
bootloader: systemd

runs-on: ubuntu-26.04
continue-on-error: ${{ matrix.test_os == 'fedora-46' }}

steps:
- uses: actions/checkout@v7
Expand All @@ -371,7 +403,14 @@ jobs:

echo "BOOTC_variant=${{ matrix.variant }}" >> $GITHUB_ENV
echo "BOOTC_filesystem=${{ matrix.filesystem }}" >> $GITHUB_ENV
echo "BOOTC_bootloader=${{ matrix.bootloader }}" >> $GITHUB_ENV
bootloader="${{ matrix.bootloader }}"
if [[ "${bootloader}" == systemd-shim ]]; then
# Not a bootc bootloader: systemd-boot, which the image's install
# config requests, installed through bootupd with shim in front of it
bootloader=systemd
echo "BOOTC_sdboot_shim=1" >> $GITHUB_ENV
fi
echo "BOOTC_bootloader=${bootloader}" >> $GITHUB_ENV
echo "BOOTC_boot_type=${{ matrix.boot_type }}" >> $GITHUB_ENV
echo "BOOTC_seal_state=${{ matrix.seal_state }}" >> $GITHUB_ENV

Expand Down Expand Up @@ -405,7 +444,7 @@ jobs:
- name: Run TMT integration tests
run: |
if [[ "${{ matrix.variant }}" = composefs ]]; then
just test-composefs "${{ matrix.bootloader }}" "${{ matrix.filesystem }}" "${{ matrix.boot_type }}" "${{ matrix.seal_state }}"
just test-composefs "$BOOTC_bootloader" "${{ matrix.filesystem }}" "${{ matrix.boot_type }}" "${{ matrix.seal_state }}"
else
just test-tmt integration
fi
Expand Down Expand Up @@ -513,6 +552,9 @@ jobs:
exclude:
# centos-9 ships an older dracut that lacks the auto-install of setup-root-conf.toml
- test_os: centos-9
# fedora-45 and fedora-46 are in the integration OS list only for the systemd-shim leg
- test_os: fedora-45
- test_os: fedora-46

runs-on: ubuntu-26.04

Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ bootc has two storage backends: `ostree` (default, production) and `composefs`
| `boot_type` | `bls`, `uki` | UKI embeds the composefs digest |
| `seal_state` | `unsealed`, `sealed` | Sealed signs the UKI for Secure Boot |
| `filesystem` | `ext4`, `btrfs`, `xfs` | xfs lacks fsverity, incompatible with sealed |
| `sdboot_shim` | unset, `1` | Keep bootupd and shim; the distro-signed systemd-boot is installed through bootupd |

These are controlled via `BOOTC_`-prefixed environment variables.
Using environment variables (rather than `just` command-line overrides)
Expand All @@ -176,6 +177,10 @@ The constraints are:
- `sealed` requires `boot_type=uki` (the digest lives in the UKI cmdline)
- `sealed` requires `filesystem` with fsverity support (`ext4` or `btrfs`)
- `uki` requires `bootloader=systemd`
- `sdboot_shim=1` requires `bootloader=systemd`, `seal_state=unsealed` and a
base whose bootupd accepts `--bootloader systemd` (0.3.0 or newer) and whose
signed `systemd-boot-<arch>` package is laid out as a bootupd component, such
as Fedora 45 and rawhide

Common workflows:

Expand Down
35 changes: 27 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ ARG SKIP_CONFIGS
ARG boot_type
ARG seal_state
ARG bootloader
ARG sdboot_shim=""
# All network-fetching operations: package installs from distro repos, Copr, Koji.
# Separated so `just build-fetch --target=fetch` can be retried independently on
# transient network failures without re-running the configuration phase.
Expand Down Expand Up @@ -172,13 +173,30 @@ RUN --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
dnf install -y "${pkgs_to_install[@]}"
fi

# Currently dnf installs grub-cc at /usr/lib/efi/grub2/1:2.12-60.eln156/EFI/eln/cc/grubx64-cc.efi
# which is less than ideal because:
# - the "cc" subdirectory
# - no support for installing grub-cc in bootupd
#
# So we move the binary to /usr/lib/grub-cc/grub-cc.efi so we have a predictale location from which
# we can copy the EFI binary to the ESP
# systemd-boot behind shim goes through bootupd, which has to accept
# `--bootloader systemd` (bootupd 0.3.0 and newer), and needs the
# distribution's signed systemd-boot laid out as a bootupd component
# (Fedora's systemd-boot-<arch> since 262). A stable release may carry
# those only in updates-testing at first, so enable it where it exists.
if [[ -n "${sdboot_shim}" ]]; then
case "$(uname -m)" in
x86_64) sdboot_pkg=systemd-boot-x64 ;;
aarch64) sdboot_pkg=systemd-boot-aa64 ;;
*) echo "sdboot_shim is not supported on $(uname -m)" >&2; exit 1 ;;
esac
testing=()
if dnf repolist --all 2>/dev/null | grep -q '^updates-testing '; then
testing=(--enablerepo=updates-testing)
fi
dnf -y "${testing[@]}" install "${sdboot_pkg}"
dnf -y "${testing[@]}" upgrade bootupd
fi

# The grub-cc package ships its binary inside the grub2 component, e.g.
# /usr/lib/efi/grub2/<evr>/EFI/fedora/cc/grubx64-cc.efi, and bootupd only
# installs grub-cc from a component of its own. So bootc asks bootupd for
# GRUB and then swaps in the binary, which we stage at the predictable
# /usr/lib/grub-cc/grub-cc.efi (BootloaderInstallMethod::BootupdGrubCcSwap).
if [[ "$bootloader" == "grub-cc" ]]; then
mkdir /var/grub-cc
rpm2archive /var/grub-cc.rpm | tar -xvz -C /var/grub-cc
Expand Down Expand Up @@ -333,6 +351,7 @@ ARG variant
ARG bootloader
ARG boot_type
ARG baseconfigs=""
ARG sdboot_shim=""

# Switch to a signed systemd-boot, if configured
RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp \
Expand All @@ -341,7 +360,7 @@ RUN --network=none --mount=type=tmpfs,target=/run --mount=type=tmpfs,target=/tmp
set -xeuo pipefail

if [[ "${bootloader}" == "systemd" ]]; then
/run/packaging/switch-to-sdboot /run/sdboot-signed
SDBOOT_SHIM="${sdboot_shim}" /run/packaging/switch-to-sdboot /run/sdboot-signed
fi

# Composefs test images are installed without --composefs-backend (see
Expand Down
7 changes: 7 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ filesystem := env("BOOTC_filesystem", "ext4")
boot_type := env("BOOTC_boot_type", "bls")
# Only used for composefs tests
seal_state := env("BOOTC_seal_state", "unsealed")
# Only used for composefs systemd-boot tests: set to keep bootupd and shim in
# the image, so systemd-boot is installed through bootupd with shim in front
sdboot_shim := env("BOOTC_sdboot_shim", "")
# Only used for composefs UKI tests: "v1" or "v2"
erofs_version := env("BOOTC_erofs_version", "v1")
# Baseconfigs to inject into the image for testing (e.g. "etc-transient" or "root-transient")
Expand Down Expand Up @@ -78,6 +81,7 @@ base_buildargs := generic_buildargs + " " + _extra_src_args \
+ " --build-arg=seal_state=" + seal_state \
+ " --build-arg=filesystem=" + filesystem \
+ " --build-arg=erofs_version=" + erofs_version \
+ " --build-arg=sdboot_shim=" + sdboot_shim \
+ " --build-arg=baseconfigs=" + baseconfigs
buildargs := base_buildargs \
+ " --cap-add=all --security-opt=label=type:container_runtime_t --device /dev/fuse" \
Expand Down Expand Up @@ -170,6 +174,9 @@ list-variants:
- The specified boot type (BLS/UKI)
- The specified seal state (sealed/unsealed) determining whether we sign the UKI and
use secure boot or not
- With BOOTC_sdboot_shim=1 (systemd-boot, unsealed, e.g. Fedora 45 and rawhide): keep
bootupd and shim, so the distro-signed systemd-boot is installed through bootupd with
shim in front

Use `just build-sealed` as shortcut to build a sealed composefs image with systemd-boot as the bootloader

Expand Down
47 changes: 39 additions & 8 deletions contrib/packaging/switch-to-sdboot
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,54 @@
# SRC should contain an "out" subdirectory with:
# - systemd-boot-unsigned RPM (*.rpm)
# - signed systemd-boot binary (systemd-boot*.efi)
#
# By default bootupd and shim are removed, so bootc installs systemd-boot
# with a bare `bootctl install`. With SDBOOT_SHIM=1 both are kept and the
# distribution's signed systemd-boot, which must already be laid out as a
# bootupd component (Fedora's systemd-boot-x64 since 262), is registered with
# bootupd, so that `bootc install --bootloader systemd` goes through bootupd
# and ends up with shim in front of systemd-boot. SRC is unused in that mode.
set -xeuo pipefail

src=$1/out
shift

# systemd-boot (and, when sealed, the UKI) is signed and booted directly
# with our own Secure Boot key, so shim is never in the trust chain
# regardless of sealing. Uninstall bootupd (managed differently for
# sd-boot) and shim together so neither lingers in the image.
case "$(uname -m)" in
x86_64) shim_pkg=shim-x64 ;;
aarch64) shim_pkg=shim-aa64 ;;
*) shim_pkg="" ;;
x86_64) efi_arch=x64 ;;
aarch64) efi_arch=aa64 ;;
*) efi_arch="" ;;
esac

if [ -n "${SDBOOT_SHIM:-}" ]; then
[ -n "${efi_arch}" ] || { echo "SDBOOT_SHIM is not supported on $(uname -m)" >&2; exit 1; }
# bootc only hands systemd-boot to bootupd when bootupd accepts
# --bootloader systemd; with any other bootupd the image would silently take
# the bootctl path and this variant would test nothing. Packaged 0.2.36
# builds advertise --bootloader but accept only grub, so probe the value:
# clap rejects it before acting on the --help after it.
command -v bootupctl >/dev/null || { echo "bootupd is not installed" >&2; exit 1; }
bootupctl backend install --bootloader systemd --help >/dev/null 2>&1 \
|| { echo "bootupd does not accept --bootloader systemd" >&2; exit 1; }
# The signed systemd-boot has to sit under the second-stage name baked into
# shim, inside the component's EFI directory; a package that ships it
# anywhere else is invisible to bootupd, which then installs shim alone.
found=""
for f in /usr/lib/efi/systemd-boot/*/EFI/*/grub"${efi_arch}".efi; do
[ -f "${f}" ] && found=${f}
done
[ -n "${found}" ] || { echo "no signed systemd-boot laid out as a bootupd component under /usr/lib/efi/systemd-boot" >&2; exit 1; }
bootupctl backend generate-update-metadata
exit 0
fi

# systemd-boot (and, when sealed, the UKI) is signed and booted directly
# with our own Secure Boot key, so shim is never in the trust chain
# regardless of sealing. Uninstall bootupd and shim together, so that bootc
# takes the bare `bootctl install` path (the shim-less layout this mode
# tests) and neither lingers in the image. Also uninstall the distribution's
# signed systemd-boot, if any: bootctl would prefer its .efi.signed over ours.
pkgs_to_remove=()
for pkg in bootupd "${shim_pkg}"; do
for pkg in bootupd "${efi_arch:+shim-${efi_arch}}" "${efi_arch:+systemd-boot-${efi_arch}}"; do
[ -n "${pkg}" ] || continue
rpm -q "${pkg}" &>/dev/null && pkgs_to_remove+=("${pkg}")
done
Expand Down
43 changes: 37 additions & 6 deletions crates/blockdev/src/blockdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,7 @@ impl Device {
/// Calls find_all_roots() to discover physical disks, then searches each for an ESP.
/// Returns None if no ESPs are found.
pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
let mut esps = Vec::new();
for root in &self.find_all_roots()? {
if let Some(esp) = root.find_partition_of_esp_optional()? {
esps.push(esp.clone());
}
}
let esps = esps_of_roots(&self.find_all_roots()?)?;
Ok((!esps.is_empty()).then_some(esps))
}

Expand Down Expand Up @@ -456,6 +451,20 @@ impl Device {
}
}

/// The ESPs found on `roots`, each listed once: an ESP on a firmware RAID
/// array (such as Intel VROC) is found through every disk in the array.
fn esps_of_roots(roots: &[Device]) -> Result<Vec<Device>> {
let mut esps: Vec<Device> = Vec::new();
for root in roots {
if let Some(esp) = root.find_partition_of_esp_optional()? {
if !esps.iter().any(|known| known.path() == esp.path()) {
esps.push(esp.clone());
}
}
}
Ok(esps)
}

#[context("Listing device {dev}")]
pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
let mut devs: DevicesOutput = Command::new("lsblk")
Expand Down Expand Up @@ -991,6 +1000,28 @@ mod test {
}
}

#[test]
fn test_esps_of_roots() {
let cases = [
// Each disk of a software RAID has an ESP of its own.
(
include_str!("../tests/fixtures/lsblk-swraid.json"),
&["sda1", "sdb1"][..],
),
// Both NVMe disks of a firmware RAID lead to the array's one ESP.
(
include_str!("../tests/fixtures/lsblk-vroc.json"),
&["md126p1"][..],
),
];
for (fixture, expected) in cases {
let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
let esps = esps_of_roots(&devs.blockdevices).unwrap();
let names = esps.iter().map(|esp| esp.name.as_str()).collect::<Vec<_>>();
assert_eq!(names, expected);
}
}

#[test]
fn test_parse_lsblk_swraid() {
let fixture = include_str!("../tests/fixtures/lsblk-swraid.json");
Expand Down
Loading
Loading