Make spawned VMs easier to use and restore from the host. Add shared DNS and runtime helpers, publish <vm-name>.vm records through mapdns, and teach run/customize/interactive/restore to persist the metadata needed for SSH, DNS cleanup, and clean restores. Seed per-VM /home and /var disks from the rootfs snapshot so package state is present on first boot, add an interactive customization entrypoint plus ssh.sh and human-friendly list output, and let stop/kill/rm operate on multiple VM identifiers. Tear down stale TAP, dm, and loop state when VMs stop so restore can recreate them safely, and validate the updated scripts with bash -n plus targeted dry-run harnesses for teardown and restore paths.
112 lines
2.4 KiB
Bash
Executable file
112 lines
2.4 KiB
Bash
Executable file
#!/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" \
|
|
"$@"
|