From 67cfdd659f1b4ec7a33f9139c012d9becc87d37d Mon Sep 17 00:00:00 2001 From: Thales Maciel Date: Mon, 16 Mar 2026 16:21:31 -0300 Subject: [PATCH] Remove legacy shell VM entrypoints The repository had already moved VM lifecycle management onto the Go daemon, CLI, and TUI, but the old shell entrypoints were still sitting in the tree as a second surface for the same operations. Keeping both made the repo harder to understand and invited drift between the supported workflow and the leftover scripts. Remove the redundant shell commands for run, stop, kill, rm, list, logs, restore, ssh, and ps. This commit only drops the obsolete entrypoints; the Go replacements and supporting docs were handled in earlier work. --- kill.sh | 125 -------------- list.sh | 120 -------------- logs.sh | 85 ---------- ps.sh | 23 --- restore.sh | 259 ----------------------------- rm.sh | 113 ------------- run.sh | 478 ----------------------------------------------------- ssh.sh | 112 ------------- stop.sh | 108 ------------ 9 files changed, 1423 deletions(-) delete mode 100755 kill.sh delete mode 100755 list.sh delete mode 100755 logs.sh delete mode 100755 ps.sh delete mode 100755 restore.sh delete mode 100755 rm.sh delete mode 100755 run.sh delete mode 100755 ssh.sh delete mode 100755 stop.sh diff --git a/kill.sh b/kill.sh deleted file mode 100755 index 90ebe55..0000000 --- a/kill.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[kill] %s\n' "$*" -} - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/dns.sh" -STATE="$DIR/state" -VM_ROOT="$STATE/vms" - -usage() { - cat <<'EOF' -Usage: ./kill.sh ... [--signal SIGTERM|SIGKILL|...] - -Sends a signal to the Firecracker process. -EOF -} - -find_vm_json() { - local query="$1" - local vm_json match_count=0 match="" - - for vm_json in "$VM_ROOT"/*/vm.json; do - [[ -f "$vm_json" ]] || continue - local id name - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then - match="$vm_json" - match_count=$((match_count + 1)) - fi - done - - if (( match_count == 0 )); then - log "no VM found for prefix: $query" - exit 1 - fi - if (( match_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - printf '%s' "$match" -} - -SIGNAL="TERM" -QUERIES=() -while [[ $# -gt 0 ]]; do - case "$1" in - --signal) - SIGNAL="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - QUERIES+=("$1") - shift - ;; - esac -done - -if (( ${#QUERIES[@]} == 0 )); then - usage - exit 1 -fi - -VM_JSONS=() -for query in "${QUERIES[@]}"; do - VM_JSON="$(find_vm_json "$query")" - already_added=0 - for existing_vm_json in "${VM_JSONS[@]}"; do - if [[ "$existing_vm_json" == "$VM_JSON" ]]; then - already_added=1 - break - fi - done - if (( already_added == 0 )); then - VM_JSONS+=("$VM_JSON") - fi -done - -for vm_json in "${VM_JSONS[@]}"; do - vm_id="$(jq -r '.meta.id // empty' "$vm_json")" - vm_name="$(jq -r '.meta.name // empty' "$vm_json")" - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - tap="$(jq -r '.meta.tap // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - base_loop="$(jq -r '.meta.base_loop // empty' "$vm_json")" - cow_loop="$(jq -r '.meta.cow_loop // empty' "$vm_json")" - dm_dev="$(jq -r '.meta.dm_dev // empty' "$vm_json")" - dm_name="$(jq -r '.meta.dm_name // empty' "$vm_json")" - dns_name="$(jq -r '.meta.dns_name // empty' "$vm_json")" - if [[ -z "$dns_name" ]]; then - dns_name="$(banger_dns_name "$vm_name")" - fi - if [[ -z "$pid" ]]; then - log "pid not found in $vm_json" - exit 1 - fi - if [[ -z "$api_sock" ]]; then - log "api_sock not found in $vm_json" - exit 1 - fi - - if ! ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock"; then - log "pid $pid does not match a running VM for ${vm_name:-$vm_json}" - exit 1 - fi - - log "sending SIG$SIGNAL to ${vm_name:-$vm_json} (pid $pid)" - sudo kill "-$SIGNAL" "$pid" - if ! banger_wait_for_vm_exit "$pid" "$api_sock" 30; then - log "timed out waiting for ${vm_name:-$vm_json} to exit" - exit 1 - fi - banger_teardown_vm_runtime "$tap" "$api_sock" "$dm_name" "$dm_dev" "$cow_loop" "$base_loop" - banger_mark_vm_stopped "$vm_json" - banger_dns_remove_record_name "$dns_name" - log "signal sent to ${vm_name:-$vm_json}" -done diff --git a/list.sh b/list.sh deleted file mode 100755 index 93bab50..0000000 --- a/list.sh +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -shopt -s nullglob - -format_count_unit() { - local count="$1" - local singular="$2" - local plural="$3" - - if (( count == 1 )); then - printf '%s %s ago' "$count" "$singular" - else - printf '%s %s ago' "$count" "$plural" - fi -} - -humanize_created_at() { - local created_at="$1" - local created_epoch elapsed - - if [[ -z "$created_at" ]]; then - printf '%s' "-" - return 0 - fi - - created_epoch="$(date -d "$created_at" +%s 2>/dev/null || true)" - if [[ -z "$created_epoch" ]]; then - printf '%s' "$created_at" - return 0 - fi - - elapsed=$((NOW_EPOCH - created_epoch)) - if (( elapsed < 0 )); then - elapsed=0 - fi - - if (( elapsed < 45 )); then - printf '%s' "a few moments ago" - elif (( elapsed < 90 )); then - printf '%s' "about a minute ago" - elif (( elapsed < 3600 )); then - format_count_unit "$((elapsed / 60))" "minute" "minutes" - elif (( elapsed < 5400 )); then - printf '%s' "about an hour ago" - elif (( elapsed < 86400 )); then - format_count_unit "$((elapsed / 3600))" "hour" "hours" - elif (( elapsed < 172800 )); then - printf '%s' "1 day ago" - elif (( elapsed < 604800 )); then - format_count_unit "$((elapsed / 86400))" "day" "days" - elif (( elapsed < 1209600 )); then - printf '%s' "1 week ago" - elif (( elapsed < 2592000 )); then - format_count_unit "$((elapsed / 604800))" "week" "weeks" - elif (( elapsed < 5184000 )); then - printf '%s' "1 month ago" - elif (( elapsed < 31536000 )); then - format_count_unit "$((elapsed / 2592000))" "month" "months" - elif (( elapsed < 63072000 )); then - printf '%s' "1 year ago" - else - format_count_unit "$((elapsed / 31536000))" "year" "years" - fi -} - -statuses=() -ids=() -names=() -ips=() -created=() -max_name=4 -max_ip=2 -max_created=7 -GREEN='\033[32m' -YELLOW='\033[33m' -RESET='\033[0m' -NOW_EPOCH="$(date +%s)" - -for vm_json in state/vms/*/vm.json; do - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - created_at="$(jq -r '.meta.created_at // empty' "$vm_json")" - created_display="$(humanize_created_at "$created_at")" - guest_ip="$(jq -r '.meta.guest_ip // empty' "$vm_json")" - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - - status="stopped" - if [[ -n "$pid" && -n "$api_sock" ]]; then - if ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock"; then - status="running" - fi - fi - - short_id="${id:0:12}" - statuses+=("$status") - ids+=("$short_id") - names+=("$name") - ips+=("$guest_ip") - created+=("$created_display") - if (( ${#name} > max_name )); then - max_name=${#name} - fi - if (( ${#guest_ip} > max_ip )); then - max_ip=${#guest_ip} - fi - if (( ${#created_display} > max_created )); then - max_created=${#created_display} - fi -done - -for i in "${!ids[@]}"; do - color="$YELLOW" - if [[ "${statuses[$i]}" == "running" ]]; then - color="$GREEN" - fi - printf '%-10b %-12s %-*s %-*s %-*s\n' \ - "${color}[${statuses[$i]}]${RESET}" "${ids[$i]}" "$max_name" "${names[$i]}" "$max_ip" "${ips[$i]}" "$max_created" "${created[$i]}" -done diff --git a/logs.sh b/logs.sh deleted file mode 100755 index e685e50..0000000 --- a/logs.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[logs] %s\n' "$*" -} - -usage() { - cat <<'EOF' -Usage: ./logs.sh [--follow] - -Prints the Firecracker log for a VM. Use --follow to tail -f. -EOF -} - -find_vm_json() { - local query="$1" - local vm_json match_count=0 match="" - - for vm_json in state/vms/*/vm.json; do - [[ -f "$vm_json" ]] || continue - local id name - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then - match="$vm_json" - match_count=$((match_count + 1)) - fi - done - - if (( match_count == 0 )); then - log "no VM found for prefix: $query" - exit 1 - fi - if (( match_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - printf '%s' "$match" -} - -QUERY="${1:-}" -FOLLOW=0 -while [[ $# -gt 0 ]]; do - case "$1" in - --follow|-f) - FOLLOW=1 - shift - ;; - -h|--help) - usage - exit 0 - ;; - *) - if [[ -z "$QUERY" ]]; then - QUERY="$1" - shift - else - log "unknown option: $1" - usage - exit 1 - fi - ;; - esac -done - -if [[ -z "$QUERY" ]]; then - usage - exit 1 -fi - -VM_JSON="$(find_vm_json "$QUERY")" -LOG_FILE="$(jq -r '.meta.log // empty' "$VM_JSON")" - -if [[ -z "$LOG_FILE" || ! -f "$LOG_FILE" ]]; then - log "log file not found: $LOG_FILE" - exit 1 -fi - -if (( FOLLOW == 1 )); then - tail -f "$LOG_FILE" -else - cat "$LOG_FILE" -fi diff --git a/ps.sh b/ps.sh deleted file mode 100755 index b486872..0000000 --- a/ps.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -shopt -s nullglob - -for vm_json in state/vms/*/vm.json; do - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - created_at="$(jq -r '.meta.created_at // empty' "$vm_json")" - guest_ip="$(jq -r '.meta.guest_ip // empty' "$vm_json")" - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - tap="$(jq -r '.meta.tap // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - - if [[ -z "$pid" || -z "$api_sock" ]]; then - continue - fi - - if ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock"; then - printf 'id=%s name=%s created_at=%s guest_ip=%s pid=%s tap=%s\n' \ - "$id" "$name" "$created_at" "$guest_ip" "$pid" "$tap" - fi -done diff --git a/restore.sh b/restore.sh deleted file mode 100755 index dfddbd0..0000000 --- a/restore.sh +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[restore] %s\n' "$*" -} - -usage() { - cat <<'EOF' -Usage: ./restore.sh - -Restarts a VM using existing disks and COW snapshot. -EOF -} - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/dns.sh" - -find_vm_json() { - local query="$1" - local vm_json match_count=0 match="" - - for vm_json in "$DIR"/state/vms/*/vm.json; do - [[ -f "$vm_json" ]] || continue - local id name - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then - match="$vm_json" - match_count=$((match_count + 1)) - fi - done - - if (( match_count == 0 )); then - log "no VM found for prefix: $query" - exit 1 - fi - if (( match_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - printf '%s' "$match" -} - -QUERY="${1:-}" -if [[ "$QUERY" == "-h" || "$QUERY" == "--help" ]]; then - usage - exit 0 -fi -if [[ -z "$QUERY" ]]; then - usage - exit 1 -fi - -FC_BIN="$DIR/firecracker" - -if ! command -v jq >/dev/null 2>&1; then - log "jq is required" - exit 1 -fi -if ! command -v dmsetup >/dev/null 2>&1 || ! command -v losetup >/dev/null 2>&1 || ! command -v blockdev >/dev/null 2>&1; then - log "dmsetup, losetup, and blockdev are required" - exit 1 -fi -if ! command -v e2cp >/dev/null 2>&1 || ! command -v e2rm >/dev/null 2>&1; then - log "e2cp and e2rm are required" - exit 1 -fi - -VM_JSON="$(find_vm_json "$QUERY")" -VM_DIR="$(dirname "$VM_JSON")" - -VM_ID="$(jq -r '.meta.id // empty' "$VM_JSON")" -VM_NAME="$(jq -r '.meta.name // empty' "$VM_JSON")" -PID="$(jq -r '.meta.pid // empty' "$VM_JSON")" -ROOTFS="$(jq -r '.meta.rootfs // empty' "$VM_JSON")" -KERNEL="$(jq -r '.meta.kernel // empty' "$VM_JSON")" -TAP_DEV="$(jq -r '.meta.tap // empty' "$VM_JSON")" -API_SOCK="$(jq -r '.meta.api_sock // empty' "$VM_JSON")" -LOG_FILE="$(jq -r '.meta.log // empty' "$VM_JSON")" -GUEST_IP="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")" -DM_NAME="$(jq -r '.meta.dm_name // empty' "$VM_JSON")" -DM_DEV_OLD="$(jq -r '.meta.dm_dev // empty' "$VM_JSON")" -BASE_LOOP_OLD="$(jq -r '.meta.base_loop // empty' "$VM_JSON")" -COW_FILE="$(jq -r '.meta.cow_file // empty' "$VM_JSON")" -COW_LOOP_OLD="$(jq -r '.meta.cow_loop // empty' "$VM_JSON")" -INITRD_PATH="$(jq -r '.config["boot-source"].initrd_path // empty' "$VM_JSON")" -DNS_NAME="$(banger_dns_name "$VM_NAME")" - -if [[ -z "$ROOTFS" || -z "$KERNEL" || -z "$API_SOCK" || -z "$TAP_DEV" || -z "$GUEST_IP" || -z "$DM_NAME" || -z "$COW_FILE" ]]; then - log "vm.json missing required fields" - exit 1 -fi -if [[ ! -f "$ROOTFS" || ! -f "$KERNEL" || ! -f "$COW_FILE" || ! -f "$FC_BIN" ]]; then - log "missing disk/kernel file(s)" - exit 1 -fi - -if banger_vm_process_running "$PID" "$API_SOCK"; then - log "vm is already running: $VM_NAME" - exit 1 -fi - -sudo -v - -BR_DEV="br-fc" -BR_IP="172.16.0.1" -CIDR="24" -DNS_SERVER="1.1.1.1" - -VM_STARTED=0 -cleanup() { - local dm_dev_cleanup="${DM_DEV:-$DM_DEV_OLD}" - local cow_loop_cleanup="${COW_LOOP:-$COW_LOOP_OLD}" - local base_loop_cleanup="${BASE_LOOP:-$BASE_LOOP_OLD}" - - if [[ "$VM_STARTED" -eq 1 ]]; then - return - fi - banger_teardown_vm_runtime "$TAP_DEV" "$API_SOCK" "$DM_NAME" "$dm_dev_cleanup" "$cow_loop_cleanup" "$base_loop_cleanup" - banger_mark_vm_stopped "$VM_JSON" - banger_dns_remove_record_name "${DNS_NAME:-}" -} -trap cleanup EXIT - -# 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 - -sock_dir="$(dirname "$API_SOCK")" -sudo mkdir -p "$sock_dir" -sudo chown "$(id -u):$(id -g)" "$sock_dir" - -banger_teardown_vm_runtime "$TAP_DEV" "$API_SOCK" "$DM_NAME" "$DM_DEV_OLD" "$COW_LOOP_OLD" "$BASE_LOOP_OLD" -banger_mark_vm_stopped "$VM_JSON" - -# Recreate dm-snapshot -BASE_LOOP="$(sudo losetup -f --show --read-only "$ROOTFS")" -COW_LOOP="$(sudo losetup -f --show "$COW_FILE")" -SECTORS="$(sudo blockdev --getsz "$BASE_LOOP")" -sudo dmsetup create "$DM_NAME" --table "0 $SECTORS snapshot $BASE_LOOP $COW_LOOP P 8" -DM_DEV="/dev/mapper/$DM_NAME" - -jq \ - --arg base_loop "$BASE_LOOP" \ - --arg cow_loop "$COW_LOOP" \ - --arg dm_dev "$DM_DEV" \ - '.meta.base_loop=$base_loop | .meta.cow_loop=$cow_loop | .meta.dm_dev=$dm_dev' \ - "$VM_JSON" > "$VM_JSON.tmp" && mv "$VM_JSON.tmp" "$VM_JSON" - -# Update /etc/resolv.conf and hostname in snapshot -RESOLV_TMP="$VM_DIR/resolv.conf" -HOSTNAME_TMP="$VM_DIR/hostname" -HOSTS_TMP="$VM_DIR/hosts" -printf 'nameserver %s\n' "$DNS_SERVER" >"$RESOLV_TMP" -printf '%s\n' "$VM_NAME" >"$HOSTNAME_TMP" -printf '127.0.0.1 localhost\n127.0.1.1 %s\n' "$VM_NAME" >"$HOSTS_TMP" -sudo e2rm "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || true -sudo e2rm "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || true -sudo e2rm "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || true -sudo e2cp "$RESOLV_TMP" "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || true -sudo e2cp "$HOSTNAME_TMP" "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || true -sudo e2cp "$HOSTS_TMP" "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || true - -# TAP -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; } - -SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)" -if [[ -n "$SUDO_CHILD_PID" ]]; then - FC_PID="$SUDO_CHILD_PID" -fi - -log "configuring machine" -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ - -H "Content-Type: application/json" \ - -d "$(jq -c '.config["machine-config"]' "$VM_JSON")" >/dev/null - -boot_args="$(jq -r '.config["boot-source"].boot_args // empty' "$VM_JSON")" -boot_args="$(printf '%s' "$boot_args" | sed -E 's/(^| )hostname=[^ ]+//g; s/(^| )ip=[^ ]+//g; s/(^| )systemd\.mask=home\.mount//g; s/(^| )systemd\.mask=var\.mount//g' | awk '{$1=$1; print}')" -boot_args="$boot_args ip=${GUEST_IP}::${BR_IP}:255.255.255.0::eth0:off:${DNS_SERVER}" -boot_args="$boot_args hostname=$VM_NAME systemd.mask=home.mount systemd.mask=var.mount" -INITRD_JSON="" -if [[ -n "$INITRD_PATH" ]]; then - INITRD_JSON=", \"initrd_path\": \"$INITRD_PATH\"" -fi - -log "configuring boot source" -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/boot-source \ - -H "Content-Type: application/json" \ - -d "{ - \"kernel_image_path\": \"$KERNEL\", - \"boot_args\": \"$boot_args\"${INITRD_JSON} - }" >/dev/null - -log "attaching drives" -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"rootfs\", - \"path_on_host\": \"$DM_DEV\", - \"is_root_device\": true, - \"is_read_only\": false - }" >/dev/null - -log "configuring network interface" -/usr/bin/sudo /usr/bin/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 - -log "starting virtual machine" -/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -X PUT http://localhost/actions \ - -H "Content-Type: application/json" \ - -d '{ "action_type": "InstanceStart" }' >/dev/null - -CREATED_AT="$(date -Iseconds)" -banger_dns_write_record "$VM_NAME" "$GUEST_IP" -jq \ - --arg pid "$FC_PID" \ - --arg created_at "$CREATED_AT" \ - --arg dns_name "$DNS_NAME" \ - '.meta.pid=$pid | .meta.created_at=$created_at | .meta.dns_name=$dns_name | del(.meta.dns_file)' \ - "$VM_JSON" > "$VM_JSON.tmp" && mv "$VM_JSON.tmp" "$VM_JSON" - -VM_CONFIG_JSON="$(/usr/bin/sudo /usr/bin/curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)" -jq \ - --argjson config "$VM_CONFIG_JSON" \ - '.config=$config' \ - "$VM_JSON" > "$VM_JSON.tmp" && mv "$VM_JSON.tmp" "$VM_JSON" -VM_STARTED=1 - -log "restored" diff --git a/rm.sh b/rm.sh deleted file mode 100755 index 8e38197..0000000 --- a/rm.sh +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[rm] %s\n' "$*" -} - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/dns.sh" -STATE="$DIR/state" -VM_ROOT="$STATE/vms" - -usage() { - cat <<'EOF' -Usage: ./rm.sh ... - -Removes VM artifacts from state/ and cleans up TAP and mapped devices. -EOF -} - -find_vm_json() { - local query="$1" - local vm_json match_count=0 match="" - - for vm_json in "$VM_ROOT"/*/vm.json; do - [[ -f "$vm_json" ]] || continue - local id name - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then - match="$vm_json" - match_count=$((match_count + 1)) - fi - done - - if (( match_count == 0 )); then - log "no VM found for prefix: $query" - exit 1 - fi - if (( match_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - printf '%s' "$match" -} - -QUERIES=("$@") -if (( ${#QUERIES[@]} == 1 )) && [[ "${QUERIES[0]}" == "-h" || "${QUERIES[0]}" == "--help" ]]; then - usage - exit 0 -fi -if (( ${#QUERIES[@]} == 0 )); then - usage - exit 1 -fi - -VM_JSONS=() -for query in "${QUERIES[@]}"; do - VM_JSON="$(find_vm_json "$query")" - already_added=0 - for existing_vm_json in "${VM_JSONS[@]}"; do - if [[ "$existing_vm_json" == "$VM_JSON" ]]; then - already_added=1 - break - fi - done - if (( already_added == 0 )); then - VM_JSONS+=("$VM_JSON") - fi -done - -for vm_json in "${VM_JSONS[@]}"; do - vm_dir="$(dirname "$vm_json")" - vm_id="$(jq -r '.meta.id // empty' "$vm_json")" - vm_name="$(jq -r '.meta.name // empty' "$vm_json")" - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - tap="$(jq -r '.meta.tap // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - base_loop="$(jq -r '.meta.base_loop // empty' "$vm_json")" - cow_loop="$(jq -r '.meta.cow_loop // empty' "$vm_json")" - dm_dev="$(jq -r '.meta.dm_dev // empty' "$vm_json")" - dm_name="$(jq -r '.meta.dm_name // empty' "$vm_json")" - dns_name="$(jq -r '.meta.dns_name // empty' "$vm_json")" - if [[ -z "$dns_name" ]]; then - dns_name="$(banger_dns_name "$vm_name")" - fi - - if [[ -n "$pid" ]]; then - sudo kill "$pid" 2>/dev/null || true - fi - if [[ -n "$tap" ]]; then - sudo ip link del "$tap" 2>/dev/null || true - fi - if [[ -n "$api_sock" ]]; then - rm -f "$api_sock" - fi - banger_dns_remove_record_name "$dns_name" - if [[ -n "$dm_dev" || -n "$dm_name" ]]; then - sudo dmsetup remove "${dm_name:-$dm_dev}" 2>/dev/null || true - fi - if [[ -n "$cow_loop" ]]; then - sudo losetup -d "$cow_loop" 2>/dev/null || true - fi - if [[ -n "$base_loop" ]]; then - sudo losetup -d "$base_loop" 2>/dev/null || true - fi - if [[ -d "$vm_dir" ]]; then - rm -rf "$vm_dir" - fi - - log "removed ${vm_name:-$vm_dir}" -done diff --git a/run.sh b/run.sh deleted file mode 100755 index 28b0711..0000000 --- a/run.sh +++ /dev/null @@ -1,478 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[spawn] %s\n' "$*" -} - -usage() { - cat <<'EOF' -Usage: ./run.sh [options] - -Options: - --name VM name (lowercase letters, digits, -) - --vcpu vCPU count (default: 2) - --ram RAM in MiB (default: 1024) - --overlay-size Writable overlay size (e.g. 8G, 16384M) - --rootfs Root filesystem image (default: ./rootfs-docker.ext4) - --kernel Kernel image (default: ./vmlinux) - --initrd Initrd image (optional) - -h, --help Show this help -EOF -} - -log "starting" - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/dns.sh" -source "$DIR/packages.sh" -STATE="$DIR/state" -VM_ROOT="$STATE/vms" -mkdir -p "$VM_ROOT" - -FC_BIN="$DIR/firecracker" -DEFAULT_KERNEL="$DIR/wtf/root/boot/vmlinux-6.8.0-94-generic" -DEFAULT_ROOTFS="$DIR/rootfs-docker.ext4" -DEFAULT_INITRD="$DIR/wtf/root/boot/initrd.img-6.8.0-94-generic" -SSH_KEY="$DIR/id_ed25519" -NAMEGEN="$DIR/namegen" - -BR_DEV="br-fc" -BR_IP="172.16.0.1" -CIDR="24" - -DEFAULT_VCPU=2 -DEFAULT_RAM=1024 -DEFAULT_OVERLAY_SIZE="8G" -MIN_VCPU=1 -MAX_VCPU=16 -MIN_RAM=256 -MAX_RAM=32768 -MAX_DISK_BYTES=$((128 * 1024 * 1024 * 1024)) -DNS_SERVER="1.1.1.1" - -VCPU_COUNT="$DEFAULT_VCPU" -RAM_MIB="$DEFAULT_RAM" -OVERLAY_SIZE="$DEFAULT_OVERLAY_SIZE" -KERNEL="$DEFAULT_KERNEL" -ROOTFS="$DEFAULT_ROOTFS" -INITRD="$DEFAULT_INITRD" -VM_NAME="" - -shopt -s nullglob - -name_taken() { - local candidate="$1" - local vm_json existing_name - for vm_json in "$VM_ROOT"/*/vm.json; do - existing_name="$(jq -r '.meta.name // empty' "$vm_json" 2>/dev/null)" - if [[ "$existing_name" == "$candidate" ]]; then - return 0 - fi - done - return 1 -} - -parse_disk_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 -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --name) - VM_NAME="${2:-}" - shift 2 - ;; - --vcpu) - VCPU_COUNT="${2:-}" - shift 2 - ;; - --ram) - RAM_MIB="${2:-}" - shift 2 - ;; - --overlay-size) - OVERLAY_SIZE="${2:-}" - shift 2 - ;; - --rootfs) - ROOTFS="${2:-}" - shift 2 - ;; - --kernel) - KERNEL="${2:-}" - shift 2 - ;; - --initrd) - INITRD="${2:-}" - shift 2 - ;; - -h|--help) - usage - exit 0 - ;; - *) - log "unknown option: $1" - usage - exit 1 - ;; - esac -done - -if ! [[ "$VCPU_COUNT" =~ ^[0-9]+$ ]]; then - log "invalid --vcpu value: $VCPU_COUNT" - exit 1 -fi -if (( VCPU_COUNT < MIN_VCPU || VCPU_COUNT > MAX_VCPU )); then - log "vcpu must be between $MIN_VCPU and $MAX_VCPU" - exit 1 -fi - -if ! [[ "$RAM_MIB" =~ ^[0-9]+$ ]]; then - log "invalid --ram value: $RAM_MIB" - exit 1 -fi -if (( RAM_MIB < MIN_RAM || RAM_MIB > MAX_RAM )); then - log "ram must be between $MIN_RAM and $MAX_RAM MiB" - exit 1 -fi - -if ! OVERLAY_BYTES="$(parse_disk_size "$OVERLAY_SIZE")"; then - log "invalid --overlay-size value: $OVERLAY_SIZE" - exit 1 -fi -if (( OVERLAY_BYTES > MAX_DISK_BYTES )); then - log "overlay-size exceeds max of $((MAX_DISK_BYTES / 1024 / 1024 / 1024))G" - exit 1 -fi - -if [[ -z "$VM_NAME" ]]; then - if [[ ! -x "$NAMEGEN" ]]; then - log "name generator not found: $NAMEGEN" - exit 1 - fi - for _ in $(seq 1 20); do - VM_NAME="$("$NAMEGEN")" - if ! name_taken "$VM_NAME"; then - break - fi - done -fi - -if [[ -z "$VM_NAME" ]]; then - log "failed to generate a unique name" - exit 1 -fi - -if ! [[ "$VM_NAME" =~ ^[a-z0-9][a-z0-9-]{0,63}$ ]]; then - log "invalid --name value: $VM_NAME" - exit 1 -fi -if [[ ! -f "$ROOTFS" ]]; then - if [[ "$ROOTFS" == "$DEFAULT_ROOTFS" && -x "$DIR/make-rootfs.sh" ]]; then - log "rootfs missing; building via make-rootfs.sh" - "$DIR/make-rootfs.sh" - fi -fi -if [[ ! -f "$ROOTFS" ]]; then - log "rootfs not found: $ROOTFS" - exit 1 -fi -if [[ "$ROOTFS" == "$DEFAULT_ROOTFS" ]]; then - ROOTFS_WARNING="$(banger_rootfs_manifest_warning "$ROOTFS" || true)" - if [[ -n "$ROOTFS_WARNING" ]]; then - log "warning: $ROOTFS_WARNING" - fi -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 name_taken "$VM_NAME"; then - log "name already in use: $VM_NAME" - exit 1 -fi - -VM_ID="$(head -c 32 /dev/urandom | xxd -p -c 256)" -VM_TAG="${VM_ID:0:8}" -VM_DIR="$VM_ROOT/$VM_ID" -mkdir -p "$VM_DIR" - -RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" -SOCK_DIR="$RUNTIME_DIR/banger" -sudo mkdir -p "$SOCK_DIR" -sudo chown "$(id -u):$(id -g)" "$SOCK_DIR" -API_SOCK="$SOCK_DIR/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" - -log "vm id: $VM_ID" -log "vm name: $VM_NAME" -log "allocated guest ip: $GUEST_IP" - -sudo -v - -VM_STARTED=0 -CLEANUP_ON_EXIT=0 -KEEP_VM_DIR_ON_FAIL=1 -COW_FILE="$VM_DIR/cow.ext4" -BASE_LOOP="" -COW_LOOP="" -DM_NAME="fc-rootfs-$VM_TAG" -DM_DEV="" -DNS_NAME="" - -cleanup() { - local exit_code=$? - if [[ "$VM_STARTED" -eq 1 && "$CLEANUP_ON_EXIT" -eq 0 ]]; then - return - fi - log "cleaning up" - if [[ -n "${FC_PID:-}" ]]; then - sudo kill "$FC_PID" 2>/dev/null || true - fi - if [[ -n "${TAP_DEV:-}" ]]; then - sudo ip link del "$TAP_DEV" 2>/dev/null || true - fi - rm -f "${API_SOCK:-}" - banger_dns_remove_record_name "${DNS_NAME:-}" - if [[ -n "${DM_NAME:-}" ]]; then - sudo dmsetup remove "$DM_NAME" 2>/dev/null || true - fi - if [[ -n "${COW_LOOP:-}" ]]; then - sudo losetup -d "$COW_LOOP" 2>/dev/null || true - fi - if [[ -n "${BASE_LOOP:-}" ]]; then - sudo losetup -d "$BASE_LOOP" 2>/dev/null || true - fi - if [[ -n "${VM_DIR:-}" && -d "$VM_DIR" ]]; then - if [[ "$KEEP_VM_DIR_ON_FAIL" -eq 0 || "$VM_STARTED" -eq 1 || "$CLEANUP_ON_EXIT" -eq 1 ]]; then - rm -rf "$VM_DIR" - fi - fi - return "$exit_code" -} - -on_signal() { - CLEANUP_ON_EXIT=1 - exit 1 -} - -trap cleanup EXIT -trap on_signal INT TERM - -FC_USE_SUDO="${FC_USE_SUDO:-1}" -FC_RUN=("$FC_BIN") -CURL_CMD=(curl) -if [[ "$FC_USE_SUDO" == "1" ]]; then - log "running firecracker with sudo (FC_USE_SUDO=1)" - FC_RUN=(sudo -E "$FC_BIN") - CURL_CMD=(sudo -E curl) -fi - -if command -v setcap >/dev/null 2>&1; then - if ! getcap "$FC_BIN" 2>/dev/null | rg -q "cap_net_admin"; then - log "granting cap_net_admin to firecracker binary" - sudo setcap cap_net_admin+ep "$FC_BIN" - fi -else - log "setcap not available; firecracker may need root to open TAP" -fi - -if ! command -v dmsetup >/dev/null 2>&1 || ! command -v losetup >/dev/null 2>&1 || ! command -v blockdev >/dev/null 2>&1; then - log "dmsetup, losetup, and blockdev are required for rootfs snapshots" - exit 1 -fi -if ! command -v e2cp >/dev/null 2>&1 || ! command -v e2rm >/dev/null 2>&1; then - log "e2cp and e2rm are required to set hostname and resolv.conf" - exit 1 -fi -if ! command -v jq >/dev/null 2>&1; then - log "jq is required to persist VM metadata" - exit 1 -fi - -BASE_LOOP="$(sudo losetup -f --show --read-only "$ROOTFS")" -truncate -s "$OVERLAY_BYTES" "$COW_FILE" -COW_LOOP="$(sudo losetup -f --show "$COW_FILE")" -SECTORS="$(sudo blockdev --getsz "$BASE_LOOP")" -sudo dmsetup create "$DM_NAME" --table "0 $SECTORS snapshot $BASE_LOOP $COW_LOOP P 8" -DM_DEV="/dev/mapper/$DM_NAME" - -RESOLV_TMP="$VM_DIR/resolv.conf" -HOSTNAME_TMP="$VM_DIR/hostname" -HOSTS_TMP="$VM_DIR/hosts" -printf 'nameserver %s\n' "$DNS_SERVER" >"$RESOLV_TMP" -printf '%s\n' "$VM_NAME" >"$HOSTNAME_TMP" -printf '127.0.0.1 localhost\n127.0.1.1 %s\n' "$VM_NAME" >"$HOSTS_TMP" -sudo e2rm "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || true -sudo e2rm "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || true -sudo e2rm "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || true -sudo e2cp "$RESOLV_TMP" "$DM_DEV:/etc/resolv.conf" >/dev/null 2>&1 || { - log "failed to write /etc/resolv.conf into rootfs snapshot" - exit 1 -} -sudo e2cp "$HOSTNAME_TMP" "$DM_DEV:/etc/hostname" >/dev/null 2>&1 || { - log "failed to write /etc/hostname into rootfs snapshot" - exit 1 -} -sudo e2cp "$HOSTS_TMP" "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || { - log "failed to write /etc/hosts into rootfs snapshot" - exit 1 -} - -# 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 - log "host bridge $BR_DEV already exists" - # Ensure existing bridge is up in case it was left down. - sudo ip link set "$BR_DEV" up -fi - -# Per-VM TAP -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 - -# Start Firecracker -log "starting firecracker process" -rm -f "$API_SOCK" -nohup "${FC_RUN[@]}" --api-sock "$API_SOCK" >"$LOG_FILE" 2>&1 & -FC_PID="$!" -log "firecracker pid: $FC_PID" - -# Wait for API socket -log "waiting for firecracker api socket" -for _ in $(seq 1 200); do - [[ -S "$API_SOCK" ]] && break - sleep 0.02 -done -if [[ ! -S "$API_SOCK" ]]; then - log "firecracker api socket not ready" - if [[ -f "$LOG_FILE" ]]; then - log "firecracker log (tail)" - tail -n 20 "$LOG_FILE" || true - fi - exit 1 -fi -log "api socket ready" - -if [[ "$FC_USE_SUDO" == "1" ]]; then - SUDO_CHILD_PID="$(pgrep -n -f "$API_SOCK" || true)" - if [[ -n "$SUDO_CHILD_PID" ]]; then - FC_PID="$SUDO_CHILD_PID" - log "firecracker child pid: $FC_PID" - fi -fi -echo "$FC_PID" > "$VM_DIR/pid" - -# Machine config -log "configuring machine" -"${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ - -H "Content-Type: application/json" \ - -d "{ - \"vcpu_count\": $VCPU_COUNT, - \"mem_size_mib\": $RAM_MIB, - \"smt\": false - }" >/dev/null - -# Boot source -log "configuring boot source" -KCMD="console=ttyS0 reboot=k panic=1 pci=off root=/dev/vda rw ip=${GUEST_IP}::${BR_IP}:255.255.255.0::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 - -"${CURL_CMD[@]}" --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 - -# Root filesystem -log "attaching root filesystem" -"${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/drives/rootfs \ - -H "Content-Type: application/json" \ - -d "{ - \"drive_id\": \"rootfs\", - \"path_on_host\": \"$DM_DEV\", - \"is_root_device\": true, - \"is_read_only\": false - }" >/dev/null - -# Network interface -log "configuring network interface" -"${CURL_CMD[@]}" --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 - -# Start VM -log "starting virtual machine" -"${CURL_CMD[@]}" --unix-socket "$API_SOCK" -X PUT http://localhost/actions \ - -H "Content-Type: application/json" \ - -d '{ "action_type": "InstanceStart" }' >/dev/null -VM_CONFIG_JSON="$("${CURL_CMD[@]}" --unix-socket "$API_SOCK" -sS http://localhost/vm/config)" -CREATED_AT="$(date -Iseconds)" -DNS_NAME="$(banger_dns_name "$VM_NAME")" -banger_dns_write_record "$VM_NAME" "$GUEST_IP" -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 "$ROOTFS" \ - --arg kernel "$KERNEL" \ - --arg base_loop "$BASE_LOOP" \ - --arg cow_file "$COW_FILE" \ - --arg cow_loop "$COW_LOOP" \ - --arg dm_name "$DM_NAME" \ - --arg dm_dev "$DM_DEV" \ - --arg dns_name "$DNS_NAME" \ - --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,base_loop:$base_loop,cow_file:$cow_file,cow_loop:$cow_loop,dm_name:$dm_name,dm_dev:$dm_dev,dns_name:$dns_name},config:$config}' \ - > "$VM_DIR/vm.json" -VM_STARTED=1 - -log "vm started successfully" -log "guest ip: $GUEST_IP" -log "ssh: ssh -i \"$SSH_KEY\" root@$GUEST_IP" -log "logs: $LOG_FILE" diff --git a/ssh.sh b/ssh.sh deleted file mode 100755 index 6990e92..0000000 --- a/ssh.sh +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[ssh] %s\n' "$*" -} - -usage() { - cat <<'EOF' -Usage: ./ssh.sh [ssh-args...] - -Resolves a running VM by name, guest IP, or ID prefix and opens SSH as root. -EOF -} - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STATE="$DIR/state" -VM_ROOT="$STATE/vms" -SSH_KEY="$DIR/id_ed25519" - -find_vm_json() { - local query="$1" - local vm_json exact_match="" prefix_match="" - local exact_count=0 prefix_count=0 - - for vm_json in "$VM_ROOT"/*/vm.json; do - [[ -f "$vm_json" ]] || continue - - local id name guest_ip - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - guest_ip="$(jq -r '.meta.guest_ip // empty' "$vm_json")" - - if [[ "$guest_ip" == "$query" || "$name" == "$query" || "$id" == "$query" ]]; then - exact_match="$vm_json" - exact_count=$((exact_count + 1)) - continue - fi - - if [[ "$name" == "$query"* || "$id" == "$query"* ]]; then - prefix_match="$vm_json" - prefix_count=$((prefix_count + 1)) - fi - done - - if (( exact_count == 1 )); then - printf '%s' "$exact_match" - return 0 - fi - if (( exact_count > 1 )); then - log "multiple VMs found for: $query" - exit 1 - fi - if (( prefix_count == 1 )); then - printf '%s' "$prefix_match" - return 0 - fi - if (( prefix_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - log "no VM found for: $query" - exit 1 -} - -vm_is_running() { - local vm_json="$1" - local pid api_sock - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - - [[ -n "$pid" && -n "$api_sock" ]] || return 1 - ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock" -} - -QUERY="${1:-}" -if [[ "$QUERY" == "-h" || "$QUERY" == "--help" ]]; then - usage - exit 0 -fi -if [[ -z "$QUERY" ]]; then - usage - exit 1 -fi -shift - -if [[ ! -f "$SSH_KEY" ]]; then - log "ssh key not found: $SSH_KEY" - exit 1 -fi - -VM_JSON="$(find_vm_json "$QUERY")" -if ! vm_is_running "$VM_JSON"; then - log "vm is not running: $QUERY" - exit 1 -fi - -GUEST_IP="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")" -VM_NAME="$(jq -r '.meta.name // empty' "$VM_JSON")" -if [[ -z "$GUEST_IP" ]]; then - log "guest IP not found for: $QUERY" - exit 1 -fi - -log "connecting to $VM_NAME ($GUEST_IP)" -exec ssh \ - -i "$SSH_KEY" \ - -o StrictHostKeyChecking=no \ - -o UserKnownHostsFile=/dev/null \ - "root@$GUEST_IP" \ - "$@" diff --git a/stop.sh b/stop.sh deleted file mode 100755 index 01f2f37..0000000 --- a/stop.sh +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -log() { - printf '[stop] %s\n' "$*" -} - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$DIR/dns.sh" -STATE="$DIR/state" -VM_ROOT="$STATE/vms" - -usage() { - cat <<'EOF' -Usage: ./stop.sh ... - -Sends Ctrl+Alt+Del to the guest via the Firecracker API socket. -EOF -} - -find_vm_json() { - local query="$1" - local vm_json match_count=0 match="" - - for vm_json in "$VM_ROOT"/*/vm.json; do - [[ -f "$vm_json" ]] || continue - local id name - id="$(jq -r '.meta.id // empty' "$vm_json")" - name="$(jq -r '.meta.name // empty' "$vm_json")" - if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then - match="$vm_json" - match_count=$((match_count + 1)) - fi - done - - if (( match_count == 0 )); then - log "no VM found for prefix: $query" - exit 1 - fi - if (( match_count > 1 )); then - log "multiple VMs found for prefix: $query" - exit 1 - fi - - printf '%s' "$match" -} - -QUERIES=("$@") -if (( ${#QUERIES[@]} == 1 )) && [[ "${QUERIES[0]}" == "-h" || "${QUERIES[0]}" == "--help" ]]; then - usage - exit 0 -fi -if (( ${#QUERIES[@]} == 0 )); then - usage - exit 1 -fi - -VM_JSONS=() -for query in "${QUERIES[@]}"; do - VM_JSON="$(find_vm_json "$query")" - already_added=0 - for existing_vm_json in "${VM_JSONS[@]}"; do - if [[ "$existing_vm_json" == "$VM_JSON" ]]; then - already_added=1 - break - fi - done - if (( already_added == 0 )); then - VM_JSONS+=("$VM_JSON") - fi -done - -for vm_json in "${VM_JSONS[@]}"; do - vm_id="$(jq -r '.meta.id // empty' "$vm_json")" - vm_name="$(jq -r '.meta.name // empty' "$vm_json")" - pid="$(jq -r '.meta.pid // empty' "$vm_json")" - tap="$(jq -r '.meta.tap // empty' "$vm_json")" - api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")" - base_loop="$(jq -r '.meta.base_loop // empty' "$vm_json")" - cow_loop="$(jq -r '.meta.cow_loop // empty' "$vm_json")" - dm_dev="$(jq -r '.meta.dm_dev // empty' "$vm_json")" - dm_name="$(jq -r '.meta.dm_name // empty' "$vm_json")" - dns_name="$(jq -r '.meta.dns_name // empty' "$vm_json")" - if [[ -z "$dns_name" ]]; then - dns_name="$(banger_dns_name "$vm_name")" - fi - if [[ -z "$pid" ]]; then - log "pid not found for ${vm_name:-$vm_json}" - exit 1 - fi - if [[ -z "$api_sock" || ! -S "$api_sock" ]]; then - log "api socket not found for ${vm_name:-$vm_json}: $api_sock" - exit 1 - fi - - log "sending Ctrl+Alt+Del to ${vm_name:-$vm_json}" - sudo -E curl --unix-socket "$api_sock" -X PUT http://localhost/actions \ - -H "Content-Type: application/json" \ - -d '{ "action_type": "SendCtrlAltDel" }' >/dev/null - if ! banger_wait_for_vm_exit "$pid" "$api_sock" 30; then - log "timed out waiting for ${vm_name:-$vm_json} to exit" - exit 1 - fi - banger_teardown_vm_runtime "$tap" "$api_sock" "$dm_name" "$dm_dev" "$cow_loop" "$base_loop" - banger_mark_vm_stopped "$vm_json" - banger_dns_remove_record_name "$dns_name" - log "requested shutdown for ${vm_name:-$vm_json}" -done