Store VM metadata as JSON

This commit is contained in:
Thales Maciel 2026-01-31 23:17:12 -03:00
parent bbd57d8dd2
commit 7af04b7535
No known key found for this signature in database
GPG key ID: 33112E6833C34679
11 changed files with 188 additions and 178 deletions

View file

@ -4,7 +4,7 @@ Minimal Firecracker launcher.
## Requirements ## Requirements
- Linux host with KVM (`/dev/kvm` access) - Linux host with KVM (`/dev/kvm` access)
- `sudo`, `ip`, `curl`, `ssh` - `sudo`, `ip`, `curl`, `ssh`, `jq`
- `dmsetup`, `losetup`, `blockdev` (device-mapper snapshot for rootfs) - `dmsetup`, `losetup`, `blockdev` (device-mapper snapshot for rootfs)
- `e2cp`, `e2rm` (writes hostname and resolv.conf into rootfs snapshot) - `e2cp`, `e2rm` (writes hostname and resolv.conf into rootfs snapshot)
@ -63,22 +63,9 @@ reboot
``` ```
## VM Info File ## VM Info File
Each VM writes a metadata file at `state/vms/<id>/info` with the following fields: Each VM writes `state/vms/<id>/vm.json` with:
- `id`: unique identifier for the VM instance. - `meta`: local metadata (id, name, pid, created_at, guest_ip, tap, api_sock, log, rootfs, kernel, snapshot info).
- `name`: VM name. - `config`: full `/vm/config` response from Firecracker.
- `pid`: Firecracker process ID.
- `created_at`: timestamp when the VM was launched.
- `rootfs`: root filesystem image path used to launch the VM.
- `kernel`: kernel image path used to launch the VM.
- `guest_ip`: IP address assigned to the guest.
- `tap`: host TAP interface name attached to the bridge.
- `api_sock`: path to the Firecracker API socket (stored under `$XDG_RUNTIME_DIR/banger/` when available).
- `log`: path to the Firecracker log file.
- `base_loop`: loop device backing the base rootfs (if present).
- `cow_file`: copy-on-write image file (if present).
- `cow_loop`: loop device for the COW image (if present).
- `dm_name`: device-mapper name for the merged rootfs (if present).
- `dm_dev`: device-mapper device path for the merged rootfs (if present).
## Log Notes ## Log Notes
- `PCI: Fatal: No config space access function found` and `MissingAddressRange` lines are expected with `pci=off` in `run.sh`. - `PCI: Fatal: No config space access function found` and `MissingAddressRange` lines are expected with `pci=off` in `run.sh`.

View file

