diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-05-12 15:06:28 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-05-12 22:27:15 +0300 |
| commit | 4715a28fdcd87440400d17154bfa361d99db29cc (patch) | |
| tree | 52b825ca69ed3fc535f8f46de0ee1d2b1a6e2ade /vm.sh | |
| parent | ed338ea29f1c0f2ad82d3a45b23c0544371b5575 (diff) | |
Add local vm for testing, adjust configs
Diffstat (limited to 'vm.sh')
| -rwxr-xr-x | vm.sh | 246 |
1 files changed, 246 insertions, 0 deletions
@@ -0,0 +1,246 @@ +#!/usr/bin/env bash +set -euo pipefail + +# vm.sh — Manage a QEMU FreeBSD aarch64 VM for testing Ansible playbooks. +# +# Usage: +# ./vm.sh dl Download the base image (one-time) +# ./vm.sh init Extract disk, create seed ISO and SSH key +# ./vm.sh up Start the VM (requires sudo for vmnet) +# ./vm.sh down Shut down the VM gracefully +# ./vm.sh kill Force-kill the VM +# ./vm.sh reset Delete disk & state, keeping the downloaded base image +# ./vm.sh ssh SSH into the VM +# ./vm.sh status Check if the VM is running +# ./vm.sh console Attach to serial console (ctrl-a x to exit) +# +# Prerequisites: brew install qemu socat cdrtools +# +# Networking (vmnet-shared, subnet 10.0.20.0/24): +# .1 Mac (vmnet gateway, provides NAT + DNS) +# .2 VM host LAN (lan0) +# .3 Ingress jail WAN interface +# .101-XXX Jails LAN (jail number + 100) +# +# All IPs are directly reachable from the Mac without extra routes. +# Run playbook: ansible-playbook -i inventory playbook.yml --limit test_vm + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VM_DIR="${SCRIPT_DIR}/vm" +DISK="${VM_DIR}/disk.qcow2" +PFLASH_VARS="${VM_DIR}/pflash_vars.fd" +PID_FILE="${VM_DIR}/qemu.pid" +MONITOR_SOCK="${VM_DIR}/qemu-monitor.sock" +SERIAL_SOCK="${VM_DIR}/qemu-serial.sock" + +FREEBSD_VERSION="${FREEBSD_VERSION:-15.0}" +IMAGE_NAME="FreeBSD-${FREEBSD_VERSION}-RELEASE-arm64-aarch64-BASIC-CLOUDINIT-zfs.qcow2" +IMAGE_XZ="${VM_DIR}/${IMAGE_NAME}.xz" +IMAGE_URL="https://download.freebsd.org/releases/VM-IMAGES/${FREEBSD_VERSION}-RELEASE/aarch64/Latest/${IMAGE_NAME}.xz" + +PFLASH_CODE="$(brew --prefix)/share/qemu/edk2-aarch64-code.fd" +SEED_ISO="${VM_DIR}/seed.iso" +SSH_KEY="${VM_DIR}/id_ed25519" + +SSH_PORT_LAN=22 +VM_RAM=2048 +VM_CPUS=2 +DISK_SIZE=20G + +cmd_dl() { + mkdir -p "$VM_DIR" + if [[ -f "$IMAGE_XZ" ]]; then + echo "Base image already downloaded: $IMAGE_XZ" + return + fi + echo "Downloading FreeBSD ${FREEBSD_VERSION} aarch64 VM image..." + curl --fail -C - -L -o "$IMAGE_XZ" "$IMAGE_URL" || { rm -f "$IMAGE_XZ"; echo "Download failed: $IMAGE_URL"; exit 1; } + echo "Done." +} + +prepare_disk() { + if [[ ! -f "$IMAGE_XZ" ]]; then + echo "Base image not found. Run: ./vm.sh dl" + exit 1 + fi + echo "Extracting disk from base image..." + xz -dk "$IMAGE_XZ" + mv "${VM_DIR}/${IMAGE_NAME}" "$DISK" + qemu-img resize "$DISK" "$DISK_SIZE" +} + +prepare_seed() { + [[ -f "$SEED_ISO" ]] && return + if [[ ! -f "$SSH_KEY" ]]; then + ssh-keygen -t ed25519 -f "$SSH_KEY" -N "" -q + fi + local pubkey + pubkey=$(cat "${SSH_KEY}.pub") + local seed_dir="${VM_DIR}/seed" + mkdir -p "$seed_dir" + cat > "$seed_dir/meta-data" <<EOF +instance-id: pursotin-vm +local-hostname: pursotin +EOF + cat > "$seed_dir/user-data" <<EOF +#cloud-config +ssh_pwauth: true +disable_root: false + +users: + - name: root + ssh_authorized_keys: + - ${pubkey} + +packages: + - python3 + +runcmd: + - pkg install -y python3 + - cp /usr/share/zoneinfo/Europe/Helsinki /etc/localtime + - sysrc ifconfig_vtnet0="inet 10.0.20.2/24" + - sysrc defaultrouter="10.0.20.1" + - ifconfig vtnet0 inet 10.0.20.2/24 + - route add default 10.0.20.1 + - sed -i '' 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config + - sed -i '' 's/^PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config + - service sshd restart +EOF + mkisofs -output "$SEED_ISO" -volid cidata -joliet -rock \ + "$seed_dir/user-data" "$seed_dir/meta-data" + rm -rf "$seed_dir" +} + +cmd_init() { + mkdir -p "$VM_DIR" + if [[ -f "$DISK" ]]; then + echo "Already initialized. Use './vm.sh reset' first to re-initialize." + exit 0 + fi + [[ -f "$IMAGE_XZ" ]] || cmd_dl + prepare_disk + prepare_seed + if [[ ! -f "$PFLASH_VARS" ]]; then + dd if=/dev/zero of="$PFLASH_VARS" bs=1m count=64 2>/dev/null + fi + echo "Initialized. Run './vm.sh up' to start." +} + +cmd_up() { + if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "VM already running (PID $(cat "$PID_FILE"))" + exit 0 + fi + + if [[ ! -f "$DISK" ]]; then + echo "Not initialized. Run: ./vm.sh init" + exit 1 + fi + + echo "Starting VM (requires sudo for vmnet)..." + sudo -v || { echo "sudo required for vmnet networking"; exit 1; } + sudo qemu-system-aarch64 \ + -M virt,highmem=on \ + -accel hvf \ + -cpu host \ + -m "$VM_RAM" \ + -smp "$VM_CPUS" \ + -drive if=pflash,format=raw,file="$PFLASH_CODE",readonly=on \ + -drive if=pflash,format=raw,file="$PFLASH_VARS" \ + -drive file="$DISK",format=qcow2,if=virtio \ + -cdrom "$SEED_ISO" \ + -netdev vmnet-shared,id=lan,start-address=10.0.20.1,end-address=10.0.20.254,subnet-mask=255.255.255.0 \ + -device virtio-net-pci,netdev=lan \ + -netdev vmnet-shared,id=wan,start-address=10.0.20.1,end-address=10.0.20.254,subnet-mask=255.255.255.0 \ + -device virtio-net-pci,netdev=wan \ + -serial unix:"$SERIAL_SOCK",server,nowait \ + -monitor unix:"$MONITOR_SOCK",server,nowait \ + -pidfile "$PID_FILE" \ + -daemonize \ + -display none \ + && sudo chmod 644 "$PID_FILE" \ + && sudo chmod 777 "$SERIAL_SOCK" "$MONITOR_SOCK" \ + && echo "VM started (PID $(cat "$PID_FILE"))" \ + && echo "SSH: ./vm.sh ssh" \ + && echo "Console: ./vm.sh console" +} + +cmd_down() { + if [[ ! -S "$MONITOR_SOCK" ]]; then + echo "VM not running (no monitor socket)" + exit 1 + fi + echo "system_powerdown" | sudo socat - UNIX-CONNECT:"$MONITOR_SOCK" + echo "Sent powerdown signal." + local pid + pid=$(cat "$PID_FILE" 2>/dev/null || true) + if [[ -n "$pid" ]]; then + for _ in $(seq 1 30); do + sudo kill -0 "$pid" 2>/dev/null || break + sleep 1 + done + if sudo kill -0 "$pid" 2>/dev/null; then + echo "VM still running after 30s. Use './vm.sh kill' to force." + else + rm -f "$PID_FILE" + echo "VM stopped." + fi + fi +} + +cmd_kill() { + if [[ -f "$PID_FILE" ]]; then + local pid + pid=$(cat "$PID_FILE") + sudo kill -9 "$pid" 2>/dev/null && echo "Killed VM (PID $pid)" || echo "Process not found" + rm -f "$PID_FILE" + else + echo "No PID file found" + fi +} + +cmd_reset() { + cmd_kill 2>/dev/null || true + rm -f "$DISK" "$PFLASH_VARS" "$SEED_ISO" "$SSH_KEY" "${SSH_KEY}.pub" + rm -f "$MONITOR_SOCK" "$SERIAL_SOCK" "$PID_FILE" + echo "VM state reset. Base image kept at $IMAGE_XZ" +} + +cmd_ssh() { + exec ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -i "$SSH_KEY" root@10.0.20.2 "$@" +} + +cmd_console() { + if [[ ! -S "$SERIAL_SOCK" ]]; then + echo "VM not running (no serial socket)" + exit 1 + fi + echo "Connecting to serial console (ctrl-a x to exit)..." + exec socat -,rawer,escape=0x01 UNIX-CONNECT:"$SERIAL_SOCK" +} + +cmd_status() { + if [[ -f "$PID_FILE" ]] && sudo kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then + echo "VM running (PID $(cat "$PID_FILE"))" + else + echo "VM not running" + rm -f "$PID_FILE" 2>/dev/null + fi +} + +case "${1:-}" in + dl) cmd_dl ;; + init) cmd_init ;; + up) cmd_up ;; + down) cmd_down ;; + kill) cmd_kill ;; + reset) cmd_reset ;; + ssh) shift; cmd_ssh "$@" ;; + console) cmd_console ;; + status) cmd_status ;; + *) + echo "Usage: $0 {dl|init|up|down|kill|reset|ssh|console|status}" + exit 1 + ;; +esac |
