Provisioning was still installing `claude` and `pi` through a separate npm-global prefix even after the guest images had switched to `mise` for Node and opencode. That left two competing install paths and made the runtime layout harder to reason about. Switch the Debian and Void image setup flows to install `claude` and `pi` as `mise` npm tools, assert their shims exist after `mise reshim`, and symlink `node`, `npm`, `opencode`, `claude`, and `pi` directly from the mise shim directory into `/usr/local/bin`. Update the imagebuild test expectations and bump the Void rootfs default size to 4G so the larger default toolset still fits reliably.
597 lines
17 KiB
Bash
Executable file
597 lines
17 KiB
Bash
Executable file
#!/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)"
|
|
STATE="${BANGER_STATE_DIR:-${XDG_STATE_HOME:-$HOME/.local/state}/banger/image-build}"
|
|
VM_ROOT="$STATE/vms"
|
|
mkdir -p "$VM_ROOT"
|
|
|
|
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
|
|
FC_BIN="$("$BANGER_BIN" internal firecracker-path)"
|
|
SSH_KEY="$("$BANGER_BIN" internal ssh-key-path)"
|
|
VSOCK_AGENT="$("$BANGER_BIN" internal vsock-agent-path)"
|
|
|
|
banger_nat() {
|
|
local action="$1"
|
|
"$BANGER_BIN" internal nat "$action" --guest-ip "$GUEST_IP" --tap "$TAP_DEV"
|
|
}
|
|
|
|
load_package_preset() {
|
|
local preset="$1"
|
|
local -n out="$2"
|
|
mapfile -t out < <("$BANGER_BIN" internal packages "$preset")
|
|
(( ${#out[@]} > 0 ))
|
|
}
|
|
|
|
write_rootfs_manifest_metadata() {
|
|
local rootfs_path="$1"
|
|
local manifest_hash="$2"
|
|
printf '%s\n' "$manifest_hash" > "${rootfs_path}.packages.sha256"
|
|
}
|
|
|
|
BASE_ROOTFS=""
|
|
OUT_ROOTFS=""
|
|
SIZE_SPEC=""
|
|
INSTALL_DOCKER=0
|
|
KERNEL=""
|
|
INITRD=""
|
|
MISE_VERSION="v2025.12.0"
|
|
MISE_INSTALL_PATH="/usr/local/bin/mise"
|
|
MISE_ACTIVATE_LINE='eval "$(/usr/local/bin/mise activate bash)"'
|
|
NODE_TOOL="node@22"
|
|
CLAUDE_CODE_TOOL="npm:@anthropic-ai/claude-code"
|
|
PI_TOOL="npm:@mariozechner/pi-coding-agent"
|
|
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=""
|
|
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 [[ -z "$KERNEL" ]]; then
|
|
log "kernel path is required; pass --kernel"
|
|
exit 1
|
|
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 preset metadata"
|
|
exit 1
|
|
fi
|
|
if [[ ! -x "$VSOCK_AGENT" ]]; then
|
|
log "vsock agent not found or not executable: $VSOCK_AGENT"
|
|
log "run 'make build'"
|
|
exit 1
|
|
fi
|
|
|
|
APT_PACKAGES=()
|
|
if ! load_package_preset debian APT_PACKAGES; then
|
|
log "debian package preset is empty"
|
|
exit 1
|
|
fi
|
|
if ! PACKAGES_HASH="$(printf '%s\n' "${APT_PACKAGES[@]}" | sha256sum | awk '{print $1}')"; then
|
|
log "failed to hash package preset"
|
|
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 rootfstype=ext4 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 "$NODE_TOOL"
|
|
"$MISE_INSTALL_PATH" use -g github:anomalyco/opencode
|
|
"$MISE_INSTALL_PATH" use -g "$CLAUDE_CODE_TOOL"
|
|
"$MISE_INSTALL_PATH" use -g "$PI_TOOL"
|
|
"$MISE_INSTALL_PATH" reshim
|
|
if [[ ! -e /root/.local/share/mise/shims/node ]]; then
|
|
echo 'node shim not found after mise install' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -e /root/.local/share/mise/shims/npm ]]; then
|
|
echo 'npm shim not found after mise install' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -e /root/.local/share/mise/shims/opencode ]]; then
|
|
echo 'opencode shim not found after mise install' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -e /root/.local/share/mise/shims/claude ]]; then
|
|
echo 'claude shim not found after mise install' >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -e /root/.local/share/mise/shims/pi ]]; then
|
|
echo 'pi shim not found after mise install' >&2
|
|
exit 1
|
|
fi
|
|
ln -snf /root/.local/share/mise/shims/node /usr/local/bin/node
|
|
ln -snf /root/.local/share/mise/shims/npm /usr/local/bin/npm
|
|
ln -snf /root/.local/share/mise/shims/opencode /usr/local/bin/opencode
|
|
ln -snf /root/.local/share/mise/shims/claude /usr/local/bin/claude
|
|
ln -snf /root/.local/share/mise/shims/pi /usr/local/bin/pi
|
|
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
|
|
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"
|