From 69631d3fb9098ecf36683d3bf34b7e1064c0e4ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:57:43 -0400 Subject: [PATCH] [scanner] feat: add PXE installer support Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- AGENTS.md | 2 + Justfile | 11 ++- docs/skills/ddi-installer-build.md | 6 +- docs/skills/ddi-installer.md | 12 +-- docs/skills/index.md | 1 + docs/skills/pxe-netboot.md | 37 ++++++++ elements/installer/installer-stack.bst | 9 +- elements/oci/bluefin-server-installer.bst | 106 +++++++++++++++++++--- 8 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 docs/skills/pxe-netboot.md diff --git a/AGENTS.md b/AGENTS.md index c5593d0..f6e9d11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ All `just` targets run BuildStream inside the FSDK `bst2` container via `just bs | `just export-ddi` | Export DDI artifacts to `dist/ddi/`. | | `just build-installer` | Local full installer build. | | `just export-installer` | Export installer + UKI to `dist/`. | +| `just export-pxe` | Export standalone PXE kernel/initrd to `dist/pxe/`. | | `just build-sysext` | Build the k3s `systemd-sysext`. | | `just export-sysext` | Export sysext artifacts to `dist/sysext/`. | | `just show-me-the-future` | Local QEMU installer smoke test. | @@ -40,6 +41,7 @@ All `just` targets run BuildStream inside the FSDK `bst2` container via `just bs | Task | Skill | |---|---| | Build or debug the installer / DDI | [`docs/skills/ddi-installer.md`](docs/skills/ddi-installer.md), [`docs/skills/ddi-installer-build.md`](docs/skills/ddi-installer-build.md) | +| PXE/netboot installer deployment | [`docs/skills/pxe-netboot.md`](docs/skills/pxe-netboot.md) | | Factory role, k3s sysext rationale, lab integration | [`docs/skills/factory-integration.md`](docs/skills/factory-integration.md) | | Work with `systemd-sysext` / `systemd-confext` | [`docs/skills/systemd-sysext-extensions.md`](docs/skills/systemd-sysext-extensions.md) | | Build or ship the k3s sysext | [`docs/skills/k3s-sysext.md`](docs/skills/k3s-sysext.md), [`docs/skills/k3s-sysext-ops.md`](docs/skills/k3s-sysext-ops.md) | diff --git a/Justfile b/Justfile index 1e91ee1..3e5b832 100644 --- a/Justfile +++ b/Justfile @@ -109,12 +109,21 @@ cluster-build REF="main": export-installer: build-installer rm -rf dist/installer-checkout mkdir -p dist dist/installer-checkout - rm -f dist/bluefin-server-installer-*.raw.zst dist/bluefin-server-*.efi dist/SHA256SUMS + rm -f dist/bluefin-server-installer-*.raw.zst dist/bluefin-server-*.efi dist/bluefin-server-pxe-* dist/SHA256SUMS just bst artifact checkout oci/bluefin-server-installer.bst --directory /src/dist/installer-checkout mv dist/installer-checkout/* dist/ rm -rf dist/installer-checkout @echo "==> wrote:" && ls -lh dist/ +# Export standalone PXE kernel and initrd to dist/pxe/. +[group('installer')] +export-pxe: export-installer + rm -rf dist/pxe + mkdir -p dist/pxe + cp dist/bluefin-server-pxe-* dist/pxe/ + (cd dist/pxe && sha256sum --binary -- bluefin-server-pxe-* > SHA256SUMS) + @echo "==> wrote PXE artifacts:" && ls -lh dist/pxe/ + # -- k3s systemd-sysext ------------------------------------------------------- # Produces a systemd-sysext extension image for k3s. diff --git a/docs/skills/ddi-installer-build.md b/docs/skills/ddi-installer-build.md index 4d9792a..c612cf1 100644 --- a/docs/skills/ddi-installer-build.md +++ b/docs/skills/ddi-installer-build.md @@ -22,7 +22,8 @@ The repo exposes the main build entrypoints through `just`: just validate # resolve the BuildStream graph just cluster-build # submit an Argo workflow to build/publish just build-installer # build the installer locally -just export-installer # export installer + UKI + SHA256SUMS to dist/ +just export-installer # export installer, UKI, PXE inputs + SHA256SUMS to dist/ +just export-pxe # copy standalone PXE inputs + checksums to dist/pxe/ just build-ddi # build the OS DDI payload just export-ddi # export DDI + SHA256SUMS to dist/ddi/ just build-sysext # build the k3s sysext @@ -87,7 +88,8 @@ Use direct I/O and full-block reads to avoid dirtying the page cache. The release process is driven by `.github/workflows/build.yml`: - Renovate point-release updates or direct pushes to `main` trigger a full build. -- CI builds the DDI payload, installer, target UKI, and k3s sysext. +- CI builds the DDI payload, installer, target UKI, standalone PXE kernel/initrd, + and k3s sysext. - CI uploads the versioned release assets to the corresponding `installer-v` GitHub Release. - CI also produces a combined `dist/release/SHA256SUMS` manifest and signs it diff --git a/docs/skills/ddi-installer.md b/docs/skills/ddi-installer.md index 6f36b1d..10002ce 100644 --- a/docs/skills/ddi-installer.md +++ b/docs/skills/ddi-installer.md @@ -24,14 +24,14 @@ metadata: - OCI-only image work (no installer involvement). - Bootc-specific changes. - Desktop or nspawn machine image work. -- Adding a network-pull installer — the design is offline; the DDI is embedded as - a data partition. +- PXE/netboot deployment — see [pxe-netboot.md](pxe-netboot.md). ## Architecture -The installer is offline, self-contained, and systemd-native. The OS DDI payload -(`bluefin-server-ddi.bst`) is embedded as a data partition on the installer -media at build time. No network access is required at install time. +The default installer is offline, self-contained, and systemd-native. The OS DDI +payload (`bluefin-server-ddi.bst`) is embedded as a data partition on installer +media at build time. PXE boot can opt into fetching the same DDI over HTTP(S); +see [pxe-netboot.md](pxe-netboot.md). The installer UI is systemd's built-in `systemd-sysinstall` which provides a terminal-based interactive installation that: @@ -142,7 +142,7 @@ For the detailed build/export/flash/release workflow, see | "A bash script is simpler." | A bash script cannot run the systemd-native interactive installer TUI. Use `systemd-sysinstall`. | | "Use knuckle instead." | knuckle is deprecated in favor of native `systemd-sysinstall` (systemd 261+). | | "Hardcode `root=/dev/vda2` for QEMU." | Bare metal has different device names. Always use PARTUUID. | -| "Pull the DDI from the network at install time." | Network failures = broken installs. The DDI is embedded in the installer media. | +| "Pull the DDI from the network at install time." | Only PXE boot enables this; failures stop before target-disk installation. | | "Put the DDI in the initrd cpio." | The DDI is 2 GiB+. The initrd cpio step must run before the DDI is placed in `/layer`. | | "Store the DDI in the ESP (FAT32)." | FAT32 has a 4 GiB per-file limit. Use a separate XFS partition. | | "Add an 8 GiB minimum size floor to the DDI." | The rootfs is immutable. It never grows in-place. Content + overhead is enough. | diff --git a/docs/skills/index.md b/docs/skills/index.md index 8382e0a..0e85449 100644 --- a/docs/skills/index.md +++ b/docs/skills/index.md @@ -27,6 +27,7 @@ This is the lazy-load routing table for agent skills. Keep this file in memory w | [`ci-tooling.md`](ci-tooling.md) | GitHub Actions, workflow SHA pinning, CI conventions | CI conventions and release pipeline rules. | | [`ddi-installer-build.md`](ddi-installer-build.md) | Building the installer or DDI on the cluster | Cluster build pipeline and local installer/DDI build. | | [`ddi-installer.md`](ddi-installer.md) | Installer boot flow, `systemd-sysinstall`, `systemd-repart` | High-level DDI install architecture and local smoke test. | +| [`pxe-netboot.md`](pxe-netboot.md) | Network booting the installer | PXE artifacts, kernel command line, and network DDI fetch. | | [`factory-integration.md`](factory-integration.md) | Lab integration, boot-test workflow, factory role | How Bluefin Server is consumed by the CI lab. | | [`gap-analysis-distros.md`](gap-analysis-distros.md) | Comparing Bluefin Server to other server OSes | Source-verified comparison to Ubuntu, Talos, Flatcar, FCOS. | | [`k3s-sysext-ops.md`](k3s-sysext-ops.md) | Building the k3s sysext | BuildStream element and publish steps for the k3s sysext. | diff --git a/docs/skills/pxe-netboot.md b/docs/skills/pxe-netboot.md new file mode 100644 index 0000000..a3cbf52 --- /dev/null +++ b/docs/skills/pxe-netboot.md @@ -0,0 +1,37 @@ +--- +name: pxe-netboot +description: Use when provisioning Bluefin Server over PXE or iPXE. +metadata: + type: reference + status: stable +--- +# PXE/netboot installer + +Releases include standalone `bluefin-server-pxe-vmlinuz-` and +`bluefin-server-pxe-initrd-.cpio.gz` assets. The matching DDI +(`bluefin-server-ddi-.raw.zst`) is fetched only when `inst.ddi_url` +is supplied; normal installer media remains offline and uses its embedded DDI. + +Example iPXE configuration: + +```ipxe +#!ipxe +set base https://mirror.example/bluefin +kernel ${base}/bluefin-server-pxe-vmlinuz- systemd.unit=system-install.target console=tty0 console=ttyS0,115200 rw unattended inst.ddi_url=${base}/bluefin-server-ddi-.raw.zst inst.ddi_sha256= inst.target_disk=/dev/sda +initrd ${base}/bluefin-server-pxe-initrd-.cpio.gz +boot +``` + +`inst.ddi_url` must use HTTP(S) and requires `inst.ddi_sha256`, the +64-character SHA-256 digest of the compressed DDI. The initrd verifies the +download before unpacking or invoking the installer. `inst.target_disk` selects +a writable whole-disk block device; without it, unattended mode selects the +first suitable disk not containing embedded installer media. + +Fetch, checksum, decompression, and target-disk validation failures stop +installation before the target disk is modified. Mirror release assets and +verify them against the signed `SHA256SUMS` manifest. DHCP is enabled on +Ethernet interfaces; provide serial console arguments above for diagnostics. + +Without `inst.ddi_url`, behavior is unchanged: the installer reads the embedded +DDI from `bluefin-installer-data` and needs no network. diff --git a/elements/installer/installer-stack.bst b/elements/installer/installer-stack.bst index de1ab3a..441d007 100644 --- a/elements/installer/installer-stack.bst +++ b/elements/installer/installer-stack.bst @@ -8,8 +8,8 @@ description: | bluefin-server-installer.bst. This keeps the toolchain minimal and removes the dracut dependency chain. - The DDI payload is embedded in the installer media (offline install). - No network access required at install time. + The DDI payload is embedded in installer media by default. PXE boot can + opt into fetching it over HTTP(S). depends: # Base runtime @@ -27,6 +27,11 @@ depends: - freedesktop-sdk.bst:components/cryptsetup.bst - freedesktop-sdk.bst:components/xfsprogs.bst - freedesktop-sdk.bst:components/dosfstools.bst + # Optional PXE installer path: fetch and unpack a network DDI. + - freedesktop-sdk.bst:components/curl.bst + - freedesktop-sdk.bst:components/coreutils.bst + - freedesktop-sdk.bst:components/util-linux.bst + - freedesktop-sdk.bst:components/zstd.bst # Kernel (installer boots from this; vmlinuz used as the UKI --linux= arg) - freedesktop-sdk.bst:components/linux.bst diff --git a/elements/oci/bluefin-server-installer.bst b/elements/oci/bluefin-server-installer.bst index d4c85e8..1e2dc17 100644 --- a/elements/oci/bluefin-server-installer.bst +++ b/elements/oci/bluefin-server-installer.bst @@ -127,26 +127,99 @@ config: #!/bin/bash set -euo pipefail - # Auto-detect target disk for unattended install (any raw disk of type "disk" that is not the installer, is not read-only, and has size > 0) - TARGET_DISK="" - INSTALLER_PART="$(readlink -f /dev/disk/by-partlabel/bluefin-installer-data 2>/dev/null || true)" + CMDLINE="$(cat /proc/cmdline 2>/dev/null || true)" + CMDLINE_ARGS=() + read -r -a CMDLINE_ARGS <<< "${CMDLINE}" + get_cmdline_value() { + local key="$1" arg + for arg in "${CMDLINE_ARGS[@]}"; do + case "${arg}" in + "${key}"=*) printf '%s' "${arg#*=}"; return 0 ;; + esac + done + return 1 + } - while read -r name type ro size; do - [ "${type}" = "disk" ] || continue - [ "${ro}" = "0" ] || continue - [ "${size}" -gt 0 ] || continue + INSTALLER_PART="$(readlink -f /dev/disk/by-partlabel/bluefin-installer-data 2>/dev/null || true)" + DDI_URL="$(get_cmdline_value inst.ddi_url || true)" + DDI_SHA256="$(get_cmdline_value inst.ddi_sha256 || true)" + if [ -n "${DDI_URL}" ]; then + case "${DDI_URL}" in + http://*|https://*) ;; + *) echo "ERROR: inst.ddi_url must use http:// or https://: ${DDI_URL}" >&2; exit 1 ;; + esac + if [[ ! "${DDI_SHA256}" =~ ^[[:xdigit:]]{64}$ ]]; then + echo "ERROR: inst.ddi_sha256 must be a 64-character SHA-256 digest when inst.ddi_url is set" >&2 + exit 1 + fi + DDI_COMPRESSED="/run/installer/bluefin-server-ddi.raw.zst" + DDI_RAW="/run/installer/bluefin-server-ddi.raw" + DDI_COMPRESSED_PART="${DDI_COMPRESSED}.part" + DDI_RAW_PART="${DDI_RAW}.part" + echo "==> Fetching DDI from ${DDI_URL}..." + mkdir -p /run/installer /dev/disk/by-partlabel + rm -f "${DDI_COMPRESSED_PART}" "${DDI_RAW_PART}" + if ! curl --fail --show-error --location --connect-timeout 10 \ + --retry 10 --retry-delay 2 \ + --output "${DDI_COMPRESSED_PART}" "${DDI_URL}"; then + echo "ERROR: failed to fetch DDI from ${DDI_URL}" >&2 + exit 1 + fi + if ! printf '%s %s\n' "${DDI_SHA256}" "${DDI_COMPRESSED_PART}" | + sha256sum --check --status -; then + echo "ERROR: DDI checksum verification failed" >&2 + exit 1 + fi + echo "==> DDI checksum verified." + if ! zstd --decompress --stdout "${DDI_COMPRESSED_PART}" > "${DDI_RAW_PART}"; then + echo "ERROR: downloaded DDI is not a valid zstd stream" >&2 + exit 1 + fi + mv -f "${DDI_RAW_PART}" "${DDI_RAW}" + rm -f "${DDI_COMPRESSED_PART}" + DDI_LOOP="$(losetup --find --show --read-only "${DDI_RAW}")" + ln -sfn "${DDI_LOOP}" /dev/disk/by-partlabel/bluefin-installer-data + fi - # Skip the disk containing the installer partition - if [ -n "${INSTALLER_PART}" ] && [[ "${INSTALLER_PART}" == "${name}"* ]]; then - continue + # Auto-detect target disk for unattended install (any raw disk of type "disk" that is not the installer, is not read-only, and has size > 0) + TARGET_DISK="$(get_cmdline_value inst.target_disk || true)" + if [ -n "${TARGET_DISK}" ]; then + case "${TARGET_DISK}" in + /dev/*) ;; + *) echo "ERROR: inst.target_disk must be an absolute /dev path: ${TARGET_DISK}" >&2; exit 1 ;; + esac + TARGET_TYPE="$(lsblk -ndo TYPE "${TARGET_DISK}" 2>/dev/null || true)" + TARGET_RO="$(lsblk -ndo RO "${TARGET_DISK}" 2>/dev/null || true)" + TARGET_SIZE="$(lsblk -ndo SIZE "${TARGET_DISK}" 2>/dev/null || true)" + if [ ! -b "${TARGET_DISK}" ] || [ "${TARGET_TYPE}" != "disk" ] || + [ "${TARGET_RO}" != "0" ] || + ! [[ "${TARGET_SIZE}" =~ ^[0-9]+$ ]] || [ "${TARGET_SIZE}" -le 0 ]; then + echo "ERROR: inst.target_disk is not a writable whole-disk block device: ${TARGET_DISK}" >&2 + exit 1 + fi + if [ -n "${INSTALLER_PART}" ] && [[ "${INSTALLER_PART}" == "${TARGET_DISK}"* ]]; then + echo "ERROR: inst.target_disk points to installer media: ${TARGET_DISK}" >&2 + exit 1 fi + fi + + if [ -z "${TARGET_DISK}" ]; then + while read -r name type ro size; do + [ "${type}" = "disk" ] || continue + [ "${ro}" = "0" ] || continue + [ "${size}" -gt 0 ] || continue - TARGET_DISK="${name}" - break - done < <(lsblk -p -d -n -o NAME,TYPE,RO,SIZE -b) + # Skip the disk containing the installer partition + if [ -n "${INSTALLER_PART}" ] && [[ "${INSTALLER_PART}" == "${name}"* ]]; then + continue + fi + + TARGET_DISK="${name}" + break + done < <(lsblk -p -d -n -o NAME,TYPE,RO,SIZE -b) + fi # Check for unattended in kernel command line - CMDLINE="$(cat /proc/cmdline 2>/dev/null || true)" if [[ " ${CMDLINE} " == *" unattended "* ]]; then echo "==> Running in UNATTENDED mode..." if [ -n "${TARGET_DISK}" ]; then @@ -316,7 +389,12 @@ config: # Also export the target UKI (.efi) for GitHub Releases and systemd-sysupdate cp /layer/usr/lib/bluefin-server/bluefin-server.efi "bluefin-server-%{installer-version}.efi" + # Export separate PXE inputs; PXE supplies its own kernel command line. + cp /layer/boot/vmlinuz "bluefin-server-pxe-vmlinuz-%{installer-version}" + cp /installer.cpio.gz "bluefin-server-pxe-initrd-%{installer-version}.cpio.gz" sha256sum --binary "${FNAME}.zst" > SHA256SUMS sha256sum --binary "bluefin-server-%{installer-version}.efi" >> SHA256SUMS + sha256sum --binary "bluefin-server-pxe-vmlinuz-%{installer-version}" >> SHA256SUMS + sha256sum --binary "bluefin-server-pxe-initrd-%{installer-version}.cpio.gz" >> SHA256SUMS ls -lh