@ -89,6 +89,10 @@ if ! command -v resize2fs >/dev/null 2>&1; then
log "resize2fs required" log "resize2fs required"
exit 1 exit 1
fi fi
if ! command -v jq >/dev/null 2>&1; then
log "jq required"
exit 1
fi
log "copying base rootfs to $OUT_ROOTFS" log "copying base rootfs to $OUT_ROOTFS"
cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS" cp --reflink=auto "$BASE_ROOTFS" "$OUT_ROOTFS"
@ -165,16 +169,22 @@ for _ in $(seq 1 200); do
done done
[[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; } [[ -S "$API_SOCK" ]] || { log "firecracker api socket not ready"; exit 1; }
cat > "$VM_DIR/info" <<EOF VM_CONFIG_JSON="$(sudo -E curl --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
id=$VM_ID CREATED_AT="$(date -Iseconds)"
name=$VM_NAME jq -n \
pid=$FC_PID --arg id "$VM_ID" \
created_at=$(date -Iseconds) --arg name "$VM_NAME" \
guest_ip=$GUEST_IP --arg pid "$FC_PID" \
tap=$TAP_DEV --arg created_at "$CREATED_AT" \
api_sock=$API_SOCK --arg guest_ip "$GUEST_IP" \
log=$LOG_FILE --arg tap "$TAP_DEV" \
EOF --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 "configuring machine" log "configuring machine"
sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \ sudo -E curl --unix-socket "$API_SOCK" -X PUT http://localhost/machine-config \
@ -232,6 +242,19 @@ ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf printf 'nameserver %s\n' \"$DNS_SERVER\" > /etc/resolv.conf
echo \"$VM_NAME\" > /etc/hostname echo \"$VM_NAME\" > /etc/hostname
printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts printf '127.0.0.1 localhost\n127.0.1.1 %s\n' \"$VM_NAME\" > /etc/hosts
mkdir -p /home /var
if ! grep -q '^/dev/vdb ' /etc/fstab; then
echo '/dev/vdb /home ext4 defaults 0 2' >> /etc/fstab
fi
if ! grep -q '^/dev/vdc ' /etc/fstab; then
echo '/dev/vdc /var ext4 defaults 0 2' >> /etc/fstab
fi
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 apt-get update
DEBIAN_FRONTEND=noninteractive apt-get -y upgrade DEBIAN_FRONTEND=noninteractive apt-get -y upgrade
DEBIAN_FRONTEND=noninteractive apt-get -y install git less tree ca-certificates curl DEBIAN_FRONTEND=noninteractive apt-get -y install git less tree ca-certificates curl

30
kill.sh
View file

@ -13,23 +13,17 @@ Sends a signal to the Firecracker process.
EOF EOF
} }
get_prop() { find_vm_json() {
local info="$1"
local key="$2"
awk -F= -v k="$key" '$1==k {print $2}' "$info"
}
find_vm_info() {
local query="$1" local query="$1"
local info match_count=0 match="" local vm_json match_count=0 match=""
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
[[ -f "$info" ]] || continue [[ -f "$vm_json" ]] || continue
local id name local id name
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then
match="$info" match="$vm_json"
match_count=$((match_count + 1)) match_count=$((match_count + 1))
fi fi
done done
@ -75,15 +69,15 @@ if [[ -z "$QUERY" || "$QUERY" == "-h" || "$QUERY" == "--help" ]]; then
exit 1 exit 1
fi fi
INFO_FILE="$(find_vm_info "$QUERY")" VM_JSON="$(find_vm_json "$QUERY")"
PID="$(get_prop "$INFO_FILE" "pid")" PID="$(jq -r '.meta.pid // empty' "$VM_JSON")"
API_SOCK="$(get_prop "$INFO_FILE" "api_sock")" API_SOCK="$(jq -r '.meta.api_sock // empty' "$VM_JSON")"
if [[ -z "$PID" ]]; then if [[ -z "$PID" ]]; then
log "pid not found in $INFO_FILE" log "pid not found in $VM_JSON"
exit 1 exit 1
fi fi
if [[ -z "$API_SOCK" ]]; then if [[ -z "$API_SOCK" ]]; then
log "api_sock not found in $INFO_FILE" log "api_sock not found in $VM_JSON"
exit 1 exit 1
fi fi

55
list.sh
View file

@ -3,28 +3,51 @@ set -euo pipefail
shopt -s nullglob shopt -s nullglob
get_prop() { statuses=()
local info="$1" ids=()
local key="$2" names=()
awk -F= -v k="$key" '$1==k {print $2}' "$info" ips=()
} created=()
max_name=4
max_ip=2
GREEN='\033[32m'
YELLOW='\033[33m'
RESET='\033[0m'
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
created_at="$(get_prop "$info" "created_at")" created_at="$(jq -r '.meta.created_at // empty' "$vm_json")"
guest_ip="$(get_prop "$info" "guest_ip")" guest_ip="$(jq -r '.meta.guest_ip // empty' "$vm_json")"
pid="$(get_prop "$info" "pid")" pid="$(jq -r '.meta.pid // empty' "$vm_json")"
tap="$(get_prop "$info" "tap")" api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")"
api_sock="$(get_prop "$info" "api_sock")"
status="stale" status="stopped"
if [[ -n "$pid" && -n "$api_sock" ]]; then 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 if ps -p "$pid" -o comm=,args= 2>/dev/null | rg -q "firecracker.*--api-sock $api_sock"; then
status="running" status="running"
fi fi
fi fi
printf 'status=%s id=%s name=%s created_at=%s guest_ip=%s pid=%s tap=%s\n' \ short_id="${id:0:12}"
"$status" "$id" "$name" "$created_at" "$guest_ip" "$pid" "$tap" statuses+=("$status")
ids+=("$short_id")
names+=("$name")
ips+=("$guest_ip")
created+=("$created_at")
if (( ${#name} > max_name )); then
max_name=${#name}
fi
if (( ${#guest_ip} > max_ip )); then
max_ip=${#guest_ip}
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]}" "${created[$i]}"
done done

24
logs.sh
View file

@ -13,23 +13,17 @@ Prints the Firecracker log for a VM. Use --follow to tail -f.
EOF EOF
} }
get_prop() { find_vm_json() {
local info="$1"
local key="$2"
awk -F= -v k="$key" '$1==k {print $2}' "$info"
}
find_vm_info() {
local query="$1" local query="$1"
local info match_count=0 match="" local vm_json match_count=0 match=""
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
[[ -f "$info" ]] || continue [[ -f "$vm_json" ]] || continue
local id name local id name
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then
match="$info" match="$vm_json"
match_count=$((match_count + 1)) match_count=$((match_count + 1))
fi fi
done done
@ -76,8 +70,8 @@ if [[ -z "$QUERY" ]]; then
exit 1 exit 1
fi fi
INFO_FILE="$(find_vm_info "$QUERY")" VM_JSON="$(find_vm_json "$QUERY")"
LOG_FILE="$(get_prop "$INFO_FILE" "log")" LOG_FILE="$(jq -r '.meta.log // empty' "$VM_JSON")"
if [[ -z "$LOG_FILE" || ! -f "$LOG_FILE" ]]; then if [[ -z "$LOG_FILE" || ! -f "$LOG_FILE" ]]; then
log "log file not found: $LOG_FILE" log "log file not found: $LOG_FILE"

28
nat.sh
View file

@ -13,23 +13,17 @@ Manage per-VM NAT rules for internet access.
EOF EOF
} }
get_prop() { find_vm_json() {
local info="$1"
local key="$2"
awk -F= -v k="$key" '$1==k {print $2}' "$info"
}
find_vm_info() {
local query="$1" local query="$1"
local info match_count=0 match="" local vm_json match_count=0 match=""
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
[[ -f "$info" ]] || continue [[ -f "$vm_json" ]] || continue
local id name local id name
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then
match="$info" match="$vm_json"
match_count=$((match_count + 1)) match_count=$((match_count + 1))
fi fi
done done
@ -64,12 +58,12 @@ if [[ -z "$ACTION" || -z "$QUERY" ]]; then
exit 1 exit 1
fi fi
INFO_FILE="$(find_vm_info "$QUERY")" VM_JSON="$(find_vm_json "$QUERY")"
GUEST_IP="$(get_prop "$INFO_FILE" "guest_ip")" GUEST_IP="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")"
TAP="$(get_prop "$INFO_FILE" "tap")" TAP="$(jq -r '.meta.tap // empty' "$VM_JSON")"
if [[ -z "$GUEST_IP" || -z "$TAP" ]]; then if [[ -z "$GUEST_IP" || -z "$TAP" ]]; then
log "missing guest_ip or tap in $INFO_FILE" log "missing guest_ip or tap in $VM_JSON"
exit 1 exit 1
fi fi

22
ps.sh
View file

@ -3,20 +3,14 @@ set -euo pipefail
shopt -s nullglob shopt -s nullglob
get_prop() { for vm_json in state/vms/*/vm.json; do
local info="$1" id="$(jq -r '.meta.id // empty' "$vm_json")"
local key="$2" name="$(jq -r '.meta.name // empty' "$vm_json")"
awk -F= -v k="$key" '$1==k {print $2}' "$info" 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")"
for info in state/vms/*/info; do tap="$(jq -r '.meta.tap // empty' "$vm_json")"
id="$(get_prop "$info" "id")" api_sock="$(jq -r '.meta.api_sock // empty' "$vm_json")"
name="$(get_prop "$info" "name")"
created_at="$(get_prop "$info" "created_at")"
guest_ip="$(get_prop "$info" "guest_ip")"
pid="$(get_prop "$info" "pid")"
tap="$(get_prop "$info" "tap")"
api_sock="$(get_prop "$info" "api_sock")"
if [[ -z "$pid" || -z "$api_sock" ]]; then if [[ -z "$pid" || -z "$api_sock" ]]; then
continue continue

38
rm.sh
View file

@ -13,23 +13,17 @@ Removes VM artifacts from state/ and cleans up TAP and mapped devices.
EOF EOF
} }
get_prop() { find_vm_json() {
local info="$1"
local key="$2"
awk -F= -v k="$key" '$1==k {print $2}' "$info"
}
find_vm_info() {
local query="$1" local query="$1"
local info match_count=0 match="" local vm_json match_count=0 match=""
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
[[ -f "$info" ]] || continue [[ -f "$vm_json" ]] || continue
local id name local id name
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then
match="$info" match="$vm_json"
match_count=$((match_count + 1)) match_count=$((match_count + 1))
fi fi
done done
@ -52,15 +46,15 @@ if [[ -z "$QUERY" || "$QUERY" == "-h" || "$QUERY" == "--help" ]]; then
exit 1 exit 1
fi fi
INFO_FILE="$(find_vm_info "$QUERY")" VM_JSON="$(find_vm_json "$QUERY")"
VM_DIR="${INFO_FILE%/info}" VM_DIR="$(dirname "$VM_JSON")"
PID="$(get_prop "$INFO_FILE" "pid")" PID="$(jq -r '.meta.pid // empty' "$VM_JSON")"
TAP="$(get_prop "$INFO_FILE" "tap")" TAP="$(jq -r '.meta.tap // empty' "$VM_JSON")"
API_SOCK="$(get_prop "$INFO_FILE" "api_sock")" API_SOCK="$(jq -r '.meta.api_sock // empty' "$VM_JSON")"
BASE_LOOP="$(get_prop "$INFO_FILE" "base_loop")" BASE_LOOP="$(jq -r '.meta.base_loop // empty' "$VM_JSON")"
COW_LOOP="$(get_prop "$INFO_FILE" "cow_loop")" COW_LOOP="$(jq -r '.meta.cow_loop // empty' "$VM_JSON")"
DM_DEV="$(get_prop "$INFO_FILE" "dm_dev")" DM_DEV="$(jq -r '.meta.dm_dev // empty' "$VM_JSON")"
DM_NAME="$(get_prop "$INFO_FILE" "dm_name")" DM_NAME="$(jq -r '.meta.dm_name // empty' "$VM_JSON")"
if [[ -n "$PID" ]]; then if [[ -n "$PID" ]]; then
sudo kill "$PID" 2>/dev/null || true sudo kill "$PID" 2>/dev/null || true

51
run.sh
View file

@ -62,9 +62,9 @@ shopt -s nullglob
name_taken() { name_taken() {
local candidate="$1" local candidate="$1"
local info existing_name local vm_json existing_name
for info in "$VM_ROOT"/*/info; do for vm_json in "$VM_ROOT"/*/vm.json; do
existing_name="$(awk -F= '$1=="name"{print $2}' "$info")" existing_name="$(jq -r '.meta.name // empty' "$vm_json" 2>/dev/null)"
if [[ "$existing_name" == "$candidate" ]]; then if [[ "$existing_name" == "$candidate" ]]; then
return 0 return 0
fi fi
@ -311,6 +311,10 @@ 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" log "e2cp and e2rm are required to set hostname and resolv.conf"
exit 1 exit 1
fi fi
if ! command -v jq >/dev/null 2>&1; then
log "jq is required to persist VM metadata"
exit 1
fi
COW_BYTES="$(parse_disk_size "$COW_SIZE")" COW_BYTES="$(parse_disk_size "$COW_SIZE")"
if [[ -z "$COW_BYTES" ]]; then if [[ -z "$COW_BYTES" ]]; then
@ -469,24 +473,29 @@ log "starting virtual machine"
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d '{ "action_type": "InstanceStart" }' >/dev/null -d '{ "action_type": "InstanceStart" }' >/dev/null
VM_STARTED=1 VM_STARTED=1
VM_CONFIG_JSON="$("${CURL_CMD[@]}" --unix-socket "$API_SOCK" -sS http://localhost/vm/config)"
cat > "$VM_DIR/info" <<EOF CREATED_AT="$(date -Iseconds)"
id=$VM_ID jq -n \
name=$VM_NAME --arg id "$VM_ID" \
pid=$FC_PID --arg name "$VM_NAME" \
created_at=$(date -Iseconds) --arg pid "$FC_PID" \
rootfs=$ROOTFS --arg created_at "$CREATED_AT" \
kernel=$KERNEL --arg guest_ip "$GUEST_IP" \
guest_ip=$GUEST_IP --arg tap "$TAP_DEV" \
tap=$TAP_DEV --arg api_sock "$API_SOCK" \
api_sock=$API_SOCK --arg log "$LOG_FILE" \
log=$LOG_FILE --arg rootfs "$ROOTFS" \
base_loop=$BASE_LOOP --arg kernel "$KERNEL" \
cow_file=$COW_FILE --arg home_path "$HOME_PATH" \
cow_loop=$COW_LOOP --arg var_path "$VAR_PATH" \
dm_name=$DM_NAME --arg base_loop "$BASE_LOOP" \
dm_dev=$DM_DEV --arg cow_file "$COW_FILE" \
EOF --arg cow_loop "$COW_LOOP" \
--arg dm_name "$DM_NAME" \
--arg dm_dev "$DM_DEV" \
--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,home_path:$home_path,var_path:$var_path,base_loop:$base_loop,cow_file:$cow_file,cow_loop:$cow_loop,dm_name:$dm_name,dm_dev:$dm_dev},config:$config}' \
> "$VM_DIR/vm.json"
log "vm started successfully" log "vm started successfully"
log "guest ip: $GUEST_IP" log "guest ip: $GUEST_IP"

24
stop.sh
View file

@ -13,23 +13,17 @@ Sends Ctrl+Alt+Del to the guest via the Firecracker API socket.
EOF EOF
} }
get_prop() { find_vm_json() {
local info="$1"
local key="$2"
awk -F= -v k="$key" '$1==k {print $2}' "$info"
}
find_vm_info() {
local query="$1" local query="$1"
local info match_count=0 match="" local vm_json match_count=0 match=""
for info in state/vms/*/info; do for vm_json in state/vms/*/vm.json; do
[[ -f "$info" ]] || continue [[ -f "$vm_json" ]] || continue
local id name local id name
id="$(get_prop "$info" "id")" id="$(jq -r '.meta.id // empty' "$vm_json")"
name="$(get_prop "$info" "name")" name="$(jq -r '.meta.name // empty' "$vm_json")"
if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then if [[ "$id" == "$query"* || "$name" == "$query"* ]]; then
match="$info" match="$vm_json"
match_count=$((match_count + 1)) match_count=$((match_count + 1))
fi fi
done done
@ -52,8 +46,8 @@ if [[ -z "$QUERY" || "$QUERY" == "-h" || "$QUERY" == "--help" ]]; then
exit 1 exit 1
fi fi
INFO_FILE="$(find_vm_info "$QUERY")" VM_JSON="$(find_vm_json "$QUERY")"
API_SOCK="$(get_prop "$INFO_FILE" "api_sock")" API_SOCK="$(jq -r '.meta.api_sock // empty' "$VM_JSON")"
if [[ -z "$API_SOCK" || ! -S "$API_SOCK" ]]; then if [[ -z "$API_SOCK" || ! -S "$API_SOCK" ]]; then
log "api socket not found: $API_SOCK" log "api socket not found: $API_SOCK"
exit 1 exit 1

View file

@ -6,18 +6,19 @@ log() {
} }
cleanup() { cleanup() {
if [[ -z "${VM_INFO:-}" || ! -f "$VM_INFO" ]]; then if [[ -z "${VM_JSON:-}" || ! -f "$VM_JSON" ]]; then
return return
fi fi
# shellcheck disable=SC1090 pid="$(jq -r '.meta.pid // empty' "$VM_JSON")"
source "$VM_INFO" tap="$(jq -r '.meta.tap // empty' "$VM_JSON")"
if [[ -n "${pid:-}" ]]; then vm_dir="$(dirname "$VM_JSON")"
if [[ -n "$pid" ]]; then
sudo kill "$pid" 2>/dev/null || true sudo kill "$pid" 2>/dev/null || true
fi fi
if [[ -n "${tap:-}" ]]; then if [[ -n "$tap" ]]; then
sudo ip link del "$tap" 2>/dev/null || true sudo ip link del "$tap" 2>/dev/null || true
fi fi
if [[ -n "${vm_dir:-}" ]]; then if [[ -n "$vm_dir" ]]; then
rm -rf "$vm_dir" rm -rf "$vm_dir"
fi fi
} }
@ -35,18 +36,21 @@ if [[ -z "$VM_DIR" ]]; then
log "no VM state directory found" log "no VM state directory found"
exit 1 exit 1
fi fi
VM_INFO="$VM_DIR/info" VM_JSON="$VM_DIR/vm.json"
if [[ ! -f "$VM_INFO" ]]; then if [[ ! -f "$VM_JSON" ]]; then
log "info file not found: $VM_INFO" log "vm.json not found: $VM_JSON"
exit 1 exit 1
fi fi
# shellcheck disable=SC1090 name="$(jq -r '.meta.name // empty' "$VM_JSON")"
source "$VM_INFO" created_at="$(jq -r '.meta.created_at // empty' "$VM_JSON")"
guest_ip="$(jq -r '.meta.guest_ip // empty' "$VM_JSON")"
tap="$(jq -r '.meta.tap // empty' "$VM_JSON")"
pid="$(jq -r '.meta.pid // empty' "$VM_JSON")"
vm_dir="$VM_DIR" vm_dir="$VM_DIR"
if [[ -z "${name:-}" || -z "${created_at:-}" || -z "${guest_ip:-}" ]]; then if [[ -z "$name" || -z "$created_at" || -z "$guest_ip" ]]; then
log "missing name or created_at in info file" log "missing name or created_at in vm.json"
exit 1 exit 1
fi fi