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.
This commit is contained in:
Thales Maciel 2026-03-16 16:21:31 -03:00
parent 67e531aa27
commit 67cfdd659f
9 changed files with 0 additions and 1423 deletions

125
kill.sh
View file

@ -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 <id-or-name-prefix>... [--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

120
list.sh
View file

@ -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

85
logs.sh
View file

@ -1,85 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[logs] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./logs.sh <id-or-name-prefix> [--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

23
ps.sh
View file

@ -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

View file

@ -1,259 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[restore] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./restore.sh <id-or-name-prefix>
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"

113
rm.sh
View file

@ -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 <id-or-name-prefix>...
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

478
run.sh
View file

@ -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 <name> VM name (lowercase letters, digits, -)
--vcpu <count> vCPU count (default: 2)
--ram <mib> RAM in MiB (default: 1024)
--overlay-size <size> Writable overlay size (e.g. 8G, 16384M)
--rootfs <path> Root filesystem image (default: ./rootfs-docker.ext4)
--kernel <path> Kernel image (default: ./vmlinux)
--initrd <path> 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"

112
ssh.sh
View file

@ -1,112 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
log() {
printf '[ssh] %s\n' "$*"
}
usage() {
cat <<'EOF'
Usage: ./ssh.sh <name-or-ip-or-id-prefix> [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" \
"$@"

108
stop.sh
View file

@ -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 <id-or-name-prefix>...
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