New internal/imagepull/assets/first-boot.sh: POSIX-sh oneshot that detects the guest distro from /etc/os-release (ID + ID_LIKE fallback), installs openssh-server via the native package manager, and enables/starts sshd. Covers debian/ubuntu/kali/raspbian/pop, alpine, fedora/rhel/centos/rocky/almalinux, arch/manjaro, and opensuse/suse. Unknown distros fail clearly with a pointer at editing the script to add a branch. Marker-driven: the service has ConditionPathExists= /var/lib/banger/first-boot-pending, and the script removes the marker on success. Subsequent boots no-op. Testability seams in the script: RUN_PLAN=1 skips the sshd-already-present short-circuit and makes the dispatch echo the planned command instead of executing it. OS_RELEASE_FILE and BANGER_FIRST_BOOT_MARKER env vars override paths so the Go tests exercise the real dispatch logic in a tempdir without touching /etc or /var/lib on the host. Embedding: internal/imagepull/firstboot.go go:embeds both the script and the systemd unit; exposes FirstBootScript() and FirstBootUnit() plus the FirstBootScriptPath / FirstBootMarkerPath / FirstBootUnitName constants. Injection: InjectGuestAgents now drops /usr/local/libexec/ banger-first-boot (0755), /etc/systemd/system/banger-first-boot. service (0644), the empty /var/lib/banger/first-boot-pending marker (0644), and the multi-user.target.wants enable symlink. All uid=0, gid=0. Tests: eight-case dispatch-by-distro (debian, ubuntu, alpine, fedora, arch, opensuse, plus ID_LIKE fallbacks for weird derivatives). Script syntax check via `sh -n`. Unit-contains- expected-fields check. Existing inject round-trip test extended to assert the first-boot bits land in the ext4. Deferred: per-image FirstBootPending flag + extended SSH wait timeout at VM start. Will add if live verification (B-4) shows the naive retry UX is unacceptable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
101 lines
3 KiB
Bash
101 lines
3 KiB
Bash
#!/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.
|
|
#
|
|
# 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.
|
|
|
|
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
|
|
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
|
|
fi
|
|
|
|
DIST=""
|
|
FAMILY=""
|
|
OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"
|
|
if [ -r "$OS_RELEASE_FILE" ]; then
|
|
# shellcheck source=/dev/null
|
|
. "$OS_RELEASE_FILE"
|
|
DIST="${ID:-}"
|
|
FAMILY="${ID_LIKE:-}"
|
|
fi
|
|
|
|
log "detected distro: ID=$DIST ID_LIKE=$FAMILY"
|
|
|
|
# Dispatch. Each branch sets CMD to the single 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"
|
|
;;
|
|
alpine)
|
|
CMD="apk add --no-cache openssh"
|
|
;;
|
|
fedora|rhel|centos|rocky|almalinux)
|
|
CMD="dnf install -y openssh-server"
|
|
;;
|
|
arch|archlinux|manjaro)
|
|
CMD="pacman -Sy --noconfirm openssh"
|
|
;;
|
|
opensuse*|suse)
|
|
CMD="zypper --non-interactive install -y 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"
|
|
;;
|
|
*" rhel "* | *" fedora "*)
|
|
CMD="dnf install -y openssh-server"
|
|
;;
|
|
*" arch "*)
|
|
CMD="pacman -Sy --noconfirm openssh"
|
|
;;
|
|
*" suse "*)
|
|
CMD="zypper --non-interactive install -y 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
|
|
fi
|
|
|
|
if [ "${RUN_PLAN:-0}" = "1" ]; then
|
|
printf '%s\n' "$CMD"
|
|
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; }
|
|
|
|
rm -f "$MARKER"
|
|
log "first-boot provisioning complete"
|