Improve VM lifecycle tooling

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.
This commit is contained in:
Thales Maciel 2026-03-15 17:48:47 -03:00
parent a8078f2393
commit 9191b7e370
No known key found for this signature in database
GPG key ID: 33112E6833C34679
11 changed files with 966 additions and 144 deletions

54
run.sh
View file

@ -25,6 +25,7 @@ EOF
log "starting"
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/dns.sh"
STATE="$DIR/state"
VM_ROOT="$STATE/vms"
mkdir -p "$VM_ROOT"
@ -254,6 +255,41 @@ BASE_LOOP=""
COW_LOOP=""
DM_NAME="fc-rootfs-$VM_TAG"
DM_DEV=""
SEED_ROOT_MNT="$VM_DIR/mnt-root"
SEED_HOME_MNT="$VM_DIR/mnt-home"
SEED_VAR_MNT="$VM_DIR/mnt-var"
DNS_NAME=""
unmount_if_needed() {
local mount_path="$1"
[[ -n "$mount_path" ]] || return 0
sudo umount "$mount_path" 2>/dev/null || true
}
seed_volume_from_rootfs() {
local source_dir="$1"
local target_image="$2"
local target_mount="$3"
local label="$4"
if [[ ! -d "$source_dir" ]]; then
log "source directory missing in rootfs snapshot: $label"
exit 1
fi
mkdir -p "$target_mount"
sudo mount "$target_image" "$target_mount"
sudo cp -a "$source_dir/." "$target_mount/"
sudo umount "$target_mount"
}
populate_data_disks() {
mkdir -p "$SEED_ROOT_MNT" "$SEED_HOME_MNT" "$SEED_VAR_MNT"
sudo mount -o ro "$DM_DEV" "$SEED_ROOT_MNT"
seed_volume_from_rootfs "$SEED_ROOT_MNT/home" "$HOME_PATH" "$SEED_HOME_MNT" "/home"
seed_volume_from_rootfs "$SEED_ROOT_MNT/var" "$VAR_PATH" "$SEED_VAR_MNT" "/var"
sudo umount "$SEED_ROOT_MNT"
}
cleanup() {
local exit_code=$?
@ -268,6 +304,10 @@ cleanup() {
sudo ip link del "$TAP_DEV" 2>/dev/null || true
fi
rm -f "${API_SOCK:-}"
unmount_if_needed "${SEED_VAR_MNT:-}"
unmount_if_needed "${SEED_HOME_MNT:-}"
unmount_if_needed "${SEED_ROOT_MNT:-}"
banger_dns_remove_record_name "${DNS_NAME:-}"
if [[ -n "${DM_NAME:-}" ]]; then
sudo dmsetup remove "$DM_NAME" 2>/dev/null || true
fi
@ -328,6 +368,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"
exit 1
fi
if ! command -v mount >/dev/null 2>&1 || ! command -v umount >/dev/null 2>&1; then
log "mount and umount are required to populate home/var disks"
exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
log "jq is required to persist VM metadata"
exit 1
@ -368,6 +412,9 @@ sudo e2cp "$HOSTS_TMP" "$DM_DEV:/etc/hosts" >/dev/null 2>&1 || {
exit 1
}
log "populating /home and /var disks from rootfs snapshot"
populate_data_disks
# Host bridge
if ! ip link show "$BR_DEV" >/dev/null 2>&1; then
log "creating host bridge $BR_DEV ($BR_IP/$CIDR)"
@ -494,9 +541,10 @@ 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_STARTED=1
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" \
@ -515,9 +563,11 @@ jq -n \
--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,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}' \
'{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,dns_name:$dns_name},config:$config}' \
> "$VM_DIR/vm.json"
VM_STARTED=1
log "vm started successfully"
log "guest ip: $GUEST_IP"