Generic kernel + init= boot path for OCI-pulled images

Closes the full arc: banger kernel pull + image pull + vm create + vm ssh
now works end-to-end against docker.io/library/debian:bookworm with zero
manual image building.

Generic kernel:
 - New scripts/make-generic-kernel.sh builds vmlinux from upstream
   kernel.org sources using Firecracker's official minimal config
   (configs/firecracker-x86_64-6.1.config). All critical drivers
   (virtio_blk, virtio_net, ext4, vsock) compiled in — no modules,
   no initramfs needed.
 - Published as generic-6.12 in the catalog (kernels.thaloco.com).
 - catalog.json updated with the new entry.

Direct-boot init= override (vm_lifecycle.go):
 - For images without an initrd (direct-boot / OCI-pulled), banger now
   passes init=/usr/local/libexec/banger-first-boot on the kernel
   cmdline. The script runs as PID 1, mounts /proc /sys /dev /run,
   checks for systemd — if present execs it immediately; if not
   (container images), installs systemd-sysv + openssh-server via the
   guest's package manager, then execs systemd.
 - Also passes kernel-level ip= parameter via BuildBootArgsWithKernelIP
   so the kernel configures the network interface before init runs
   (container images don't ship iproute2, so the userspace bootstrap
   script can't call ip(8)).
 - Masks dev-ttyS0.device and dev-vdb.device systemd units that
   otherwise wait 90s for udev events that never fire in Firecracker
   guests started from container rootfses.

first-boot.sh rewritten as universal init wrapper:
 - Works as PID 1 (mounts essential filesystems) OR as a systemd
   oneshot (existing behavior).
 - Installs both systemd-sysv AND openssh-server (container images
   have neither).
 - Dispatch updated: debian, alpine, fedora, arch, opensuse families
   + ID_LIKE fallback. All tests updated.

Opencode capability skip for direct-boot images:
 - The opencode readiness check (WaitReady on vsock port 4096) now
   returns nil for images without an initrd, since pulled container
   images don't ship the opencode service. Without this, the VM
   would be marked as error for lacking an opinionated add-on.

Docs: README and kernel-catalog.md updated to recommend generic-6.12
as the default kernel for OCI-pulled images. AGENTS.md notes the new
build script.

Verified live:
 - banger kernel pull generic-6.12
 - banger image pull docker.io/library/debian:bookworm --kernel-ref generic-6.12
 - banger vm create --image debian-bookworm --name testbox --nat
 - banger vm ssh testbox -- "id; uname -r; systemctl is-active banger-vsock-agent"
 → uid=0(root), kernel 6.12.8, Debian bookworm, vsock-agent active,
   sshd running, SSH working.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-16 20:12:56 -03:00
parent 2478fe3cc3
commit 8f4be112c2
No known key found for this signature in database
GPG key ID: 33112E6833C34679
10 changed files with 3808 additions and 65 deletions

View file

@ -1,34 +1,61 @@
#!/bin/sh
# banger-first-boot — runs once at the first boot of a pulled OCI image.
# Installs openssh-server via the guest's native package manager, enables
# and starts the ssh daemon, and removes its own trigger file so the
# service is a no-op on subsequent boots.
# banger-first-boot — universal init wrapper for banger VMs.
#
# Distro dispatch is driven by /etc/os-release's ID / ID_LIKE values.
# RUN_PLAN=1 in the environment makes this script echo the commands it
# would run instead of executing them — used by tests.
# When passed as init= on the kernel cmdline (direct-boot images without
# an initramfs), this script runs as PID 1. It:
# 1. Mounts the essential virtual filesystems (/proc, /sys, /dev, /run)
# 2. If systemd (or any init) is already installed, execs it immediately
# 3. Otherwise: brings up the network, installs systemd + openssh-server
# via the guest's native package manager, then execs systemd
#
# On subsequent boots (after systemd is installed), step 2 fires in <10ms.
#
# Test hooks:
# RUN_PLAN=1 echo the install command instead of executing it
# OS_RELEASE_FILE=<path> override /etc/os-release for distro detection
# BANGER_FIRST_BOOT_MARKER=<path> override the marker file path
set -eu
log() { printf '[banger-first-boot] %s\n' "$*" >&2; }
MARKER="${BANGER_FIRST_BOOT_MARKER:-/var/lib/banger/first-boot-pending}"
if [ ! -f "$MARKER" ]; then
log "marker absent; nothing to do"
exit 0
# --- Step 1: essential mounts (only when running as PID 1) ---
if [ "$$" = "1" ]; then
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sysfs /sys 2>/dev/null || true
mount -t devtmpfs devtmpfs /dev 2>/dev/null || true
mount -t tmpfs tmpfs /run 2>/dev/null || true
mount -t tmpfs tmpfs /tmp 2>/dev/null || true
fi
# If sshd is already present, just enable + start and finish.
# The RUN_PLAN env skips this short-circuit so tests can exercise the
# dispatch logic on hosts that happen to have sshd installed.
if [ "${RUN_PLAN:-0}" != "1" ] && command -v sshd >/dev/null 2>&1; then
log "sshd already installed; enabling and starting"
systemctl enable --now ssh.service 2>/dev/null || \
systemctl enable --now sshd.service 2>/dev/null || true
rm -f "$MARKER"
exit 0
# --- Step 2: if a real init exists, hand off immediately ---
# (RUN_PLAN mode skips this so the dispatch logic can be tested on hosts
# that have systemd installed.)
if [ "${RUN_PLAN:-0}" != "1" ]; then
for candidate_init in /usr/lib/systemd/systemd /lib/systemd/systemd /sbin/init; do
if [ -x "$candidate_init" ]; then
MARKER="${BANGER_FIRST_BOOT_MARKER:-/var/lib/banger/first-boot-pending}"
if [ -f "$MARKER" ]; then
rm -f "$MARKER"
fi
log "found init at $candidate_init; handing off"
exec "$candidate_init" "$@"
fi
done
fi
# --- Step 3: no init found — we're on a container image, provision it ---
log "no init system found; installing systemd + openssh-server"
# Bring up network so apt-get/apk can reach package repos.
# banger-network-bootstrap reads IP from /proc/cmdline (kernel ip= arg)
# or /etc/banger-network.conf (written by vm_disk.patchRootOverlay).
if [ -x /usr/local/libexec/banger-network-bootstrap ]; then
log "bringing up network"
/usr/local/libexec/banger-network-bootstrap || log "network bootstrap failed (continuing anyway)"
fi
# Detect distro
DIST=""
FAMILY=""
OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"
@ -38,50 +65,48 @@ if [ -r "$OS_RELEASE_FILE" ]; then
DIST="${ID:-}"
FAMILY="${ID_LIKE:-}"
fi
log "detected distro: ID=$DIST ID_LIKE=$FAMILY"
# Dispatch. Each branch sets CMD to the single install command.
# Dispatch install command
CMD=""
case "$DIST" in
debian|ubuntu|kali|raspbian|linuxmint|pop)
CMD="env DEBIAN_FRONTEND=noninteractive apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server"
CMD="apt-get update && apt-get install -y systemd-sysv openssh-server"
;;
alpine)
CMD="apk add --no-cache openssh"
CMD="apk add --no-cache openrc openssh systemd"
;;
fedora|rhel|centos|rocky|almalinux)
CMD="dnf install -y openssh-server"
CMD="dnf install -y systemd openssh-server"
;;
arch|archlinux|manjaro)
CMD="pacman -Sy --noconfirm openssh"
;;
opensuse*|suse)
CMD="zypper --non-interactive install -y openssh"
CMD="zypper --non-interactive install -y systemd openssh"
;;
*)
# Fall back to ID_LIKE.
case " $FAMILY " in
*" debian "*)
CMD="env DEBIAN_FRONTEND=noninteractive apt-get update && env DEBIAN_FRONTEND=noninteractive apt-get install -y openssh-server"
CMD="apt-get update && apt-get install -y systemd-sysv openssh-server"
;;
*" rhel "* | *" fedora "*)
CMD="dnf install -y openssh-server"
CMD="dnf install -y systemd openssh-server"
;;
*" arch "*)
CMD="pacman -Sy --noconfirm openssh"
;;
*" suse "*)
CMD="zypper --non-interactive install -y openssh"
CMD="zypper --non-interactive install -y systemd openssh"
;;
esac
;;
esac
if [ -z "$CMD" ]; then
log "no known install command for distro '$DIST' (ID_LIKE='$FAMILY')"
log "edit /usr/local/libexec/banger-first-boot to add a branch, then restart banger-first-boot.service"
exit 1
log "FATAL: no known install command for distro '$DIST' (ID_LIKE='$FAMILY')"
log "drop to emergency shell"
exec /bin/sh
fi
if [ "${RUN_PLAN:-0}" = "1" ]; then
@ -89,13 +114,29 @@ if [ "${RUN_PLAN:-0}" = "1" ]; then
exit 0
fi
log "installing openssh-server: $CMD"
sh -c "$CMD"
log "enabling sshd"
systemctl enable --now ssh.service 2>/dev/null || \
systemctl enable --now sshd.service 2>/dev/null || \
{ log "could not enable sshd service"; exit 1; }
log "running: $CMD"
eval "$CMD" || {
log "package install failed; dropping to shell"
exec /bin/sh
}
# Remove first-boot marker
MARKER="${BANGER_FIRST_BOOT_MARKER:-/var/lib/banger/first-boot-pending}"
rm -f "$MARKER"
log "first-boot provisioning complete"
# systemd should now be installed — find and exec it
for candidate_init in /usr/lib/systemd/systemd /lib/systemd/systemd /sbin/init; do
if [ -x "$candidate_init" ]; then
log "provisioning complete; starting $candidate_init"
# Unmount our temp mounts — systemd will re-mount them properly
umount /tmp 2>/dev/null || true
umount /run 2>/dev/null || true
umount /dev 2>/dev/null || true
umount /sys 2>/dev/null || true
umount /proc 2>/dev/null || true
exec "$candidate_init" "$@"
fi
done
log "FATAL: init not found after install; dropping to shell"
exec /bin/sh

View file

@ -65,14 +65,14 @@ func TestFirstBootScriptDispatchesByDistro(t *testing.T) {
osRel string
wantRe string
}{
{"debian", `ID=debian` + "\n" + `ID_LIKE=""`, "apt-get install -y openssh-server"},
{"ubuntu", `ID=ubuntu`, "apt-get install -y openssh-server"},
{"alpine", `ID=alpine`, "apk add --no-cache openssh"},
{"fedora", `ID=fedora`, "dnf install -y openssh-server"},
{"debian", `ID=debian` + "\n" + `ID_LIKE=""`, "systemd-sysv openssh-server"},
{"ubuntu", `ID=ubuntu`, "systemd-sysv openssh-server"},
{"alpine", `ID=alpine`, "apk add"},
{"fedora", `ID=fedora`, "dnf install -y systemd openssh-server"},
{"arch", `ID=arch`, "pacman -Sy --noconfirm openssh"},
{"opensuse-leap", `ID="opensuse-leap"`, "zypper --non-interactive install -y openssh"},
{"unknown-with-debian-like", `ID=someweirddistro` + "\n" + `ID_LIKE=debian`, "apt-get install -y openssh-server"},
{"unknown-with-rhel-like", `ID=something` + "\n" + `ID_LIKE="rhel fedora"`, "dnf install -y openssh-server"},
{"opensuse-leap", `ID="opensuse-leap"`, "zypper --non-interactive install"},
{"unknown-with-debian-like", `ID=someweirddistro` + "\n" + `ID_LIKE=debian`, "systemd-sysv openssh-server"},
{"unknown-with-rhel-like", `ID=something` + "\n" + `ID_LIKE="rhel fedora"`, "dnf install -y systemd openssh-server"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
@ -88,17 +88,21 @@ func TestFirstBootScriptContainsDistroCases(t *testing.T) {
s := FirstBootScript()
for _, snippet := range []string{
"debian|ubuntu|kali|raspbian",
"apt-get install -y openssh-server",
"apt-get",
"systemd-sysv",
"openssh-server",
"alpine)",
"apk add --no-cache openssh",
"apk add",
"fedora|rhel|centos|rocky|almalinux",
"dnf install -y openssh-server",
"dnf install",
"arch|archlinux|manjaro",
"pacman -Sy --noconfirm openssh",
"pacman -Sy",
"opensuse*|suse",
"zypper --non-interactive install -y openssh",
"zypper",
`ID_LIKE`,
"RUN_PLAN",
"/usr/lib/systemd/systemd",
"mount -t proc",
} {
if !strings.Contains(s, snippet) {
t.Errorf("script missing %q", snippet)