Skip to content
Closed
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand All @@ -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) |
Expand Down
11 changes: 10 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions docs/skills/ddi-installer-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<release-version>` GitHub Release.
- CI also produces a combined `dist/release/SHA256SUMS` manifest and signs it
Expand Down
12 changes: 6 additions & 6 deletions docs/skills/ddi-installer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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. |
Expand Down
1 change: 1 addition & 0 deletions docs/skills/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
37 changes: 37 additions & 0 deletions docs/skills/pxe-netboot.md
Original file line number Diff line number Diff line change
@@ -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-<version>` and
`bluefin-server-pxe-initrd-<version>.cpio.gz` assets. The matching DDI
(`bluefin-server-ddi-<version>.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-<version> systemd.unit=system-install.target console=tty0 console=ttyS0,115200 rw unattended inst.ddi_url=${base}/bluefin-server-ddi-<version>.raw.zst inst.ddi_sha256=<sha256> inst.target_disk=/dev/sda
initrd ${base}/bluefin-server-pxe-initrd-<version>.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.
9 changes: 7 additions & 2 deletions elements/installer/installer-stack.bst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
106 changes: 92 additions & 14 deletions elements/oci/bluefin-server-installer.bst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading