banger/verify.sh
Thales Maciel 7667249b47
Install opencode in Void rootfs
Bring the experimental Void image closer to the default dev image path by installing pinned mise inside the rootfs build, using it to install opencode, and activating mise automatically for root bash sessions.

Keep the change scoped to the Void builder rather than packages.void so the image still stays language-agnostic at the package-manifest level, then clean mise download/cache artifacts before sealing the rootfs and work-seed.

Extend verify-void so the smoke path now proves mise and opencode are actually present in a fresh void-exp VM. Verified with bash -n make-rootfs-void.sh verify.sh, GOCACHE=/tmp/banger-gocache go test ./..., and make build.
2026-03-19 19:04:57 -03:00

292 lines
7.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[verify] %s\n' "$*"
}
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DEFAULT_RUNTIME_DIR="$DIR"
if [[ -d "$DIR/runtime" ]]; then
DEFAULT_RUNTIME_DIR="$DIR/runtime"
fi
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
SSH_KEY="$RUNTIME_DIR/id_ed25519"
if [[ ! -d "$RUNTIME_DIR" ]]; then
log "runtime bundle not found: $RUNTIME_DIR"
log "run 'make runtime-bundle' or set BANGER_RUNTIME_DIR"
exit 1
fi
if [[ ! -f "$SSH_KEY" ]]; then
log "ssh key not found: $SSH_KEY"
exit 1
fi
DAEMON_LOG="${XDG_STATE_HOME:-$HOME/.local/state}/banger/bangerd.log"
SSH_COMMON_ARGS=(
-F /dev/null
-i "$SSH_KEY"
-o IdentitiesOnly=yes
-o BatchMode=yes
-o PreferredAuthentications=publickey
-o PasswordAuthentication=no
-o KbdInteractiveAuthentication=no
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
)
firecracker_running() {
local pid="$1"
local api_sock="$2"
local cmdline=""
if [[ -z "$pid" || "$pid" -le 0 || -z "$api_sock" ]]; then
return 1
fi
if [[ ! -r "/proc/$pid/cmdline" ]]; then
return 1
fi
cmdline="$(cat "/proc/$pid/cmdline" 2>/dev/null | tr '\0' ' ' || true)"
[[ "$cmdline" == *firecracker* && "$cmdline" == *"$api_sock"* ]]
}
pooled_tap() {
local tap="$1"
[[ "$tap" == tap-pool-* ]]
}
wait_for_ssh() {
local guest_ip="$1"
local deadline="$2"
while ((SECONDS < deadline)); do
if ssh "${SSH_COMMON_ARGS[@]}" -o ConnectTimeout=2 "root@${guest_ip}" "true" >/dev/null 2>&1; then
return 0
fi
sleep 1
done
return 1
}
refresh_vm_metadata() {
if ! VM_JSON="$(./banger vm show "$VM_NAME" 2>/dev/null)"; then
return 1
fi
TAP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.tap_device // empty')"
VM_DIR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.vm_dir // empty')"
GUEST_IP="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.guest_ip // empty')"
API_SOCK="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.api_sock_path // empty')"
PID="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.pid // 0')"
VM_STATE="$(printf '%s\n' "$VM_JSON" | jq -r '.state // empty')"
LAST_ERROR="$(printf '%s\n' "$VM_JSON" | jq -r '.runtime.last_error // empty')"
return 0
}
wait_for_vm_ready() {
local deadline="$1"
while ((SECONDS < deadline)); do
if ! refresh_vm_metadata; then
sleep 1
continue
fi
if [[ "$VM_STATE" == "error" || -n "$LAST_ERROR" ]]; then
return 2
fi
if [[ -n "$API_SOCK" && "${PID:-0}" -gt 0 ]] && ! firecracker_running "$PID" "$API_SOCK"; then
return 3
fi
if [[ "$VM_STATE" == "running" && -n "$GUEST_IP" && -n "$TAP" && -n "$VM_DIR" && -n "$API_SOCK" && "${PID:-0}" -gt 0 ]]; then
if [[ -S "$API_SOCK" ]] && ip link show "$TAP" >/dev/null 2>&1; then
return 0
fi
fi
sleep 1
done
return 1
}
dump_diagnostics() {
log "diagnostics for $VM_NAME"
./banger vm show "$VM_NAME" || true
if [[ "${PID:-0}" -gt 0 ]]; then
log "process state for pid $PID"
ps -fp "$PID" || true
fi
log "recent firecracker log"
./banger vm logs "$VM_NAME" 2>/dev/null | tail -n 200 || true
if [[ -f "$DAEMON_LOG" ]]; then
log "recent daemon log"
tail -n 200 "$DAEMON_LOG" || true
fi
if [[ -n "${TAP:-}" ]]; then
log "tap state for $TAP"
ip link show "$TAP" || true
fi
if [[ -n "${API_SOCK:-}" ]]; then
log "api socket $API_SOCK"
ls -l "$API_SOCK" 2>/dev/null || true
fi
if (( NAT_ENABLED )) && [[ -n "${UPLINK:-}" && -n "${GUEST_IP:-}" && -n "${TAP:-}" ]]; then
log "nat rules for ${GUEST_IP} via ${UPLINK}"
sudo iptables -t nat -S POSTROUTING | grep "${GUEST_IP}/32" || true
sudo iptables -S FORWARD | grep "$TAP" || true
fi
}
usage() {
cat <<'EOF'
Usage: ./verify.sh [--nat] [--image <name>]
Run a basic smoke test for the Go VM workflow.
Use --nat to additionally verify outbound NAT and host rule cleanup.
Use --image to verify a non-default image such as void-exp.
EOF
}
NAT_ENABLED=0
IMAGE_NAME=""
BOOT_TIMEOUT_SECS="${VERIFY_BOOT_TIMEOUT_SECS:-90}"
while [[ $# -gt 0 ]]; do
case "$1" in
--nat)
NAT_ENABLED=1
shift
;;
--image)
IMAGE_NAME="${2:-}"
if [[ -z "$IMAGE_NAME" ]]; then
usage
exit 1
fi
shift 2
;;
*)
usage
exit 1
;;
esac
done
VM_NAME="verify-$(date +%s)"
VM_JSON=""
TAP=""
VM_DIR=""
GUEST_IP=""
UPLINK=""
API_SOCK=""
PID="0"
VM_STATE=""
LAST_ERROR=""
delete_vm() {
if [[ -n "${VM_NAME:-}" ]]; then
./banger vm delete "$VM_NAME"
fi
}
cleanup() {
if [[ -n "${VM_NAME:-}" ]]; then
./banger vm delete "$VM_NAME" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
log "starting VM"
CREATE_ARGS=(./banger vm create --name "$VM_NAME")
if [[ -n "$IMAGE_NAME" ]]; then
CREATE_ARGS+=(--image "$IMAGE_NAME")
fi
if (( NAT_ENABLED )); then
CREATE_ARGS+=(--nat)
fi
"${CREATE_ARGS[@]}" >/dev/null
BOOT_DEADLINE=$((SECONDS + BOOT_TIMEOUT_SECS))
log "waiting for VM runtime readiness"
if wait_for_vm_ready "$BOOT_DEADLINE"; then
:
else
status=$?
case "$status" in
2) log "vm entered an error state before becoming ready" ;;
3) log "firecracker exited before the guest became ready" ;;
*) log "vm did not become ready before timeout" ;;
esac
dump_diagnostics
exit 1
fi
if (( NAT_ENABLED )); then
UPLINK="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')"
if [[ -z "$UPLINK" ]]; then
log "failed to detect uplink interface"
exit 1
fi
log "asserting NAT rules are installed"
sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE
sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT
sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT
fi
log "asserting VM is reachable via SSH"
if ! wait_for_ssh "$GUEST_IP" "$BOOT_DEADLINE"; then
log "ssh did not become ready for ${GUEST_IP}"
dump_diagnostics
exit 1
fi
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "uname -a" >/dev/null
if [[ "$IMAGE_NAME" == "void-exp" ]]; then
log "asserting mise and opencode are available in the Void guest"
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "command -v mise >/dev/null 2>&1 && command -v opencode >/dev/null 2>&1 && mise --version >/dev/null 2>&1 && opencode --version >/dev/null 2>&1" >/dev/null
fi
if (( NAT_ENABLED )); then
log "asserting VM has outbound network access"
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "curl -fsS https://example.com >/dev/null" >/dev/null
fi
log "cleaning up VM"
if ! delete_vm; then
log "vm delete failed for $VM_NAME"
dump_diagnostics
exit 1
fi
log "asserting cleanup success"
if ./banger vm show "$VM_NAME" >/dev/null 2>&1; then
log "vm still exists after delete: $VM_NAME"
exit 1
fi
if ip link show "$TAP" >/dev/null 2>&1; then
if pooled_tap "$TAP"; then
log "tap returned to idle pool: $TAP"
else
log "tap still exists: $TAP"
exit 1
fi
fi
if [[ -d "$VM_DIR" ]]; then
log "vm dir still exists: $VM_DIR"
exit 1
fi
if (( NAT_ENABLED )); then
if sudo iptables -t nat -C POSTROUTING -s "${GUEST_IP}/32" -o "$UPLINK" -j MASQUERADE 2>/dev/null; then
log "nat rule still exists for ${GUEST_IP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$TAP" -o "$UPLINK" -j ACCEPT 2>/dev/null; then
log "forward-out rule still exists for ${TAP}"
exit 1
fi
if sudo iptables -C FORWARD -i "$UPLINK" -o "$TAP" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then
log "forward-in rule still exists for ${TAP}"
exit 1
fi
fi
log "ok"