#!/usr/bin/env bash # Create or delete a batch of test VMs against a running firecrab-api, for # manually exercising concurrent start/disk-prep behavior # (public-docs/guides/troubleshooting.md). set -u API="${FIRECRAB_API:-http://127.0.0.1:5523}" ORIGIN="${FIRECRAB_ORIGIN:-http://localhost:8080}" STATE_FILE="${FIRECRAB_BATCH_STATE:-/tmp/firecrab-vm-batch.ids}" usage() { cat </dev/null 2>&1 || { echo "jq is required" >&2 exit 1 } } cmd_create() { count="${1:-10}" prefix="${2:-batch}" : >"$STATE_FILE" echo "creating $count VM(s) with prefix '$prefix'..." for i in $(seq 1 "$count"); do id=$(curl -s -X POST "$API/api/vms" \ -H "Content-Type: application/json" -H "Origin: $ORIGIN" \ -d "{\"name\":\"${prefix}${i}\",\"template\":\"ubuntu-26.04\",\"cpu\":1,\"ram\":512,\"diskGb\":2}" \ | jq -r '.id // empty') if [ -z "$id" ]; then echo " [$i] create failed" >&2 continue fi echo "$id" >>"$STATE_FILE" echo " [$i] $id" done echo "$(wc -l <"$STATE_FILE" | tr -d ' ') VM id(s) saved to $STATE_FILE" } each_id() { [ -s "$STATE_FILE" ] || { echo "no ids in $STATE_FILE — run 'create' first" >&2 exit 1 } while IFS= read -r id; do [ -n "$id" ] && "$@" "$id" & done <"$STATE_FILE" wait } cmd_start() { each_id start_one } start_one() { report_async_result "$1" "$(curl -s -w '\n%{http_code}' -X POST "$API/api/vms/$1/start" -H "Origin: $ORIGIN")" } cmd_stop() { each_id stop_one } stop_one() { report_async_result "$1" "$(curl -s -w '\n%{http_code}' -X POST "$API/api/vms/$1/stop" -H "Origin: $ORIGIN")" } # A 504 here just means the response took longer than the server's request # timeout — start_vm's actual work is detached (tokio::spawn) and keeps # running regardless (public-docs/guides/troubleshooting.md), # so this isn't a failure, just "check status later" instead of "now". report_async_result() { id="$1" http_code=$(printf '%s' "$2" | tail -n1) body=$(printf '%s' "$2" | sed '$d') if [ "$http_code" = "504" ]; then echo " ${id:0:8} -> timed out client-side, still running server-side (check: status)" else state=$(printf '%s' "$body" | jq -r '.state // .error.message // "unknown"') echo " ${id:0:8} -> $state" fi } cmd_delete() { each_id delete_one : >"$STATE_FILE" } delete_one() { status=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE "$API/api/vms/$1" -H "Origin: $ORIGIN") echo " ${1:0:8} -> HTTP $status" } cmd_status() { [ -s "$STATE_FILE" ] || { echo "no ids in $STATE_FILE — run 'create' first" >&2 exit 1 } curl -s "$API/api/vms" -H "Origin: $ORIGIN" \ | jq -r --slurpfile ids <(jq -R . <"$STATE_FILE" | jq -s .) \ '.[] | select(.id as $i | $ids[0] | index($i)) | "\(.name)\t\(.state)\t\(.startupStep // "-")\t\(.id)"' \ | column -t } require_jq case "${1:-}" in create) shift; cmd_create "$@" ;; start) cmd_start ;; stop) cmd_stop ;; delete) cmd_delete ;; status) cmd_status ;; *) usage; exit 1 ;; esac