main
  1#!/usr/bin/env bash
  2# Test an Alpine appliance image in QEMU.
  3# Usage: ./alpine/test.sh <hostname> [stop|validate|build-qemu]
  4set -euo pipefail
  5
  6SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  7HOST="${1:?Usage: $0 <hostname> [stop|validate]}"
  8ACTION="${2:-start}"
  9IMAGE_DIR="$SCRIPT_DIR/images/$HOST"
 10QEMU_DIR="$IMAGE_DIR/qemu"
 11MONITOR_SOCK="/tmp/qemu-alpine-${HOST}.sock"
 12SSH_PORT="${QEMU_SSH_PORT:-2222}"
 13CONTAINER_ENGINE="${CONTAINER_ENGINE:-podman}"
 14
 15if [ "$ACTION" = "stop" ]; then
 16    if [ -S "$MONITOR_SOCK" ]; then
 17        echo "quit" | socat - UNIX-CONNECT:"$MONITOR_SOCK" 2>/dev/null
 18        echo "==> QEMU for $HOST stopped"
 19    else
 20        echo "No running QEMU found for $HOST, trying pkill..."
 21        pkill -f "qemu.*alpine-${HOST}" 2>/dev/null || echo "Nothing to kill"
 22    fi
 23    exit 0
 24fi
 25
 26if [ "$ACTION" = "validate" ]; then
 27    echo "==> Validating $HOST (via SSH on localhost:$SSH_PORT)"
 28    echo ""
 29    SSH_CMD="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $SSH_PORT root@localhost"
 30
 31    run_check() {
 32        local desc="$1"
 33        shift
 34        printf "  %-45s"  "$desc"
 35        if output=$($SSH_CMD "$@" 2>/dev/null); then
 36            echo "✓"
 37            # shellcheck disable=SC2001
 38            [ -n "${VERBOSE:-}" ] && echo "$output" | sed 's/^/    /'
 39            return 0
 40        else
 41            echo "✗"
 42            # shellcheck disable=SC2001
 43            [ -n "${VERBOSE:-}" ] && echo "$output" | sed 's/^/    /'
 44            return 1
 45        fi
 46    }
 47
 48    FAILURES=0
 49
 50    # Basic system checks
 51    run_check "SSH access" "echo ok" || FAILURES=$((FAILURES + 1))
 52    run_check "Hostname is $HOST" "test \$(hostname) = $HOST" || FAILURES=$((FAILURES + 1))
 53    run_check "OpenRC running" "rc-status --nocolor" || FAILURES=$((FAILURES + 1))
 54
 55    # Service checks
 56    run_check "sshd running" "rc-service sshd status" || FAILURES=$((FAILURES + 1))
 57    run_check "chronyd running" "rc-service chronyd status" || FAILURES=$((FAILURES + 1))
 58    run_check "crond running" "rc-service crond status" || FAILURES=$((FAILURES + 1))
 59    run_check "node-exporter running" "rc-service node-exporter status" || FAILURES=$((FAILURES + 1))
 60
 61    # Host-specific checks
 62    case "$HOST" in
 63        athena|demeter)
 64            run_check "named running" "rc-service named status" || FAILURES=$((FAILURES + 1))
 65            run_check "BIND config valid" "named-checkconf /etc/bind/named.conf" || FAILURES=$((FAILURES + 1))
 66            if [ -d "$SCRIPT_DIR/generated/zones" ]; then
 67                run_check "DNS zones present" "ls /etc/bind/zones/db.* >/dev/null" || FAILURES=$((FAILURES + 1))
 68                run_check "DNS resolves 'home'" "dig @127.0.0.1 home SOA +short" || FAILURES=$((FAILURES + 1))
 69            else
 70                echo "  DNS zones not generated (skipping zone checks)"
 71            fi
 72            ;;&
 73        demeter)
 74            run_check "mosquitto running" "rc-service mosquitto status" || FAILURES=$((FAILURES + 1))
 75            ;;
 76        aix)
 77            run_check "samba running" "rc-service samba status" || FAILURES=$((FAILURES + 1))
 78            run_check "dnsmasq running" "rc-service dnsmasq status" || FAILURES=$((FAILURES + 1))
 79            ;;
 80    esac
 81
 82    # Secrets init script (will fail without persist partition, that's OK)
 83    run_check "secrets init script exists" "test -x /etc/init.d/secrets" || FAILURES=$((FAILURES + 1))
 84
 85    # Auth
 86    run_check "root authorized_keys exists" "test -s /root/.ssh/authorized_keys" || FAILURES=$((FAILURES + 1))
 87    run_check "vincent authorized_keys exists" "test -s /home/vincent/.ssh/authorized_keys" || FAILURES=$((FAILURES + 1))
 88
 89    # Network config
 90    run_check "WireGuard config exists" "test -f /etc/wireguard/wg0.conf" || FAILURES=$((FAILURES + 1))
 91
 92    # Prometheus endpoints
 93    run_check "node-exporter responds" "wget -qO- http://127.0.0.1:9100/metrics | head -1" || FAILURES=$((FAILURES + 1))
 94
 95    echo ""
 96    if [ "$FAILURES" -eq 0 ]; then
 97        echo "  ✓ All checks passed!"
 98    else
 99        echo "$FAILURES check(s) failed"
100    fi
101    exit "$FAILURES"
102fi
103
104# --- Build QEMU-specific initramfs ---
105# The production image uses linux-rpi (no virtio drivers).
106# For QEMU we need linux-virt + DHCP networking.
107build_qemu_image() {
108    echo "==> Building QEMU test image for $HOST..."
109    mkdir -p "$QEMU_DIR"
110
111    # Resolve Nix store symlinks
112    GENERATED_MOUNTS=""
113    if [ -L "$SCRIPT_DIR/generated/zones" ]; then
114        REAL_ZONES=$(readlink -f "$SCRIPT_DIR/generated/zones")
115        GENERATED_MOUNTS="$GENERATED_MOUNTS -v $REAL_ZONES:$REAL_ZONES:ro"
116    fi
117    if [ -L "$SCRIPT_DIR/generated/wireguard" ]; then
118        REAL_WG=$(readlink -f "$SCRIPT_DIR/generated/wireguard")
119        GENERATED_MOUNTS="$GENERATED_MOUNTS -v $REAL_WG:$REAL_WG:ro"
120    fi
121
122    # shellcheck disable=SC2086,SC2016
123    $CONTAINER_ENGINE run --rm \
124        --platform linux/arm64 \
125        -v "$SCRIPT_DIR":/build:Z \
126        $GENERATED_MOUNTS \
127        -e HOST="$HOST" \
128        -w /tmp \
129        docker.io/library/alpine:3.21 \
130        sh -c '
131set -e
132HOST="$1"
133BUILD_DIR="/build"
134COMMON_DIR="$BUILD_DIR/common"
135HOST_DIR="$BUILD_DIR/hosts/$HOST"
136QEMU_DIR="$BUILD_DIR/images/$HOST/qemu"
137
138apk add --no-cache cpio gzip findutils >/dev/null
139
140ROOTFS=$(mktemp -d)
141
142# Copy skeletons (same as production)
143[ -d "$COMMON_DIR/root" ] && cp -a "$COMMON_DIR/root/." "$ROOTFS/"
144[ -d "$HOST_DIR/root" ] && cp -a "$HOST_DIR/root/." "$ROOTFS/"
145
146# DNS zones
147if [ -d "$BUILD_DIR/generated/zones" ]; then
148    mkdir -p "$ROOTFS/etc/bind/zones"
149    cp -aL "$BUILD_DIR/generated/zones/." "$ROOTFS/etc/bind/zones/"
150fi
151
152# Override network config for QEMU (DHCP)
153cat > "$ROOTFS/etc/network/interfaces" << IFACES
154auto lo
155iface lo inet loopback
156
157auto eth0
158iface eth0 inet dhcp
159IFACES
160
161# Merge packages — replace linux-rpi with linux-virt
162PACKAGES=""
163[ -f "$COMMON_DIR/packages" ] && PACKAGES="$(grep -v "^#" "$COMMON_DIR/packages" | grep -v "^$" | grep -v "linux-rpi" | tr "\n" " ")"
164[ -f "$HOST_DIR/packages" ] && PACKAGES="$PACKAGES $(grep -v "^#" "$HOST_DIR/packages" | grep -v "^$" | tr "\n" " ")"
165PACKAGES="$PACKAGES linux-virt"
166
167# Install
168mkdir -p "$ROOTFS/etc/apk" "$ROOTFS/etc/apk/keys"
169echo "https://dl-cdn.alpinelinux.org/alpine/v3.21/main" > "$ROOTFS/etc/apk/repositories"
170echo "https://dl-cdn.alpinelinux.org/alpine/v3.21/community" >> "$ROOTFS/etc/apk/repositories"
171cp /etc/apk/keys/* "$ROOTFS/etc/apk/keys/"
172apk add --root "$ROOTFS" --initdb --no-cache $PACKAGES
173
174# Run setup
175SETUP=$(mktemp)
176echo "#!/bin/sh" > "$SETUP"
177echo "set -e" >> "$SETUP"
178[ -f "$COMMON_DIR/setup.sh" ] && cat "$COMMON_DIR/setup.sh" >> "$SETUP"
179[ -f "$HOST_DIR/setup.sh" ] && cat "$HOST_DIR/setup.sh" >> "$SETUP"
180chmod +x "$SETUP"
181cp "$SETUP" "$ROOTFS/tmp/setup.sh"
182find "$ROOTFS/etc/init.d" -type f -exec chmod +x {} \; 2>/dev/null || true
183chroot "$ROOTFS" /tmp/setup.sh
184rm -f "$ROOTFS/tmp/setup.sh"
185
186# Extract kernel
187cp "$ROOTFS/boot/vmlinuz-virt" "$QEMU_DIR/"
188rm -rf "$ROOTFS/boot" "$ROOTFS/usr/share/u-boot"
189
190# Create initramfs
191cd "$ROOTFS"
192find . -print | cpio -o -H newc | gzip > "$QEMU_DIR/initramfs"
193
194echo "QEMU image ready"
195ls -lh "$QEMU_DIR/"
196' -- "$HOST"
197}
198
199# --- Start QEMU ---
200if [ ! -f "$QEMU_DIR/initramfs" ] || [ ! -f "$QEMU_DIR/vmlinuz-virt" ]; then
201    build_qemu_image
202fi
203
204# Rebuild if production image is newer
205if [ "$IMAGE_DIR/boot/initramfs" -nt "$QEMU_DIR/initramfs" ] 2>/dev/null; then
206    echo "==> Production image is newer, rebuilding QEMU image..."
207    build_qemu_image
208fi
209
210echo "==> Testing $HOST in QEMU"
211echo "    SSH:      ssh -p $SSH_PORT -o StrictHostKeyChecking=no root@localhost"
212echo "    Console:  root / alpine"
213echo "    Stop:     ./alpine/test.sh $HOST stop"
214echo "    Validate: ./alpine/test.sh $HOST validate  (in another terminal)"
215echo ""
216qemu-system-aarch64 -M virt -cpu cortex-a72 -m 1G -nographic \
217    -monitor unix:"$MONITOR_SOCK",server,nowait \
218    -kernel "$QEMU_DIR/vmlinuz-virt" \
219    -initrd "$QEMU_DIR/initramfs" \
220    -netdev user,id=net0,hostfwd=tcp::"$SSH_PORT"-:22 \
221    -device virtio-net-pci,netdev=net0 \
222    -append "console=ttyAMA0 rdinit=/sbin/init"