Reorganize the source checkout layout
Separate tracked source from generated artifacts so the repo root stops accumulating helper scripts, manifests, and local runtime outputs. Move manual shell entrypoints under scripts/, manifests under config/, and the Firecracker API reference under docs/reference/. Make build and runtimebundle now target build/bin, build/runtime, and build/dist as the canonical source-checkout paths. Update runtime discovery, helper scripts, tests, and docs to follow the new layout while keeping legacy source-checkout runtime fallbacks for existing local bundles during migration. Validated with bash -n on the moved scripts, make build, and GOCACHE=/tmp/banger-gocache go test ./....
This commit is contained in:
parent
2362d0ae39
commit
01c7cb5e65
23 changed files with 296 additions and 186 deletions
|
|
@ -52,7 +52,13 @@ fi
|
|||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
BANGER_BIN="${BANGER_BIN:-$REPO_ROOT/banger}"
|
||||
if [[ -z "${BANGER_BIN:-}" ]]; then
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
BANGER_BIN="$REPO_ROOT/build/bin/banger"
|
||||
else
|
||||
BANGER_BIN="$REPO_ROOT/banger"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -x "$BANGER_BIN" ]]; then
|
||||
log "banger binary not found: $BANGER_BIN"
|
||||
log "run 'make build' or set BANGER_BIN"
|
||||
|
|
|
|||
593
scripts/customize.sh
Executable file
593
scripts/customize.sh
Executable file
|
|
@ -0,0 +1,593 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[customize] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/customize.sh <base-rootfs> [--out <path>] [--size <size>] [--kernel <path>] [--initrd <path>] [--docker] [--modules <dir>]
|
||||
|
||||
Creates a copy of rootfs.ext4, optionally resizes it, boots a VM using the
|
||||
copy as a writable rootfs, then applies base configuration and packages.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_size() {
|
||||
local raw="$1"
|
||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
||||
local num="${BASH_REMATCH[1]}"
|
||||
local unit="${BASH_REMATCH[2]}"
|
||||
case "$unit" in
|
||||
K) echo $((num * 1024)) ;;
|
||||
M|"") echo $((num * 1024 * 1024)) ;;
|
||||
G) echo $((num * 1024 * 1024 * 1024)) ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
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
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
|
||||
VM_ROOT="$STATE/vms"
|
||||
mkdir -p "$VM_ROOT"
|
||||
|
||||
BUNDLE_METADATA="$RUNTIME_DIR/bundle.json"
|
||||
|
||||
bundle_path() {
|
||||
local key="$1"
|
||||
local fallback="$2"
|
||||
local rel=""
|
||||
|
||||
if [[ -f "$BUNDLE_METADATA" ]] && command -v jq >/dev/null 2>&1; then
|
||||
rel="$(jq -r --arg key "$key" '.[$key] // empty' "$BUNDLE_METADATA" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -n "$rel" && "$rel" != "null" ]]; then
|
||||
printf '%s\n' "$RUNTIME_DIR/$rel"
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "$fallback"
|
||||
}
|
||||
|
||||
BASE_ROOTFS="$RUNTIME_DIR/rootfs.ext4"
|
||||
FC_BIN="$RUNTIME_DIR/firecracker"
|
||||
|
||||
KERNEL="$(bundle_path default_kernel "$RUNTIME_DIR/wtf/root/boot/vmlinux-6.8.0-94-generic")"
|
||||
INITRD="$(bundle_path default_initrd "$RUNTIME_DIR/wtf/root/boot/initrd.img-6.8.0-94-generic")"
|
||||
SSH_KEY="$RUNTIME_DIR/id_ed25519"
|
||||
VSOCK_AGENT="$(bundle_path vsock_agent_path "$RUNTIME_DIR/banger-vsock-agent")"
|
||||
if [[ "$VSOCK_AGENT" == "$RUNTIME_DIR/banger-vsock-agent" && ! -x "$VSOCK_AGENT" ]]; then
|
||||
VSOCK_AGENT="$(bundle_path vsock_ping_helper_path "$RUNTIME_DIR/banger-vsock-pingd")"
|
||||
fi
|
||||
|
||||
BR_DEV="br-fc"
|
||||
BR_IP="172.16.0.1"
|
||||
CIDR="24"
|
||||
DNS_SERVER="1.1.1.1"
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; install/build banger or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
NAT_ACTIVE=0
|
||||
|
||||
banger_nat() {
|
||||
local action="$1"
|
||||
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
|
||||
}
|
||||
|
||||
BASE_ROOTFS=""
|
||||
OUT_ROOTFS=""
|
||||
SIZE_SPEC=""
|
||||
INSTALL_DOCKER=0
|
||||
MISE_VERSION="v2025.12.0"
|
||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
||||
MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"'
|
||||
TMUX_PLUGIN_DIR="/root/.tmux/plugins"
|
||||
TMUX_RESURRECT_DIR="/root/.tmux/resurrect"
|
||||
TMUX_TPM_REPO="https://github.com/tmux-plugins/tpm"
|
||||
TMUX_RESURRECT_REPO="https://github.com/tmux-plugins/tmux-resurrect"
|
||||
TMUX_CONTINUUM_REPO="https://github.com/tmux-plugins/tmux-continuum"
|
||||
TMUX_MANAGED_START="# >>> banger tmux plugins >>>"
|
||||
TMUX_MANAGED_END="# <<< banger tmux plugins <<<"
|
||||
MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")"
|
||||
PACKAGES_FILE="$(banger_packages_file)"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out)
|
||||
OUT_ROOTFS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--size)
|
||||
SIZE_SPEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--kernel)
|
||||
KERNEL="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--initrd)
|
||||
INITRD="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--docker)
|
||||
INSTALL_DOCKER=1
|
||||
shift
|
||||
;;
|
||||
--modules)
|
||||
MODULES_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
BASE_ROOTFS="$1"
|
||||
shift
|
||||
else
|
||||
log "unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$BASE_ROOTFS" ]]; then
|
||||
log "base rootfs not found: $BASE_ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$OUT_ROOTFS" ]]; then
|
||||
base_dir="$(dirname "$BASE_ROOTFS")"
|
||||
base_name="$(basename "$BASE_ROOTFS")"
|
||||
OUT_ROOTFS="${base_dir}/docker-${base_name}"
|
||||
fi
|
||||
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
|
||||
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
|
||||
else
|
||||
WORK_SEED="${OUT_ROOTFS}.work-seed"
|
||||
fi
|
||||
if [[ ! -f "$KERNEL" ]]; then
|
||||
log "kernel not found: $KERNEL"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$INITRD" && ! -f "$INITRD" ]]; then
|
||||
log "initrd not found: $INITRD"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n "$MODULES_DIR" && ! -d "$MODULES_DIR" ]]; then
|
||||
log "modules dir not found: $MODULES_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
||||
log "output rootfs already exists: $OUT_ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v resize2fs >/dev/null 2>&1; then
|
||||
log "resize2fs required"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
log "jq required"
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v sha256sum >/dev/null 2>&1; then
|
||||
log "sha256sum required to record package manifest metadata"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$PACKAGES_FILE" ]]; then
|
||||
log "package manifest not found: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
||||
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
||||
log "run 'make build' or refresh the runtime bundle"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
APT_PACKAGES=()
|
||||
if ! banger_packages_read_array APT_PACKAGES "$PACKAGES_FILE"; then
|
||||
log "package manifest is empty: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if ! PACKAGES_HASH="$(printf '%s\n' "${APT_PACKAGES[@]}" | banger_packages_hash_stream)"; then
|
||||
log "failed to hash package manifest: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
printf -v APT_PACKAGES_ESCAPED '%q ' "${APT_PACKAGES[@]}"
|
||||
|
||||
log "copying base rootfs to $OUT_ROOTFS"
|
||||
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
|
||||
|
||||
if [[ -n "$SIZE_SPEC" ]]; then
|
||||
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
|
||||
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
|
||||
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
|
||||
log "size must be >= base image size"
|
||||
exit 1
|
||||
fi
|
||||
log "resizing rootfs to $SIZE_SPEC"
|
||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
||||
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
|
||||
resize2fs "$OUT_ROOTFS" >/dev/null
|
||||
fi
|
||||
|
||||
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
|
||||
VM_TAG="${VM_ID:0:8}"
|
||||
VM_NAME="customize-${VM_TAG}"
|
||||
VM_DIR="$VM_ROOT/$VM_ID"
|
||||
mkdir -p "$VM_DIR"
|
||||
|
||||
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
|
||||
LOG_FILE="$VM_DIR/firecracker.log"
|
||||
TAP_DEV="tap-fc-$VM_TAG"
|
||||
|
||||
# Allocate guest IP
|
||||
NEXT_IP_FILE="$STATE/next_ip"
|
||||
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
|
||||
GUEST_IP="172.16.0.$NEXT_IP"
|
||||
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
|
||||
|
||||
sudo -v
|
||||
|
||||
cleanup() {
|
||||
sudo kill "${FC_PID:-}" 2>/dev/null || true
|
||||
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
|
||||
banger_nat down >/dev/null 2>&1 || true
|
||||
fi
|
||||
sudo ip link del "$TAP_DEV" 2>/dev/null || true
|
||||
rm -f "$API_SOCK"
|
||||
rm -rf "$VM_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sudo mkdir -p "$(dirname "$API_SOCK")"
|
||||
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
|
||||
|
||||
# Host bridge
|
||||
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
|
||||
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
|
||||
sudo ip link add name "$BR_DEV" type bridge
|
||||
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
|
||||
sudo ip link set "$BR_DEV" up
|
||||
else
|
||||
sudo ip link set "$BR_DEV" up
|
||||
fi
|
||||
|
||||
log "creating tap device $TAP_DEV"
|
||||
TAP_USER="${SUDO_UID:-$(id -u)}"
|
||||
TAP_GROUP="${SUDO_GID:-$(id -g)}"
|
||||
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
|
||||
sudo ip link set "$TAP_DEV" master "$BR_DEV"
|
||||
sudo ip link set "$TAP_DEV" up
|
||||
sudo ip link set "$BR_DEV" up
|
||||
|
||||
log "starting firecracker process"
|
||||
rm -f "$API_SOCK"
|
||||
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
|
||||
FC_PID="$!"
|
||||
|
||||
log "waiting for firecracker api socket"
|
||||
for _ in $(seq 1 200); do
|
||||
[[ -S "$API_SOCK" ]] && break
|
||||
sleep 0.02
|
||||
done
|
||||
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
|
||||
|
||||
log "configuring machine"
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"vcpu_count": 2,
|
||||
"mem_size_mib": 1024,
|
||||
"smt": false
|
||||
}' >/dev/null
|
||||
|
||||
KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount"
|
||||
|
||||
INITRD_JSON=""
|
||||
if [[ -n "$INITRD" ]]; then
|
||||
INITRD_JSON=", \"initrd_path\": \"$INITRD\""
|
||||
fi
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"kernel_image_path\": \"$KERNEL\",
|
||||
\"boot_args\": \"$KCMD\"${INITRD_JSON}
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"drive_id\": \"rootfs\",
|
||||
\"path_on_host\": \"$OUT_ROOTFS\",
|
||||
\"is_root_device\": true,
|
||||
\"is_read_only\": false
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"iface_id\": \"eth0\",
|
||||
\"host_dev_name\": \"$TAP_DEV\"
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "action_type": "InstanceStart" }' >/dev/null
|
||||
|
||||
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
|
||||
if [[ -n "$SUDO_CHILD_PID" ]]; then
|
||||
FC_PID="$SUDO_CHILD_PID"
|
||||
fi
|
||||
|
||||
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
|
||||
CREATED_AT="$(date -Iseconds)"
|
||||
jq -n \
|
||||
--arg id "$VM_ID" \
|
||||
--arg name "$VM_NAME" \
|
||||
--arg pid "$FC_PID" \
|
||||
--arg created_at "$CREATED_AT" \
|
||||
--arg guest_ip "$GUEST_IP" \
|
||||
--arg tap "$TAP_DEV" \
|
||||
--arg api_sock "$API_SOCK" \
|
||||
--arg log "$LOG_FILE" \
|
||||
--arg rootfs "$OUT_ROOTFS" \
|
||||
--arg kernel "$KERNEL" \
|
||||
--argjson config "$VM_CONFIG_JSON" \
|
||||
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
|
||||
> "$VM_DIR/vm.json"
|
||||
|
||||
log "enabling NAT for customization"
|
||||
banger_nat up >/dev/null
|
||||
NAT_ACTIVE=1
|
||||
|
||||
log "waiting for SSH"
|
||||
SSH_READY=0
|
||||
for _ in $(seq 1 60); do
|
||||
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
|
||||
SSH_READY=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [[ "$SSH_READY" -ne 1 ]]; then
|
||||
log "ssh did not become ready on $GUEST_IP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "configuring guest"
|
||||
log "installing vsock agent"
|
||||
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"$VSOCK_AGENT" "root@${GUEST_IP}:/usr/local/bin/banger-vsock-agent" >/dev/null
|
||||
|
||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" bash -lc "set -e
|
||||
printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf
|
||||
echo \"$VM_NAME\" > /etc/hostname
|
||||
printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts
|
||||
touch /etc/fstab
|
||||
sed -i '\|^/dev/vdb[[:space:]]\+/home[[:space:]]|d; \|^/dev/vdc[[:space:]]\+/var[[:space:]]|d' /etc/fstab
|
||||
if ! grep -q '^tmpfs /run ' /etc/fstab; then
|
||||
echo 'tmpfs /run tmpfs defaults,nodev,nosuid,mode=0755 0 0' >> /etc/fstab
|
||||
fi
|
||||
if ! grep -q '^tmpfs /tmp ' /etc/fstab; then
|
||||
echo 'tmpfs /tmp tmpfs defaults,nodev,nosuid,mode=1777 0 0' >> /etc/fstab
|
||||
fi
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install ${APT_PACKAGES_ESCAPED}
|
||||
curl -fsSL https://mise.run | MISE_INSTALL_PATH=\"$MISE_INSTALL_PATH\" MISE_VERSION=\"$MISE_VERSION\" sh
|
||||
\"$MISE_INSTALL_PATH\" use -g github:anomalyco/opencode
|
||||
\"$MISE_INSTALL_PATH\" reshim
|
||||
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
|
||||
echo 'opencode shim not found after mise install' >&2
|
||||
exit 1
|
||||
fi
|
||||
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
||||
mkdir -p /etc/profile.d
|
||||
cat > /etc/profile.d/mise.sh <<'MISEPROFILE'
|
||||
if [ -n \"\${BASH_VERSION:-}\" ] && [ -x \"$MISE_INSTALL_PATH\" ]; then
|
||||
eval \"\$($MISE_INSTALL_PATH activate bash)\"
|
||||
fi
|
||||
MISEPROFILE
|
||||
chmod 0644 /etc/profile.d/mise.sh
|
||||
touch /etc/bash.bashrc
|
||||
if ! grep -Fqx '$MISE_ACTIVATE_LINE' /etc/bash.bashrc; then
|
||||
printf '\n%s\n' '$MISE_ACTIVATE_LINE' >> /etc/bash.bashrc
|
||||
fi
|
||||
if [[ \"$INSTALL_DOCKER\" == \"1\" ]]; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y remove containerd || true
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install docker.io
|
||||
fi
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl enable --now docker || true
|
||||
fi
|
||||
fi
|
||||
rm -f /root/get-docker /root/get-docker.sh /tmp/get-docker /tmp/get-docker.sh
|
||||
chmod 0755 /usr/local/bin/banger-vsock-agent
|
||||
mkdir -p /etc/modules-load.d /etc/systemd/system
|
||||
cat > /etc/systemd/system/banger-opencode.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Banger opencode server
|
||||
After=network.target
|
||||
RequiresMountsFor=/root
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=HOME=/root
|
||||
WorkingDirectory=/root
|
||||
ExecStart=/usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 0644 /etc/systemd/system/banger-opencode.service
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now banger-opencode.service || true
|
||||
fi
|
||||
cat > /etc/modules-load.d/banger-vsock.conf <<'EOF'
|
||||
vsock
|
||||
vmw_vsock_virtio_transport
|
||||
EOF
|
||||
chmod 0644 /etc/modules-load.d/banger-vsock.conf
|
||||
cat > /etc/systemd/system/banger-vsock-agent.service <<'EOF'
|
||||
[Unit]
|
||||
Description=Banger vsock agent
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/banger-vsock-agent
|
||||
Restart=on-failure
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
chmod 0644 /etc/systemd/system/banger-vsock-agent.service
|
||||
if command -v systemctl >/dev/null 2>&1; then
|
||||
systemctl daemon-reload || true
|
||||
systemctl enable --now banger-vsock-agent.service || true
|
||||
fi
|
||||
git config --system init.defaultBranch main
|
||||
"
|
||||
|
||||
log "configuring tmux resurrect"
|
||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" bash -se <<EOF
|
||||
set -euo pipefail
|
||||
|
||||
install_tmux_plugin() {
|
||||
local dir="\$1"
|
||||
local repo="\$2"
|
||||
|
||||
if [[ -d "\$dir/.git" ]]; then
|
||||
git -C "\$dir" fetch --depth 1 origin
|
||||
git -C "\$dir" reset --hard FETCH_HEAD
|
||||
else
|
||||
rm -rf "\$dir"
|
||||
git clone --depth 1 "\$repo" "\$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
mkdir -p "$TMUX_PLUGIN_DIR" "$TMUX_RESURRECT_DIR"
|
||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tpm" "$TMUX_TPM_REPO"
|
||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-resurrect" "$TMUX_RESURRECT_REPO"
|
||||
install_tmux_plugin "$TMUX_PLUGIN_DIR/tmux-continuum" "$TMUX_CONTINUUM_REPO"
|
||||
|
||||
TMUX_CONF="/root/.tmux.conf"
|
||||
tmp_tmux_conf="\$(mktemp)"
|
||||
if [[ -f "\$TMUX_CONF" ]]; then
|
||||
awk -v begin="$TMUX_MANAGED_START" -v end="$TMUX_MANAGED_END" '
|
||||
\$0 == begin { skip = 1; next }
|
||||
\$0 == end { skip = 0; next }
|
||||
!skip { print }
|
||||
' "\$TMUX_CONF" > "\$tmp_tmux_conf"
|
||||
else
|
||||
: > "\$tmp_tmux_conf"
|
||||
fi
|
||||
if [[ -s "\$tmp_tmux_conf" ]]; then
|
||||
printf '\n' >> "\$tmp_tmux_conf"
|
||||
fi
|
||||
cat >> "\$tmp_tmux_conf" <<'TMUXCONF'
|
||||
$TMUX_MANAGED_START
|
||||
set -g @plugin 'tmux-plugins/tpm'
|
||||
set -g @plugin 'tmux-plugins/tmux-resurrect'
|
||||
set -g @plugin 'tmux-plugins/tmux-continuum'
|
||||
set -g @continuum-save-interval '15'
|
||||
set -g @continuum-restore 'off'
|
||||
set -g @resurrect-dir '/root/.tmux/resurrect'
|
||||
run '~/.tmux/plugins/tpm/tpm'
|
||||
$TMUX_MANAGED_END
|
||||
TMUXCONF
|
||||
mv "\$tmp_tmux_conf" "\$TMUX_CONF"
|
||||
chmod 0644 "\$TMUX_CONF"
|
||||
EOF
|
||||
|
||||
if [[ -n "$MODULES_DIR" ]]; then
|
||||
MODULES_BASE="$(basename "$MODULES_DIR")"
|
||||
log "copying kernel modules ($MODULES_BASE) into guest"
|
||||
tar -C "$(dirname "$MODULES_DIR")" -cf - "$MODULES_BASE" | \
|
||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" bash -lc "set -e
|
||||
mkdir -p /lib/modules
|
||||
tar -C /lib/modules -xf -
|
||||
depmod -a \"$MODULES_BASE\"
|
||||
mkdir -p /etc/modules-load.d
|
||||
printf 'nf_tables\nnft_chain_nat\nveth\nbr_netfilter\noverlay\n' > /etc/modules-load.d/docker-netfilter.conf
|
||||
mkdir -p /etc/sysctl.d
|
||||
cat > /etc/sysctl.d/99-docker.conf <<'SYSCTL'
|
||||
net.bridge.bridge-nf-call-iptables = 1
|
||||
net.bridge.bridge-nf-call-ip6tables = 1
|
||||
net.ipv4.ip_forward = 1
|
||||
SYSCTL
|
||||
sysctl --system >/dev/null 2>&1 || true
|
||||
sync
|
||||
"
|
||||
fi
|
||||
|
||||
log "shutting down guest"
|
||||
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" bash -lc "sync" || true
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "action_type": "SendCtrlAltDel" }' >/dev/null || true
|
||||
for _ in $(seq 1 200); do
|
||||
if ! ps -p "$FC_PID" >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.05
|
||||
done
|
||||
banger_write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
|
||||
log "building work seed $WORK_SEED"
|
||||
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
|
||||
log "done"
|
||||
322
scripts/interactive.sh
Executable file
322
scripts/interactive.sh
Executable file
|
|
@ -0,0 +1,322 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[interactive] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/interactive.sh <base-rootfs> [--out <path>] [--size <size>]
|
||||
|
||||
Creates a writable copy of the base rootfs and boots a VM so you can
|
||||
customize it manually over SSH. No automatic package/config changes
|
||||
are applied.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_size() {
|
||||
local raw="$1"
|
||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
||||
local num="${BASH_REMATCH[1]}"
|
||||
local unit="${BASH_REMATCH[2]}"
|
||||
case "$unit" in
|
||||
K) echo $((num * 1024)) ;;
|
||||
M|"") echo $((num * 1024 * 1024)) ;;
|
||||
G) echo $((num * 1024 * 1024 * 1024)) ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
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
|
||||
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/interactive}"
|
||||
VM_ROOT="$STATE/vms"
|
||||
mkdir -p "$VM_ROOT"
|
||||
|
||||
BUNDLE_METADATA="$RUNTIME_DIR/bundle.json"
|
||||
|
||||
bundle_path() {
|
||||
local key="$1"
|
||||
local fallback="$2"
|
||||
local rel=""
|
||||
|
||||
if [[ -f "$BUNDLE_METADATA" ]] && command -v jq >/dev/null 2>&1; then
|
||||
rel="$(jq -r --arg key "$key" '.[$key] // empty' "$BUNDLE_METADATA" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -n "$rel" && "$rel" != "null" ]]; then
|
||||
printf '%s\n' "$RUNTIME_DIR/$rel"
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "$fallback"
|
||||
}
|
||||
|
||||
FC_BIN="$RUNTIME_DIR/firecracker"
|
||||
KERNEL="$(bundle_path default_kernel "$RUNTIME_DIR/wtf/root/boot/vmlinux-6.8.0-94-generic")"
|
||||
INITRD="$(bundle_path default_initrd "$RUNTIME_DIR/wtf/root/boot/initrd.img-6.8.0-94-generic")"
|
||||
SSH_KEY="$RUNTIME_DIR/id_ed25519"
|
||||
|
||||
BR_DEV="br-fc"
|
||||
BR_IP="172.16.0.1"
|
||||
CIDR="24"
|
||||
DNS_SERVER="1.1.1.1"
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; install/build banger or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
NAT_ACTIVE=0
|
||||
|
||||
banger_nat() {
|
||||
local action="$1"
|
||||
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
|
||||
}
|
||||
|
||||
BASE_ROOTFS=""
|
||||
OUT_ROOTFS=""
|
||||
SIZE_SPEC=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out)
|
||||
OUT_ROOTFS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--size)
|
||||
SIZE_SPEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
BASE_ROOTFS="$1"
|
||||
shift
|
||||
else
|
||||
log "unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$BASE_ROOTFS" ]]; then
|
||||
log "base rootfs not found: $BASE_ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$KERNEL" ]]; then
|
||||
log "kernel not found: $KERNEL"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$INITRD" ]]; then
|
||||
log "initrd not found: $INITRD"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$OUT_ROOTFS" ]]; then
|
||||
base_dir="$(dirname "$BASE_ROOTFS")"
|
||||
base_name="$(basename "$BASE_ROOTFS")"
|
||||
OUT_ROOTFS="${base_dir}/rw-${base_name}"
|
||||
fi
|
||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
||||
log "output rootfs already exists: $OUT_ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "copying base rootfs to $OUT_ROOTFS"
|
||||
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
|
||||
|
||||
if [[ -n "$SIZE_SPEC" ]]; then
|
||||
SIZE_BYTES="$(parse_size "$SIZE_SPEC")"
|
||||
BASE_BYTES="$(stat -c%s "$BASE_ROOTFS")"
|
||||
if [[ -z "$SIZE_BYTES" || "$SIZE_BYTES" -lt "$BASE_BYTES" ]]; then
|
||||
log "size must be >= base image size"
|
||||
exit 1
|
||||
fi
|
||||
log "resizing rootfs to $SIZE_SPEC"
|
||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
||||
e2fsck -p -f "$OUT_ROOTFS" >/dev/null
|
||||
resize2fs "$OUT_ROOTFS" >/dev/null
|
||||
fi
|
||||
|
||||
VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)"
|
||||
VM_TAG="${VM_ID:0:8}"
|
||||
VM_NAME="interactive-${VM_TAG}"
|
||||
VM_DIR="$VM_ROOT/$VM_ID"
|
||||
mkdir -p "$VM_DIR"
|
||||
|
||||
API_SOCK="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/banger/fc-$VM_TAG.sock"
|
||||
LOG_FILE="$VM_DIR/firecracker.log"
|
||||
TAP_DEV="tap-fc-$VM_TAG"
|
||||
|
||||
# Allocate guest IP
|
||||
NEXT_IP_FILE="$STATE/next_ip"
|
||||
NEXT_IP="$(cat "$NEXT_IP_FILE" 2>/dev/null || echo 2)"
|
||||
GUEST_IP="172.16.0.$NEXT_IP"
|
||||
echo "$((NEXT_IP + 1))" > "$NEXT_IP_FILE"
|
||||
|
||||
sudo -v
|
||||
|
||||
cleanup() {
|
||||
sudo kill "${FC_PID:-}" 2>/dev/null || true
|
||||
if [[ "$NAT_ACTIVE" -eq 1 ]]; then
|
||||
banger_nat down >/dev/null 2>&1 || true
|
||||
fi
|
||||
sudo ip link del "$TAP_DEV" 2>/dev/null || true
|
||||
rm -f "$API_SOCK"
|
||||
rm -rf "$VM_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
sudo mkdir -p "$(dirname "$API_SOCK")"
|
||||
sudo chown "$(id -u):$(id -g)" "$(dirname "$API_SOCK")"
|
||||
|
||||
# Host bridge
|
||||
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
|
||||
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
|
||||
sudo ip link add name "$BR_DEV" type bridge
|
||||
sudo ip addr add "${BR_IP}/${CIDR}" dev "$BR_DEV"
|
||||
sudo ip link set "$BR_DEV" up
|
||||
else
|
||||
sudo ip link set "$BR_DEV" up
|
||||
fi
|
||||
|
||||
log "creating tap device $TAP_DEV"
|
||||
TAP_USER="${SUDO_UID:-$(id -u)}"
|
||||
TAP_GROUP="${SUDO_GID:-$(id -g)}"
|
||||
sudo ip tuntap add dev "$TAP_DEV" mode tap user "$TAP_USER" group "$TAP_GROUP"
|
||||
sudo ip link set "$TAP_DEV" master "$BR_DEV"
|
||||
sudo ip link set "$TAP_DEV" up
|
||||
sudo ip link set "$BR_DEV" up
|
||||
|
||||
log "starting firecracker process"
|
||||
rm -f "$API_SOCK"
|
||||
nohup sudo -E "$FC_BIN" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 &
|
||||
FC_PID="$!"
|
||||
|
||||
log "waiting for firecracker api socket"
|
||||
for _ in $(seq 1 200); do
|
||||
[[ -S "$API_SOCK" ]] && break
|
||||
sleep 0.02
|
||||
done
|
||||
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
|
||||
|
||||
log "configuring machine"
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"vcpu_count": 2,
|
||||
"mem_size_mib": 1024,
|
||||
"smt": false
|
||||
}' >/dev/null
|
||||
|
||||
KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0:${VM_NAME}:eth0:off:${DNS_SERVER} hostname=${VM_NAME} systemd.mask=home.mount systemd.mask=var.mount"
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"kernel_image_path\": \"$KERNEL\",
|
||||
\"boot_args\": \"$KCMD\",
|
||||
\"initrd_path\": \"$INITRD\"
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"drive_id\": \"rootfs\",
|
||||
\"path_on_host\": \"$OUT_ROOTFS\",
|
||||
\"is_root_device\": true,
|
||||
\"is_read_only\": false
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/network-interfaces/eth0 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"iface_id\": \"eth0\",
|
||||
\"host_dev_name\": \"$TAP_DEV\"
|
||||
}" >/dev/null
|
||||
|
||||
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "action_type": "InstanceStart" }' >/dev/null
|
||||
|
||||
SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)"
|
||||
if [[ -n "$SUDO_CHILD_PID" ]]; then
|
||||
FC_PID="$SUDO_CHILD_PID"
|
||||
fi
|
||||
|
||||
VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
|
||||
CREATED_AT="$(date -Iseconds)"
|
||||
jq -n \
|
||||
--arg id "$VM_ID" \
|
||||
--arg name "$VM_NAME" \
|
||||
--arg pid "$FC_PID" \
|
||||
--arg created_at "$CREATED_AT" \
|
||||
--arg guest_ip "$GUEST_IP" \
|
||||
--arg tap "$TAP_DEV" \
|
||||
--arg api_sock "$API_SOCK" \
|
||||
--arg log "$LOG_FILE" \
|
||||
--arg rootfs "$OUT_ROOTFS" \
|
||||
--arg kernel "$KERNEL" \
|
||||
--argjson config "$VM_CONFIG_JSON" \
|
||||
'{meta:{id:$id,name:$name,pid:$pid,created_at:$created_at,guest_ip:$guest_ip,tap:$tap,api_sock:$api_sock,log:$log,rootfs:$rootfs,kernel:$kernel},config:$config}' \
|
||||
> "$VM_DIR/vm.json"
|
||||
|
||||
log "enabling NAT for interactive session"
|
||||
banger_nat up >/dev/null
|
||||
NAT_ACTIVE=1
|
||||
|
||||
log "waiting for SSH"
|
||||
log "guest ip: $GUEST_IP"
|
||||
log "ssh: ssh -i \"$SSH_KEY\" root@${GUEST_IP}"
|
||||
for _ in $(seq 1 60); do
|
||||
if ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
|
||||
"root@${GUEST_IP}" "true" >/dev/null 2>&1; then
|
||||
log "ssh ready"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
log "output rootfs: $OUT_ROOTFS"
|
||||
log "press Ctrl+C to stop and clean up"
|
||||
|
||||
while kill -0 "$FC_PID" >/dev/null 2>&1; do
|
||||
sleep 1
|
||||
done
|
||||
116
scripts/lib/packages.sh
Normal file
116
scripts/lib/packages.sh
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
readonly BANGER_PACKAGES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly BANGER_REPO_ROOT="$(cd "$BANGER_PACKAGES_DIR/../.." && pwd)"
|
||||
BANGER_APT_PACKAGES_FILE="${BANGER_APT_PACKAGES_FILE:-$BANGER_REPO_ROOT/config/packages.apt}"
|
||||
|
||||
banger_packages_file() {
|
||||
printf '%s' "$BANGER_APT_PACKAGES_FILE"
|
||||
}
|
||||
|
||||
banger_packages_normalized_lines() {
|
||||
local packages_file="${1:-$BANGER_APT_PACKAGES_FILE}"
|
||||
|
||||
[[ -f "$packages_file" ]] || return 1
|
||||
awk '
|
||||
{
|
||||
sub(/\r$/, "")
|
||||
sub(/[[:space:]]*#.*$/, "")
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
||||
if ($0 != "") print
|
||||
}
|
||||
' "$packages_file"
|
||||
}
|
||||
|
||||
banger_packages_read_array() {
|
||||
local -n out="$1"
|
||||
local packages_file="${2:-$BANGER_APT_PACKAGES_FILE}"
|
||||
|
||||
mapfile -t out < <(banger_packages_normalized_lines "$packages_file")
|
||||
(( ${#out[@]} > 0 ))
|
||||
}
|
||||
|
||||
banger_packages_hash_stream() {
|
||||
command -v sha256sum >/dev/null 2>&1 || return 1
|
||||
sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
banger_packages_manifest_hash() {
|
||||
local packages_file="${1:-$BANGER_APT_PACKAGES_FILE}"
|
||||
|
||||
[[ -f "$packages_file" ]] || return 1
|
||||
banger_packages_normalized_lines "$packages_file" | banger_packages_hash_stream
|
||||
}
|
||||
|
||||
banger_rootfs_manifest_metadata_path() {
|
||||
local rootfs_path="$1"
|
||||
printf '%s.packages.sha256' "$rootfs_path"
|
||||
}
|
||||
|
||||
banger_rootfs_manifest_recorded_hash() {
|
||||
local rootfs_path="$1"
|
||||
local metadata_file recorded_hash
|
||||
|
||||
metadata_file="$(banger_rootfs_manifest_metadata_path "$rootfs_path")"
|
||||
[[ -f "$metadata_file" ]] || return 1
|
||||
|
||||
recorded_hash="$(head -n 1 "$metadata_file" | tr -d '[:space:]')"
|
||||
[[ -n "$recorded_hash" ]] || return 1
|
||||
printf '%s' "$recorded_hash"
|
||||
}
|
||||
|
||||
banger_write_rootfs_manifest_metadata() {
|
||||
local rootfs_path="$1"
|
||||
local manifest_hash="$2"
|
||||
local metadata_file
|
||||
|
||||
metadata_file="$(banger_rootfs_manifest_metadata_path "$rootfs_path")"
|
||||
printf '%s\n' "$manifest_hash" > "$metadata_file"
|
||||
}
|
||||
|
||||
banger_rootfs_manifest_status() {
|
||||
local rootfs_path="$1"
|
||||
local current_hash recorded_hash
|
||||
|
||||
if [[ ! -f "$rootfs_path" ]]; then
|
||||
printf '%s' "missing-rootfs"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! current_hash="$(banger_packages_manifest_hash)"; then
|
||||
printf '%s' "unknown"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! recorded_hash="$(banger_rootfs_manifest_recorded_hash "$rootfs_path")"; then
|
||||
printf '%s' "missing-metadata"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ "$recorded_hash" == "$current_hash" ]]; then
|
||||
printf '%s' "fresh"
|
||||
else
|
||||
printf '%s' "stale"
|
||||
fi
|
||||
}
|
||||
|
||||
banger_rootfs_manifest_warning() {
|
||||
local rootfs_path="$1"
|
||||
local status
|
||||
|
||||
status="$(banger_rootfs_manifest_status "$rootfs_path")"
|
||||
case "$status" in
|
||||
stale)
|
||||
printf '%s was built with an older package manifest; rebuild it explicitly to pick up package changes' "$rootfs_path"
|
||||
;;
|
||||
missing-metadata)
|
||||
printf '%s has no package manifest metadata; rebuild it explicitly to pick up package changes' "$rootfs_path"
|
||||
;;
|
||||
unknown)
|
||||
printf 'unable to compare %s against %s; install sha256sum and verify the package manifest manually' "$rootfs_path" "$BANGER_APT_PACKAGES_FILE"
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
624
scripts/make-rootfs-void.sh
Executable file
624
scripts/make-rootfs-void.sh
Executable file
|
|
@ -0,0 +1,624 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[make-rootfs-void] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/make-rootfs-void.sh [--out <path>] [--size <size>] [--mirror <url>] [--arch <arch>] [--packages <path>]
|
||||
|
||||
Build an experimental Void Linux rootfs image plus a matching /root work-seed.
|
||||
|
||||
Defaults:
|
||||
--out ./build/runtime/rootfs-void.ext4
|
||||
--size 2G
|
||||
--mirror https://repo-default.voidlinux.org
|
||||
--arch x86_64
|
||||
--packages ./config/packages.void
|
||||
|
||||
This path is experimental and local-only. If ./build/runtime/void-kernel exists
|
||||
it uses the staged Void kernel modules from that directory; otherwise it falls
|
||||
back to the current runtime bundle modules. It does not change the default
|
||||
Debian image flow.
|
||||
EOF
|
||||
}
|
||||
|
||||
parse_size() {
|
||||
local raw="$1"
|
||||
if [[ "$raw" =~ ^([0-9]+)([KMG])?$ ]]; then
|
||||
local num="${BASH_REMATCH[1]}"
|
||||
local unit="${BASH_REMATCH[2]}"
|
||||
case "$unit" in
|
||||
K) printf '%s\n' $((num * 1024)) ;;
|
||||
M|"") printf '%s\n' $((num * 1024 * 1024)) ;;
|
||||
G) printf '%s\n' $((num * 1024 * 1024 * 1024)) ;;
|
||||
esac
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
require_command() {
|
||||
local name="$1"
|
||||
command -v "$name" >/dev/null 2>&1 || {
|
||||
log "required command not found: $name"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; build it first with 'make build' or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
normalize_mirror() {
|
||||
local mirror="${1%/}"
|
||||
mirror="${mirror%/current}"
|
||||
mirror="${mirror%/static}"
|
||||
printf '%s\n' "$mirror"
|
||||
}
|
||||
|
||||
bundle_path() {
|
||||
local key="$1"
|
||||
local fallback="$2"
|
||||
local rel=""
|
||||
|
||||
if [[ -f "$BUNDLE_METADATA" ]] && command -v jq >/dev/null 2>&1; then
|
||||
rel="$(jq -r --arg key "$key" '.[$key] // empty' "$BUNDLE_METADATA" 2>/dev/null || true)"
|
||||
fi
|
||||
if [[ -n "$rel" && "$rel" != "null" ]]; then
|
||||
printf '%s\n' "$RUNTIME_DIR/$rel"
|
||||
return
|
||||
fi
|
||||
printf '%s\n' "$fallback"
|
||||
}
|
||||
|
||||
find_latest_module_dir() {
|
||||
local root="$1"
|
||||
if [[ ! -d "$root" ]]; then
|
||||
return 1
|
||||
fi
|
||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
||||
}
|
||||
|
||||
find_static_binary() {
|
||||
local name="$1"
|
||||
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
|
||||
}
|
||||
|
||||
find_static_keys_dir() {
|
||||
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 1
|
||||
}
|
||||
|
||||
install_root_authorized_key() {
|
||||
local public_key
|
||||
public_key="$(ssh-keygen -y -f "$SSH_KEY")"
|
||||
sudo mkdir -p "$ROOT_MOUNT/root/.ssh"
|
||||
printf '%s\n' "$public_key" | sudo tee "$ROOT_MOUNT/root/.ssh/authorized_keys" >/dev/null
|
||||
sudo chmod 700 "$ROOT_MOUNT/root/.ssh"
|
||||
sudo chmod 600 "$ROOT_MOUNT/root/.ssh/authorized_keys"
|
||||
}
|
||||
|
||||
ensure_sshd_include() {
|
||||
local cfg="$ROOT_MOUNT/etc/ssh/sshd_config"
|
||||
local tmp_cfg="$TMP_DIR/sshd_config"
|
||||
local include_line="Include /etc/ssh/sshd_config.d/*.conf"
|
||||
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/ssh/sshd_config.d"
|
||||
if sudo test -f "$cfg"; then
|
||||
sudo cat "$cfg" > "$tmp_cfg"
|
||||
else
|
||||
: > "$tmp_cfg"
|
||||
fi
|
||||
|
||||
if ! grep -Eq '^[[:space:]]*Include[[:space:]]+/etc/ssh/sshd_config\.d/\*\.conf([[:space:]]|$)' "$tmp_cfg"; then
|
||||
{
|
||||
printf '%s\n' "$include_line"
|
||||
cat "$tmp_cfg"
|
||||
} > "${tmp_cfg}.new"
|
||||
mv "${tmp_cfg}.new" "$tmp_cfg"
|
||||
sudo install -m 0644 "$tmp_cfg" "$cfg"
|
||||
fi
|
||||
}
|
||||
|
||||
install_vsock_service() {
|
||||
local service_dir="$ROOT_MOUNT/etc/sv/banger-vsock-agent"
|
||||
local run_path="$service_dir/run"
|
||||
local finish_path="$service_dir/finish"
|
||||
|
||||
sudo mkdir -p "$service_dir"
|
||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
||||
#!/bin/sh
|
||||
modprobe vsock 2>/dev/null || true
|
||||
modprobe vmw_vsock_virtio_transport 2>/dev/null || true
|
||||
exec /usr/local/bin/banger-vsock-agent
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
EOF
|
||||
sudo chmod 0755 "$run_path" "$finish_path"
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
||||
sudo ln -snf /etc/sv/banger-vsock-agent "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-vsock-agent"
|
||||
}
|
||||
|
||||
install_opencode_service() {
|
||||
local service_dir="$ROOT_MOUNT/etc/sv/banger-opencode"
|
||||
local run_path="$service_dir/run"
|
||||
local finish_path="$service_dir/finish"
|
||||
|
||||
sudo mkdir -p "$service_dir"
|
||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
||||
#!/bin/sh
|
||||
set -e
|
||||
export HOME=/root
|
||||
cd /root
|
||||
exec /usr/local/bin/opencode serve --hostname 0.0.0.0 --port 4096
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$finish_path" >/dev/null
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
EOF
|
||||
sudo chmod 0755 "$run_path" "$finish_path"
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
||||
sudo ln -snf /etc/sv/banger-opencode "$ROOT_MOUNT/etc/runit/runsvdir/default/banger-opencode"
|
||||
}
|
||||
|
||||
install_guest_network_bootstrap() {
|
||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/libexec" "$ROOT_MOUNT/etc/runit/core-services"
|
||||
sudo install -m 0755 "$GUESTNET_BOOTSTRAP_SCRIPT" "$ROOT_MOUNT/usr/local/libexec/banger-network-bootstrap"
|
||||
sudo install -m 0644 "$GUESTNET_VOID_CORE_SERVICE" "$ROOT_MOUNT/etc/runit/core-services/20-banger-network.sh"
|
||||
}
|
||||
|
||||
configure_docker_bootstrap() {
|
||||
local modules_conf="$ROOT_MOUNT/etc/modules-load.d/docker-netfilter.conf"
|
||||
local sysctl_conf="$ROOT_MOUNT/etc/sysctl.d/99-docker.conf"
|
||||
local service_dir="$ROOT_MOUNT/etc/sv/docker"
|
||||
local run_path="$service_dir/run"
|
||||
local orig_run_path="$service_dir/run.orig"
|
||||
local preflight_path="$ROOT_MOUNT/usr/local/bin/banger-docker-preflight"
|
||||
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/modules-load.d" "$ROOT_MOUNT/etc/sysctl.d" "$ROOT_MOUNT/usr/local/bin"
|
||||
cat <<'EOF' | sudo tee "$modules_conf" >/dev/null
|
||||
nf_tables
|
||||
nft_chain_nat
|
||||
veth
|
||||
br_netfilter
|
||||
overlay
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$sysctl_conf" >/dev/null
|
||||
net.bridge.bridge-nf-call-iptables = 1
|
||||
net.bridge.bridge-nf-call-ip6tables = 1
|
||||
net.ipv4.ip_forward = 1
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$preflight_path" >/dev/null
|
||||
#!/bin/sh
|
||||
for module in nf_tables nft_chain_nat veth br_netfilter overlay; do
|
||||
modprobe "$module" 2>/dev/null || true
|
||||
done
|
||||
if command -v sysctl >/dev/null 2>&1; then
|
||||
sysctl --load /etc/sysctl.d/99-docker.conf >/dev/null 2>&1 || true
|
||||
fi
|
||||
EOF
|
||||
|
||||
if [[ ! -f "$run_path" ]]; then
|
||||
log "Void rootfs is missing /etc/sv/docker/run after docker install"
|
||||
exit 1
|
||||
fi
|
||||
sudo install -m 0755 "$run_path" "$orig_run_path"
|
||||
cat <<'EOF' | sudo tee "$run_path" >/dev/null
|
||||
#!/bin/sh
|
||||
set -e
|
||||
/usr/local/bin/banger-docker-preflight
|
||||
exec /etc/sv/docker/run.orig
|
||||
EOF
|
||||
sudo chmod 0644 "$modules_conf" "$sysctl_conf"
|
||||
sudo chmod 0755 "$preflight_path" "$run_path" "$orig_run_path"
|
||||
}
|
||||
|
||||
enable_sshd_service() {
|
||||
if [[ ! -d "$ROOT_MOUNT/etc/sv/sshd" ]]; then
|
||||
log "Void rootfs is missing /etc/sv/sshd after openssh install"
|
||||
exit 1
|
||||
fi
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
||||
sudo ln -snf /etc/sv/sshd "$ROOT_MOUNT/etc/runit/runsvdir/default/sshd"
|
||||
}
|
||||
|
||||
enable_docker_service() {
|
||||
if [[ ! -d "$ROOT_MOUNT/etc/sv/docker" ]]; then
|
||||
log "Void rootfs is missing /etc/sv/docker after docker install"
|
||||
exit 1
|
||||
fi
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/runit/runsvdir/default"
|
||||
sudo ln -snf /etc/sv/docker "$ROOT_MOUNT/etc/runit/runsvdir/default/docker"
|
||||
}
|
||||
|
||||
normalize_root_shell() {
|
||||
local passwd="$ROOT_MOUNT/etc/passwd"
|
||||
local shells="$ROOT_MOUNT/etc/shells"
|
||||
local wanted_shell="/bin/bash"
|
||||
local tmp_passwd="$TMP_DIR/passwd"
|
||||
local root_shell=""
|
||||
|
||||
if [[ ! -x "$ROOT_MOUNT$wanted_shell" ]]; then
|
||||
log "required root shell is missing from the Void image: $wanted_shell"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$shells" ]]; then
|
||||
log "Void image is missing /etc/shells"
|
||||
exit 1
|
||||
fi
|
||||
if ! sudo grep -Fxq "$wanted_shell" "$shells"; then
|
||||
log "Void image does not allow $wanted_shell in /etc/shells"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo cat "$passwd" > "$tmp_passwd"
|
||||
awk -F: -v OFS=: -v shell="$wanted_shell" '
|
||||
$1 == "root" {
|
||||
$7 = shell
|
||||
found = 1
|
||||
}
|
||||
{ print }
|
||||
END {
|
||||
if (!found) {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
' "$tmp_passwd" > "${tmp_passwd}.new" || {
|
||||
log "failed to rewrite root shell in /etc/passwd"
|
||||
exit 1
|
||||
}
|
||||
mv "${tmp_passwd}.new" "$tmp_passwd"
|
||||
sudo install -m 0644 "$tmp_passwd" "$passwd"
|
||||
|
||||
root_shell="$(sudo awk -F: '$1 == "root" { print $7 }' "$passwd")"
|
||||
if [[ "$root_shell" != "$wanted_shell" ]]; then
|
||||
log "root shell normalization failed: expected $wanted_shell, got ${root_shell:-<empty>}"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
configure_root_bash_prompt() {
|
||||
local bashrc="$ROOT_MOUNT/root/.bashrc"
|
||||
local bash_profile="$ROOT_MOUNT/root/.bash_profile"
|
||||
local profile_prompt="$ROOT_MOUNT/etc/profile.d/banger-bash-prompt.sh"
|
||||
|
||||
sudo mkdir -p "$ROOT_MOUNT/root" "$ROOT_MOUNT/etc/profile.d"
|
||||
cat <<'EOF' | sudo tee "$bashrc" >/dev/null
|
||||
# banger: default interactive prompt for experimental Void guests
|
||||
case "$-" in
|
||||
*i*) ;;
|
||||
*) return ;;
|
||||
esac
|
||||
|
||||
if [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
||||
export BANGER_MISE_ACTIVATED=1
|
||||
eval "$(/usr/local/bin/mise activate bash)"
|
||||
fi
|
||||
|
||||
PS1='\u@\h:\w\$ '
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$bash_profile" >/dev/null
|
||||
if [ -f ~/.bashrc ]; then
|
||||
. ~/.bashrc
|
||||
fi
|
||||
EOF
|
||||
cat <<'EOF' | sudo tee "$profile_prompt" >/dev/null
|
||||
case "$-" in
|
||||
*i*) ;;
|
||||
*) return 0 2>/dev/null || exit 0 ;;
|
||||
esac
|
||||
|
||||
if [ -n "${BASH_VERSION:-}" ]; then
|
||||
PS1='\u@\h:\w\$ '
|
||||
fi
|
||||
EOF
|
||||
sudo chmod 0644 "$bashrc" "$bash_profile" "$profile_prompt"
|
||||
}
|
||||
|
||||
install_mise_and_opencode() {
|
||||
local profile_mise="$ROOT_MOUNT/etc/profile.d/mise.sh"
|
||||
|
||||
sudo mkdir -p "$ROOT_MOUNT/etc/profile.d"
|
||||
if [[ -r /etc/resolv.conf ]]; then
|
||||
sudo install -m 0644 /etc/resolv.conf "$ROOT_MOUNT/etc/resolv.conf"
|
||||
fi
|
||||
|
||||
sudo env \
|
||||
HOME=/root \
|
||||
PATH=/usr/local/bin:/usr/bin:/bin \
|
||||
chroot "$ROOT_MOUNT" /bin/bash -se <<EOF
|
||||
set -euo pipefail
|
||||
curl -fsSL https://mise.run | MISE_INSTALL_PATH="$MISE_INSTALL_PATH" MISE_VERSION="$MISE_VERSION" sh
|
||||
"$MISE_INSTALL_PATH" use -g "$OPENCODE_TOOL"
|
||||
"$MISE_INSTALL_PATH" reshim
|
||||
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
|
||||
echo "opencode shim not found after mise install" >&2
|
||||
exit 1
|
||||
fi
|
||||
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
||||
EOF
|
||||
|
||||
cat <<'EOF' | sudo tee "$profile_mise" >/dev/null
|
||||
if [ -n "${BASH_VERSION:-}" ] && [ -z "${BANGER_MISE_ACTIVATED:-}" ] && [ -x '/usr/local/bin/mise' ]; then
|
||||
export BANGER_MISE_ACTIVATED=1
|
||||
eval "$(/usr/local/bin/mise activate bash)"
|
||||
fi
|
||||
EOF
|
||||
sudo chmod 0644 "$profile_mise"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${ROOT_MOUNT:-}" ]] && command -v mountpoint >/dev/null 2>&1 && mountpoint -q "$ROOT_MOUNT"; then
|
||||
sudo umount "$ROOT_MOUNT" || true
|
||||
fi
|
||||
if [[ "${BUILD_DONE:-0}" != "1" ]]; then
|
||||
rm -f "${OUT_ROOTFS:-}" "${WORK_SEED:-}" "${OUT_ROOTFS:-}.packages.sha256"
|
||||
fi
|
||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
PACKAGES_FILE="$REPO_ROOT/config/packages.void"
|
||||
export BANGER_APT_PACKAGES_FILE="$PACKAGES_FILE"
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
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
|
||||
|
||||
BUNDLE_METADATA="$RUNTIME_DIR/bundle.json"
|
||||
SSH_KEY="$(bundle_path ssh_key_path "$RUNTIME_DIR/id_ed25519")"
|
||||
OUT_ROOTFS="$RUNTIME_DIR/rootfs-void.ext4"
|
||||
SIZE_SPEC="2G"
|
||||
MIRROR="https://repo-default.voidlinux.org"
|
||||
ARCH="x86_64"
|
||||
MISE_VERSION="v2025.12.0"
|
||||
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
||||
OPENCODE_TOOL="github:anomalyco/opencode"
|
||||
GUESTNET_BOOTSTRAP_SCRIPT="$REPO_ROOT/internal/guestnet/assets/bootstrap.sh"
|
||||
GUESTNET_VOID_CORE_SERVICE="$REPO_ROOT/internal/guestnet/assets/void-core-service.sh"
|
||||
MODULES_DIR="$(bundle_path default_modules_dir "$RUNTIME_DIR/wtf/root/lib/modules/6.8.0-94-generic")"
|
||||
VOID_KERNEL_MODULES_DIR="$(find_latest_module_dir "$RUNTIME_DIR/void-kernel/lib/modules" || true)"
|
||||
VSOCK_AGENT="$(bundle_path vsock_agent_path "$RUNTIME_DIR/banger-vsock-agent")"
|
||||
if [[ "$VSOCK_AGENT" == "$RUNTIME_DIR/banger-vsock-agent" && ! -x "$VSOCK_AGENT" ]]; then
|
||||
VSOCK_AGENT="$(bundle_path vsock_ping_helper_path "$RUNTIME_DIR/banger-vsock-pingd")"
|
||||
fi
|
||||
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
|
||||
MODULES_DIR="$VOID_KERNEL_MODULES_DIR"
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out)
|
||||
OUT_ROOTFS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--size)
|
||||
SIZE_SPEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--mirror)
|
||||
MIRROR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--arch)
|
||||
ARCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--packages)
|
||||
PACKAGES_FILE="${2:-}"
|
||||
export BANGER_APT_PACKAGES_FILE="$PACKAGES_FILE"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log "unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
MIRROR="$(normalize_mirror "$MIRROR")"
|
||||
REPO_URL="$MIRROR/current"
|
||||
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
|
||||
|
||||
if [[ "$ARCH" != "x86_64" ]]; then
|
||||
log "unsupported arch: $ARCH"
|
||||
log "this experimental builder currently supports only x86_64-glibc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PACKAGES_FILE" ]]; then
|
||||
log "package manifest not found: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -d "$MODULES_DIR" ]]; then
|
||||
log "modules dir not found: $MODULES_DIR"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
||||
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
||||
log "run 'make build' or refresh the runtime bundle"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$GUESTNET_BOOTSTRAP_SCRIPT" ]]; then
|
||||
log "guest network bootstrap script not found: $GUESTNET_BOOTSTRAP_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$GUESTNET_VOID_CORE_SERVICE" ]]; then
|
||||
log "guest network core-service shim not found: $GUESTNET_VOID_CORE_SERVICE"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -e "$OUT_ROOTFS" ]]; then
|
||||
log "output rootfs already exists: $OUT_ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_command curl
|
||||
require_command tar
|
||||
require_command sudo
|
||||
require_command mkfs.ext4
|
||||
require_command ssh-keygen
|
||||
require_command mount
|
||||
require_command umount
|
||||
require_command install
|
||||
require_command find
|
||||
require_command awk
|
||||
require_command sed
|
||||
require_command sha256sum
|
||||
require_command truncate
|
||||
require_command mountpoint
|
||||
|
||||
VOID_PACKAGES=()
|
||||
if ! banger_packages_read_array VOID_PACKAGES "$PACKAGES_FILE"; then
|
||||
log "package manifest is empty: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if ! PACKAGES_HASH="$(banger_packages_manifest_hash "$PACKAGES_FILE")"; then
|
||||
log "failed to hash package manifest: $PACKAGES_FILE"
|
||||
exit 1
|
||||
fi
|
||||
if ! SIZE_BYTES="$(parse_size "$SIZE_SPEC")"; then
|
||||
log "invalid size: $SIZE_SPEC"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
if [[ "$OUT_ROOTFS" == *.ext4 ]]; then
|
||||
WORK_SEED="${OUT_ROOTFS%.ext4}.work-seed.ext4"
|
||||
else
|
||||
WORK_SEED="${OUT_ROOTFS}.work-seed"
|
||||
fi
|
||||
|
||||
TMP_DIR="$(mktemp -d -t banger-void-rootfs-XXXXXX)"
|
||||
STATIC_DIR="$TMP_DIR/static"
|
||||
ROOT_MOUNT="$TMP_DIR/rootfs"
|
||||
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
|
||||
BUILD_DONE=0
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$STATIC_DIR" "$ROOT_MOUNT"
|
||||
|
||||
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
|
||||
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
|
||||
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
|
||||
|
||||
XBPS_INSTALL="$(find_static_binary xbps-install)"
|
||||
XBPS_QUERY="$(find_static_binary xbps-query)"
|
||||
STATIC_KEYS_DIR="$(find_static_keys_dir)"
|
||||
|
||||
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
|
||||
log "failed to locate xbps-install in the static archive"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
|
||||
log "failed to locate Void repository keys in the static archive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "creating $OUT_ROOTFS ($SIZE_SPEC)"
|
||||
truncate -s "$SIZE_BYTES" "$OUT_ROOTFS"
|
||||
mkfs.ext4 -F -m 0 -L banger-void-root "$OUT_ROOTFS" >/dev/null
|
||||
sudo mount -o loop "$OUT_ROOTFS" "$ROOT_MOUNT"
|
||||
sudo mkdir -p "$ROOT_MOUNT/var/db/xbps/keys"
|
||||
sudo cp -a "$STATIC_KEYS_DIR/." "$ROOT_MOUNT/var/db/xbps/keys/"
|
||||
|
||||
log "installing Void packages into the rootfs"
|
||||
sudo env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -r "$ROOT_MOUNT" -R "$REPO_URL" "${VOID_PACKAGES[@]}"
|
||||
|
||||
if [[ -n "$XBPS_QUERY" && -x "$XBPS_QUERY" ]]; then
|
||||
log "installed package set:"
|
||||
sudo env XBPS_ARCH="$ARCH" "$XBPS_QUERY" -r "$ROOT_MOUNT" -l | awk '/^ii/ {print " " $2}' || true
|
||||
fi
|
||||
|
||||
if [[ -n "$VOID_KERNEL_MODULES_DIR" ]]; then
|
||||
log "copying staged Void kernel modules into the guest"
|
||||
else
|
||||
log "copying bundled kernel modules into the guest"
|
||||
fi
|
||||
sudo mkdir -p "$ROOT_MOUNT/lib/modules"
|
||||
sudo cp -a "$MODULES_DIR" "$ROOT_MOUNT/lib/modules/"
|
||||
|
||||
log "installing the guest-side vsock agent"
|
||||
sudo mkdir -p "$ROOT_MOUNT/usr/local/bin"
|
||||
sudo install -m 0755 "$VSOCK_AGENT" "$ROOT_MOUNT/usr/local/bin/banger-vsock-agent"
|
||||
|
||||
log "preparing SSH and runit services"
|
||||
install_guest_network_bootstrap
|
||||
ensure_sshd_include
|
||||
enable_sshd_service
|
||||
install_vsock_service
|
||||
configure_docker_bootstrap
|
||||
enable_docker_service
|
||||
normalize_root_shell
|
||||
configure_root_bash_prompt
|
||||
log "installing mise and opencode"
|
||||
install_mise_and_opencode
|
||||
install_opencode_service
|
||||
install_root_authorized_key
|
||||
sudo touch "$ROOT_MOUNT/etc/fstab" "$ROOT_MOUNT/etc/hostname"
|
||||
sudo chroot "$ROOT_MOUNT" /usr/bin/ssh-keygen -A
|
||||
|
||||
log "removing bulky caches, docs, and stale installer artifacts from the experimental image"
|
||||
sudo rm -rf \
|
||||
"$ROOT_MOUNT/var/cache/xbps" \
|
||||
"$ROOT_MOUNT/usr/share/doc" \
|
||||
"$ROOT_MOUNT/usr/share/info" \
|
||||
"$ROOT_MOUNT/usr/share/man"
|
||||
sudo rm -f \
|
||||
"$ROOT_MOUNT/root/get-docker" \
|
||||
"$ROOT_MOUNT/root/get-docker.sh" \
|
||||
"$ROOT_MOUNT/root/.cache/opencode" \
|
||||
"$ROOT_MOUNT/tmp/get-docker" \
|
||||
"$ROOT_MOUNT/tmp/get-docker.sh"
|
||||
sudo rm -rf \
|
||||
"$ROOT_MOUNT/root/.cache/mise" \
|
||||
"$ROOT_MOUNT/root/.local/share/mise/downloads" \
|
||||
"$ROOT_MOUNT/root/.local/share/mise/tmp"
|
||||
|
||||
sudo umount "$ROOT_MOUNT"
|
||||
|
||||
banger_write_rootfs_manifest_metadata "$OUT_ROOTFS" "$PACKAGES_HASH"
|
||||
|
||||
log "building work-seed $WORK_SEED"
|
||||
"$BANGER_BIN" internal work-seed --rootfs "$OUT_ROOTFS" --out "$WORK_SEED"
|
||||
|
||||
BUILD_DONE=1
|
||||
log "built experimental Void rootfs: $OUT_ROOTFS"
|
||||
log "built experimental Void work-seed: $WORK_SEED"
|
||||
log "use examples/void-exp.config.toml as the local config override template"
|
||||
88
scripts/make-rootfs.sh
Executable file
88
scripts/make-rootfs.sh
Executable file
|
|
@ -0,0 +1,88 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[make-rootfs] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/make-rootfs.sh [--size <size>] [--base-rootfs <path>]
|
||||
|
||||
Builds build/runtime/rootfs-docker.ext4 using scripts/customize.sh. If
|
||||
--base-rootfs is omitted, the first existing file is used:
|
||||
./build/runtime/rootfs.ext4
|
||||
./runtime/rootfs.ext4 (legacy fallback)
|
||||
./ubuntu-noble-rootfs/rootfs.ext4
|
||||
./ubuntu-lts/rootfs.ext4
|
||||
EOF
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
source "$SCRIPT_DIR/lib/packages.sh"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
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
|
||||
OUT_ROOTFS="$RUNTIME_DIR/rootfs-docker.ext4"
|
||||
SIZE_SPEC="6G"
|
||||
BASE_ROOTFS=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--size)
|
||||
SIZE_SPEC="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--base-rootfs)
|
||||
BASE_ROOTFS="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log "unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -f "$OUT_ROOTFS" ]]; then
|
||||
OUT_ROOTFS_WARNING="$(banger_rootfs_manifest_warning "$OUT_ROOTFS" || true)"
|
||||
if [[ -n "$OUT_ROOTFS_WARNING" ]]; then
|
||||
log "warning: $OUT_ROOTFS_WARNING"
|
||||
fi
|
||||
log "already exists: $OUT_ROOTFS"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ -z "$BASE_ROOTFS" ]]; then
|
||||
if [[ -f "$RUNTIME_DIR/rootfs.ext4" ]]; then
|
||||
BASE_ROOTFS="$RUNTIME_DIR/rootfs.ext4"
|
||||
elif [[ -f "$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4" ]]; then
|
||||
BASE_ROOTFS="$REPO_ROOT/ubuntu-noble-rootfs/rootfs.ext4"
|
||||
elif [[ -f "$REPO_ROOT/ubuntu-lts/rootfs.ext4" ]]; then
|
||||
BASE_ROOTFS="$REPO_ROOT/ubuntu-lts/rootfs.ext4"
|
||||
else
|
||||
log "no base rootfs found; run 'make runtime-bundle' or pass --base-rootfs"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
|
||||
log "building $OUT_ROOTFS from $BASE_ROOTFS"
|
||||
exec env BANGER_RUNTIME_DIR="$RUNTIME_DIR" "$SCRIPT_DIR/customize.sh" "$BASE_ROOTFS" \
|
||||
--out "$OUT_ROOTFS" \
|
||||
--size "$SIZE_SPEC" \
|
||||
--docker
|
||||
390
scripts/make-void-kernel.sh
Executable file
390
scripts/make-void-kernel.sh
Executable file
|
|
@ -0,0 +1,390 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[make-void-kernel] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: ./scripts/make-void-kernel.sh [--out-dir <path>] [--mirror <url>] [--arch <arch>] [--kernel-package <name>] [--print-register-flags]
|
||||
|
||||
Download and stage a Void Linux kernel under ./build/runtime/void-kernel for
|
||||
the
|
||||
experimental Void guest flow.
|
||||
|
||||
Defaults:
|
||||
--out-dir ./build/runtime/void-kernel
|
||||
--mirror https://repo-default.voidlinux.org
|
||||
--arch x86_64
|
||||
--kernel-package linux6.12
|
||||
|
||||
The staged output contains:
|
||||
boot/vmlinux-<version> Firecracker-usable kernel extracted from vmlinuz
|
||||
boot/vmlinuz-<version> Raw distro boot image from the Void package
|
||||
boot/initramfs-<version>.img Matching initramfs generated with dracut
|
||||
boot/config-<version> Void kernel config
|
||||
lib/modules/<version>/ Matching kernel modules tree
|
||||
|
||||
If --print-register-flags is passed, the script does not download anything. It
|
||||
prints the banger image register flags for an existing staged Void kernel.
|
||||
EOF
|
||||
}
|
||||
|
||||
require_command() {
|
||||
local name="$1"
|
||||
command -v "$name" >/dev/null 2>&1 || {
|
||||
log "required command not found: $name"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
normalize_mirror() {
|
||||
local mirror="${1%/}"
|
||||
mirror="${mirror%/current}"
|
||||
mirror="${mirror%/static}"
|
||||
printf '%s\n' "$mirror"
|
||||
}
|
||||
|
||||
find_static_binary() {
|
||||
local name="$1"
|
||||
find "$STATIC_DIR" -type f \( -name "$name" -o -name "$name.static" \) -perm -u+x | sort | head -n 1
|
||||
}
|
||||
|
||||
find_static_keys_dir() {
|
||||
find "$STATIC_DIR" -type d -path '*/var/db/xbps/keys' | sort | head -n 1
|
||||
}
|
||||
|
||||
find_latest_matching() {
|
||||
local dir="$1"
|
||||
local pattern="$2"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
return 1
|
||||
fi
|
||||
find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1
|
||||
}
|
||||
|
||||
find_latest_module_dir() {
|
||||
local root="$1"
|
||||
if [[ ! -d "$root" ]]; then
|
||||
return 1
|
||||
fi
|
||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
||||
}
|
||||
|
||||
print_register_flags() {
|
||||
local kernel=""
|
||||
local initrd=""
|
||||
local modules=""
|
||||
|
||||
kernel="$(find_latest_matching "$OUT_DIR/boot" 'vmlinux-*' || true)"
|
||||
initrd="$(find_latest_matching "$OUT_DIR/boot" 'initramfs-*' || true)"
|
||||
modules="$(find_latest_module_dir "$OUT_DIR/lib/modules" || true)"
|
||||
|
||||
if [[ -z "$kernel" || -z "$modules" ]]; then
|
||||
log "staged Void kernel not found under $OUT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf -- '--kernel %q ' "$kernel"
|
||||
if [[ -n "$initrd" ]]; then
|
||||
printf -- '--initrd %q ' "$initrd"
|
||||
fi
|
||||
printf -- '--modules %q\n' "$modules"
|
||||
}
|
||||
|
||||
check_elf() {
|
||||
local path="$1"
|
||||
readelf -h "$path" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
ensure_stage_root_layout() {
|
||||
mkdir -p "$STAGE_ROOT/usr"
|
||||
|
||||
if [[ ! -e "$STAGE_ROOT/bin" ]]; then
|
||||
ln -snf usr/bin "$STAGE_ROOT/bin"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/sbin" ]]; then
|
||||
ln -snf usr/bin "$STAGE_ROOT/sbin"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/usr/sbin" ]]; then
|
||||
ln -snf bin "$STAGE_ROOT/usr/sbin"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/lib" ]]; then
|
||||
ln -snf usr/lib "$STAGE_ROOT/lib"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/lib64" ]]; then
|
||||
ln -snf usr/lib "$STAGE_ROOT/lib64"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/usr/lib64" ]]; then
|
||||
ln -snf lib "$STAGE_ROOT/usr/lib64"
|
||||
fi
|
||||
if [[ -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
|
||||
mkdir -p "$STAGE_ROOT/usr/lib/udev" "$STAGE_ROOT/usr/lib/systemd"
|
||||
if [[ ! -e "$STAGE_ROOT/usr/lib/udev/udevd" ]]; then
|
||||
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/udev/udevd"
|
||||
fi
|
||||
if [[ ! -e "$STAGE_ROOT/usr/lib/systemd/systemd-udevd" ]]; then
|
||||
ln -snf ../../bin/udevd "$STAGE_ROOT/usr/lib/systemd/systemd-udevd"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
sync_host_dracut_tree() {
|
||||
if [[ ! -d /usr/lib/dracut ]]; then
|
||||
log "host dracut support files not found under /usr/lib/dracut"
|
||||
exit 1
|
||||
fi
|
||||
rm -rf "$STAGE_ROOT/usr/lib/dracut"
|
||||
mkdir -p "$STAGE_ROOT/usr/lib"
|
||||
cp -a /usr/lib/dracut "$STAGE_ROOT/usr/lib/dracut"
|
||||
}
|
||||
|
||||
build_initramfs() {
|
||||
local kver="$1"
|
||||
local modules_dir="$2"
|
||||
local out="$3"
|
||||
local config_dir="$TMP_DIR/dracut.conf.d"
|
||||
local tmpdir="$TMP_DIR/dracut-tmp"
|
||||
local force_drivers="virtio virtio_ring virtio_mmio virtio_blk virtio_net virtio_console ext4 vsock vmw_vsock_virtio_transport"
|
||||
|
||||
mkdir -p "$config_dir" "$tmpdir"
|
||||
ensure_stage_root_layout
|
||||
sync_host_dracut_tree
|
||||
|
||||
log "generating initramfs for kernel $kver with host dracut against the staged Void sysroot"
|
||||
env dracutbasedir="/usr/lib/dracut" dracut \
|
||||
--force \
|
||||
--kver "$kver" \
|
||||
--sysroot "$STAGE_ROOT" \
|
||||
--kmoddir "$modules_dir" \
|
||||
--conf /dev/null \
|
||||
--confdir "$config_dir" \
|
||||
--tmpdir "$tmpdir" \
|
||||
--no-hostonly \
|
||||
--filesystems "ext4" \
|
||||
--force-drivers "$force_drivers" \
|
||||
--gzip \
|
||||
"$out"
|
||||
}
|
||||
|
||||
extract_vmlinux() {
|
||||
local image="$1"
|
||||
local out="$2"
|
||||
local tmp="$TMP_DIR/vmlinux.extract"
|
||||
|
||||
if check_elf "$image"; then
|
||||
install -m 0644 "$image" "$out"
|
||||
return 0
|
||||
fi
|
||||
|
||||
try_decompress() {
|
||||
local header="$1"
|
||||
local marker="$2"
|
||||
local command="$3"
|
||||
local pos=""
|
||||
|
||||
while IFS= read -r pos; do
|
||||
[[ -n "$pos" ]] || continue
|
||||
pos="${pos%%:*}"
|
||||
tail -c+"$pos" "$image" | eval "$command" >"$tmp" 2>/dev/null || true
|
||||
if check_elf "$tmp"; then
|
||||
install -m 0644 "$tmp" "$out"
|
||||
return 0
|
||||
fi
|
||||
done < <(tr "$header\n$marker" "\n$marker=" < "$image" | grep -abo "^$marker" || true)
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
try_decompress '\037\213\010' "xy" "gunzip" && return 0
|
||||
try_decompress '\3757zXZ\000' "abcde" "unxz" && return 0
|
||||
try_decompress "BZh" "xy" "bunzip2" && return 0
|
||||
try_decompress '\135\000\000\000' "xxx" "unlzma" && return 0
|
||||
try_decompress '\002!L\030' "xxx" "lz4 -d" && return 0
|
||||
try_decompress '(\265/\375' "xxx" "unzstd" && return 0
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
resolve_kernel_package_file() {
|
||||
local escaped_name=""
|
||||
escaped_name="$(printf '%s\n' "$KERNEL_PACKAGE" | sed 's/[.[\*^$()+?{|]/\\&/g')"
|
||||
|
||||
curl -fsSL "$REPO_URL/" |
|
||||
grep -o "${escaped_name}-[0-9][^\" >]*\\.${ARCH}\\.xbps" |
|
||||
sort -u |
|
||||
tail -n 1
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${TMP_DIR:-}" && -d "${TMP_DIR:-}" ]]; then
|
||||
rm -rf "$TMP_DIR"
|
||||
fi
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
OUT_DIR="$RUNTIME_DIR/void-kernel"
|
||||
MIRROR="https://repo-default.voidlinux.org"
|
||||
ARCH="x86_64"
|
||||
KERNEL_PACKAGE="linux6.12"
|
||||
PRINT_REGISTER_FLAGS=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out-dir)
|
||||
OUT_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--mirror)
|
||||
MIRROR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--arch)
|
||||
ARCH="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--kernel-package)
|
||||
KERNEL_PACKAGE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--print-register-flags)
|
||||
PRINT_REGISTER_FLAGS=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log "unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
MIRROR="$(normalize_mirror "$MIRROR")"
|
||||
REPO_URL="$MIRROR/current"
|
||||
STATIC_ARCHIVE_URL="$MIRROR/static/xbps-static-latest.x86_64-musl.tar.xz"
|
||||
|
||||
if [[ "$PRINT_REGISTER_FLAGS" == "1" ]]; then
|
||||
print_register_flags
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$ARCH" != "x86_64" ]]; then
|
||||
log "unsupported arch: $ARCH"
|
||||
log "this experimental downloader currently supports only x86_64"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
if [[ -e "$OUT_DIR" ]]; then
|
||||
log "output directory already exists: $OUT_DIR"
|
||||
log "remove it first if you want to re-stage a different Void kernel"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
require_command curl
|
||||
require_command tar
|
||||
require_command cp
|
||||
require_command find
|
||||
require_command grep
|
||||
require_command cut
|
||||
require_command readelf
|
||||
require_command file
|
||||
require_command install
|
||||
require_command tail
|
||||
require_command xz
|
||||
require_command gzip
|
||||
require_command bzip2
|
||||
require_command dracut
|
||||
|
||||
TMP_DIR="$(mktemp -d -t banger-void-kernel-XXXXXX)"
|
||||
STATIC_DIR="$TMP_DIR/static"
|
||||
STAGE_ROOT="$TMP_DIR/root"
|
||||
STAGE_OUT="$TMP_DIR/out"
|
||||
STATIC_ARCHIVE="$TMP_DIR/xbps-static.tar.xz"
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$STATIC_DIR" "$STAGE_ROOT/var/db/xbps/keys" "$STAGE_OUT/boot" "$STAGE_OUT/lib/modules"
|
||||
|
||||
log "downloading static XBPS from $STATIC_ARCHIVE_URL"
|
||||
curl -fsSL "$STATIC_ARCHIVE_URL" -o "$STATIC_ARCHIVE"
|
||||
tar -xf "$STATIC_ARCHIVE" -C "$STATIC_DIR"
|
||||
|
||||
XBPS_INSTALL="$(find_static_binary xbps-install)"
|
||||
STATIC_KEYS_DIR="$(find_static_keys_dir)"
|
||||
if [[ -z "$XBPS_INSTALL" || ! -x "$XBPS_INSTALL" ]]; then
|
||||
log "failed to locate xbps-install in the static archive"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$STATIC_KEYS_DIR" || ! -d "$STATIC_KEYS_DIR" ]]; then
|
||||
log "failed to locate Void repository keys in the static archive"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -a "$STATIC_KEYS_DIR/." "$STAGE_ROOT/var/db/xbps/keys/"
|
||||
|
||||
KERNEL_PACKAGE_FILE="$(resolve_kernel_package_file)"
|
||||
if [[ -z "$KERNEL_PACKAGE_FILE" ]]; then
|
||||
log "failed to resolve a package file for $KERNEL_PACKAGE in $REPO_URL"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "staging $KERNEL_PACKAGE_FILE into a temporary root"
|
||||
env XBPS_ARCH="$ARCH" "$XBPS_INSTALL" -S -y -U -r "$STAGE_ROOT" -R "$REPO_URL" linux-base "$KERNEL_PACKAGE" dracut eudev >/dev/null
|
||||
|
||||
VMLINUX_RAW="$(find_latest_matching "$STAGE_ROOT/boot" 'vmlinuz-*' || true)"
|
||||
KERNEL_CONFIG="$(find_latest_matching "$STAGE_ROOT/boot" 'config-*' || true)"
|
||||
MODULES_DIR="$(find_latest_module_dir "$STAGE_ROOT/usr/lib/modules" || true)"
|
||||
KERNEL_VERSION="$(basename "$MODULES_DIR")"
|
||||
INITRAMFS_NAME="initramfs-${KERNEL_VERSION}.img"
|
||||
INITRAMFS_RAW="$STAGE_OUT/boot/$INITRAMFS_NAME"
|
||||
|
||||
if [[ -z "$VMLINUX_RAW" || -z "$KERNEL_CONFIG" || -z "$MODULES_DIR" ]]; then
|
||||
log "staged Void kernel is missing expected boot artifacts"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -x "$STAGE_ROOT/usr/bin/udevd" ]]; then
|
||||
log "staged Void sysroot is missing /usr/bin/udevd after package install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VMLINUX_BASE="$(basename "$VMLINUX_RAW")"
|
||||
VMLINUX_OUT="$STAGE_OUT/boot/vmlinux-${VMLINUX_BASE#vmlinuz-}"
|
||||
install -m 0644 "$VMLINUX_RAW" "$STAGE_OUT/boot/$VMLINUX_BASE"
|
||||
install -m 0644 "$KERNEL_CONFIG" "$STAGE_OUT/boot/$(basename "$KERNEL_CONFIG")"
|
||||
build_initramfs "$KERNEL_VERSION" "$MODULES_DIR" "$INITRAMFS_RAW"
|
||||
cp -a "$MODULES_DIR" "$STAGE_OUT/lib/modules/"
|
||||
|
||||
log "extracting Firecracker kernel from $(basename "$VMLINUX_RAW")"
|
||||
if ! extract_vmlinux "$VMLINUX_RAW" "$VMLINUX_OUT"; then
|
||||
log "failed to extract an uncompressed vmlinux from $VMLINUX_RAW"
|
||||
log "raw kernel image type: $(file -b "$VMLINUX_RAW")"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cat >"$STAGE_OUT/metadata.json" <<EOF
|
||||
{
|
||||
"package": "$KERNEL_PACKAGE_FILE",
|
||||
"kernel_path": "$OUT_DIR/boot/$(basename "$VMLINUX_OUT")",
|
||||
"raw_kernel_path": "$OUT_DIR/boot/$VMLINUX_BASE",
|
||||
"config_path": "$OUT_DIR/boot/$(basename "$KERNEL_CONFIG")",
|
||||
"initrd_path": "$OUT_DIR/boot/$INITRAMFS_NAME",
|
||||
"modules_dir": "$OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"
|
||||
}
|
||||
EOF
|
||||
|
||||
mv "$STAGE_OUT" "$OUT_DIR"
|
||||
|
||||
log "staged Void kernel artifacts in $OUT_DIR"
|
||||
log "kernel image: $OUT_DIR/boot/$(basename "$VMLINUX_OUT")"
|
||||
log "initrd image: $OUT_DIR/boot/$INITRAMFS_NAME"
|
||||
log "modules dir: $OUT_DIR/lib/modules/$(basename "$MODULES_DIR")"
|
||||
95
scripts/register-void-image.sh
Executable file
95
scripts/register-void-image.sh
Executable file
|
|
@ -0,0 +1,95 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[register-void-image] %s\n' "$*" >&2
|
||||
}
|
||||
|
||||
find_latest_matching() {
|
||||
local dir="$1"
|
||||
local pattern="$2"
|
||||
if [[ ! -d "$dir" ]]; then
|
||||
return 1
|
||||
fi
|
||||
find "$dir" -maxdepth 1 -type f -name "$pattern" | sort | tail -n 1
|
||||
}
|
||||
|
||||
find_latest_module_dir() {
|
||||
local root="$1"
|
||||
if [[ ! -d "$root" ]]; then
|
||||
return 1
|
||||
fi
|
||||
find "$root" -mindepth 1 -maxdepth 1 -type d | sort | tail -n 1
|
||||
}
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; build it first with 'make build' or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/runtime"
|
||||
fi
|
||||
|
||||
RUNTIME_DIR="${BANGER_RUNTIME_DIR:-$DEFAULT_RUNTIME_DIR}"
|
||||
IMAGE_NAME="${VOID_IMAGE_NAME:-void-exp}"
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
ROOTFS="$RUNTIME_DIR/rootfs-void.ext4"
|
||||
WORK_SEED="$RUNTIME_DIR/rootfs-void.work-seed.ext4"
|
||||
PACKAGES="$REPO_ROOT/config/packages.void"
|
||||
|
||||
if [[ ! -f "$ROOTFS" ]]; then
|
||||
log "missing Void rootfs: $ROOTFS"
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$WORK_SEED" ]]; then
|
||||
log "missing Void work-seed: $WORK_SEED"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
args=(
|
||||
image register
|
||||
--name "$IMAGE_NAME"
|
||||
--rootfs "$ROOTFS"
|
||||
--work-seed "$WORK_SEED"
|
||||
--packages "$PACKAGES"
|
||||
)
|
||||
|
||||
if [[ ! -d "$RUNTIME_DIR/void-kernel" ]]; then
|
||||
log "missing staged Void kernel artifacts: $RUNTIME_DIR/void-kernel"
|
||||
log "run 'make void-kernel' before registering $IMAGE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
kernel="$(find_latest_matching "$RUNTIME_DIR/void-kernel/boot" 'vmlinux-*' || true)"
|
||||
initrd="$(find_latest_matching "$RUNTIME_DIR/void-kernel/boot" 'initramfs-*' || true)"
|
||||
modules="$(find_latest_module_dir "$RUNTIME_DIR/void-kernel/lib/modules" || true)"
|
||||
|
||||
if [[ -z "$kernel" || -z "$initrd" || -z "$modules" ]]; then
|
||||
log "staged Void kernel is incomplete; expected vmlinux, initramfs, and modules under $RUNTIME_DIR/void-kernel"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "using staged Void kernel artifacts from $RUNTIME_DIR/void-kernel"
|
||||
args+=(--kernel "$kernel" --initrd "$initrd" --modules "$modules")
|
||||
|
||||
"$BANGER_BIN" "${args[@]}"
|
||||
344
scripts/verify.sh
Executable file
344
scripts/verify.sh
Executable file
|
|
@ -0,0 +1,344 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
log() {
|
||||
printf '[verify] %s\n' "$*"
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/build/runtime"
|
||||
if [[ ! -d "$DEFAULT_RUNTIME_DIR" && -d "$REPO_ROOT/runtime" ]]; then
|
||||
DEFAULT_RUNTIME_DIR="$REPO_ROOT/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
|
||||
)
|
||||
OPENCODE_PORT=4096
|
||||
|
||||
resolve_banger_bin() {
|
||||
if [[ -n "${BANGER_BIN:-}" ]]; then
|
||||
printf '%s\n' "$BANGER_BIN"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/build/bin/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/build/bin/banger"
|
||||
return
|
||||
fi
|
||||
if [[ -x "$REPO_ROOT/banger" ]]; then
|
||||
printf '%s\n' "$REPO_ROOT/banger"
|
||||
return
|
||||
fi
|
||||
if command -v banger >/dev/null 2>&1; then
|
||||
command -v banger
|
||||
return
|
||||
fi
|
||||
log "banger binary not found; run 'make build' or set BANGER_BIN"
|
||||
exit 1
|
||||
}
|
||||
|
||||
BANGER_BIN="$(resolve_banger_bin)"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
wait_for_tcp() {
|
||||
local host="$1"
|
||||
local port="$2"
|
||||
local deadline="$3"
|
||||
|
||||
while ((SECONDS < deadline)); do
|
||||
if (exec 3<>/dev/tcp/"$host"/"$port") >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
refresh_vm_metadata() {
|
||||
if ! VM_JSON="$("$BANGER_BIN" 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_BIN" 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_BIN" 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: ./scripts/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_BIN" vm delete "$VM_NAME"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if [[ -n "${VM_NAME:-}" ]]; then
|
||||
"$BANGER_BIN" vm delete "$VM_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
log "starting VM"
|
||||
CREATE_ARGS=("$BANGER_BIN" 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
|
||||
|
||||
log "asserting opencode is available and listening in the guest"
|
||||
ssh "${SSH_COMMON_ARGS[@]}" "root@${GUEST_IP}" "command -v opencode >/dev/null 2>&1 && ss -H -lntp | awk '\$4 ~ /:${OPENCODE_PORT}\$/ { found = 1 } END { exit found ? 0 : 1 }'" >/dev/null
|
||||
|
||||
log "asserting opencode server is reachable from the host"
|
||||
if ! wait_for_tcp "$GUEST_IP" "$OPENCODE_PORT" "$BOOT_DEADLINE"; then
|
||||
log "opencode server did not become reachable at ${GUEST_IP}:${OPENCODE_PORT}"
|
||||
dump_diagnostics
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "asserting opencode port is reported by banger vm ports"
|
||||
if ! "$BANGER_BIN" vm ports "$VM_NAME" | grep -F ":${OPENCODE_PORT}" >/dev/null 2>&1; then
|
||||
log "banger vm ports did not report ${OPENCODE_PORT}"
|
||||
dump_diagnostics
|
||||
exit 1
|
||||
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_BIN" 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue