freebsd_vm.sh (25373B)
1 #!/usr/bin/env bash 2 # Prepare and run FreeBSD release VMs used as execution targets for kit tests. 3 # 4 # Defaults target FreeBSD 15.0-RELEASE VM images: 5 # amd64 -> BASIC-CLOUDINIT UFS qcow2 6 # aarch64 -> BASIC-CLOUDINIT UFS qcow2 7 # riscv64 -> UFS qcow2 (no BASIC-CLOUDINIT image is published) 8 # 9 # Downloaded .xz images live under XDG cache by default so `make clean` does 10 # not remove them. Expanded VM disks, firmware vars, and seed ISOs live under 11 # build/freebsd-vm/ by default. 12 13 set -eu 14 15 ROOT="$(cd "$(dirname "$0")/.." && pwd)" 16 VM_ROOT="${KIT_FREEBSD_VM_DIR:-$ROOT/build/freebsd-vm}" 17 RELEASE="${KIT_FREEBSD_RELEASE:-15.0-RELEASE}" 18 XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}" 19 DL_ROOT="${KIT_FREEBSD_DOWNLOAD_DIR:-$XDG_CACHE_HOME/kit/freebsd-vm/$RELEASE}" 20 BASE_URL="${KIT_FREEBSD_BASE_URL:-https://download.freebsd.org/releases/VM-IMAGES/$RELEASE}" 21 QEMU_SHARE="${KIT_QEMU_SHARE:-/opt/homebrew/share/qemu}" 22 SSH_USER="${KIT_FREEBSD_SSH_USER:-kit}" 23 # The SSH key lives in the download cache (alongside the golden disks) so 24 # `make clean` does not wipe it -- the key is baked into the golden disk's 25 # authorized_keys, so losing it would lock the cached disks out. (Sysroots are 26 # built VM-free from base.txz; see scripts/freebsd_sysroot.sh.) 27 SSH_KEY="${KIT_FREEBSD_SSH_KEY:-$DL_ROOT/ssh/id_ed25519}" 28 29 usage() { 30 cat <<EOF 31 usage: scripts/freebsd_vm.sh <command> <arch> [args...] 32 33 commands: 34 doctor print local QEMU/image tool availability 35 fetch <arch> download image and checksum 36 prepare <arch> restore/build a provisioned, ready-to-run disk 37 seed <arch> create a NoCloud cidata ISO for cloud-init images 38 provision <arch> boot once offline to create user+sshd, cache golden disk 39 run <arch> [qemu...] run VM in foreground with host-only SSH forwarding 40 wait-ssh <arch> wait for SSH and print uname 41 ssh <arch> [cmd...] SSH into a running VM 42 scp <arch> <src> <dst> copy a file into the running VM (dst is remote path) 43 run-batch <arch> <dir> <res> 44 ship a staging dir into the VM, run its run-remote.sh, 45 and bring per-binary res/<id>.{rc,out,err} back into 46 <res> (used by test/lib/exec_vm.sh) 47 48 arches: 49 amd64 | x64 | aarch64 | arm64 | rv64 | riscv64 50 51 env: 52 KIT_FREEBSD_VM_DIR artifact directory (default: build/freebsd-vm) 53 KIT_FREEBSD_DOWNLOAD_DIR 54 downloaded .xz/checksum cache 55 (default: \${XDG_CACHE_HOME:-\$HOME/.cache}/kit/freebsd-vm/<release>) 56 KIT_FREEBSD_RELEASE FreeBSD release (default: 15.0-RELEASE) 57 KIT_FREEBSD_MEM QEMU memory in MiB (default: 2048) 58 KIT_FREEBSD_CPUS QEMU CPU count (default: 2) 59 KIT_FREEBSD_PROVISION_TIMEOUT 60 seconds to wait for the offline provisioning boot to 61 reach a login prompt (default: 1800) 62 KIT_FREEBSD_ACCEL QEMU accel override (default: hvf for aarch64 on arm64 macOS) 63 KIT_FREEBSD_SSH_USER cloud-init user (default: kit) 64 KIT_FREEBSD_SSH_KEY private SSH key path 65 EOF 66 } 67 68 die() { 69 printf 'freebsd-vm: %s\n' "$*" >&2 70 exit 1 71 } 72 73 canon_arch() { 74 case "${1:-}" in 75 amd64|x64|x86_64) echo amd64 ;; 76 aarch64|arm64|aa64) echo aarch64 ;; 77 riscv64|rv64) echo riscv64 ;; 78 *) die "unknown arch '${1:-}'" ;; 79 esac 80 } 81 82 arch_dir() { 83 case "$(canon_arch "$1")" in 84 amd64) echo amd64 ;; 85 aarch64) echo aarch64 ;; 86 riscv64) echo riscv64 ;; 87 esac 88 } 89 90 image_file() { 91 case "$(canon_arch "$1")" in 92 amd64) echo "FreeBSD-$RELEASE-amd64-BASIC-CLOUDINIT-ufs.qcow2.xz" ;; 93 aarch64) echo "FreeBSD-$RELEASE-arm64-aarch64-BASIC-CLOUDINIT-ufs.qcow2.xz" ;; 94 riscv64) echo "FreeBSD-$RELEASE-riscv-riscv64-ufs.qcow2.xz" ;; 95 esac 96 } 97 98 cloudinit_supported() { 99 case "$(canon_arch "$1")" in 100 amd64|aarch64) return 0 ;; 101 riscv64) return 1 ;; 102 esac 103 } 104 105 ssh_port() { 106 case "$(canon_arch "$1")" in 107 amd64) echo "${KIT_FREEBSD_AMD64_SSH_PORT:-2222}" ;; 108 aarch64) echo "${KIT_FREEBSD_AARCH64_SSH_PORT:-2223}" ;; 109 riscv64) echo "${KIT_FREEBSD_RISCV64_SSH_PORT:-2224}" ;; 110 esac 111 } 112 113 qemu_bin() { 114 case "$(canon_arch "$1")" in 115 amd64) echo qemu-system-x86_64 ;; 116 aarch64) echo qemu-system-aarch64 ;; 117 riscv64) echo qemu-system-riscv64 ;; 118 esac 119 } 120 121 disk_path() { 122 echo "$VM_ROOT/images/freebsd-$(canon_arch "$1").qcow2" 123 } 124 125 seed_path() { 126 echo "$VM_ROOT/seeds/freebsd-$(canon_arch "$1")-seed.iso" 127 } 128 129 checksum_path() { 130 echo "$DL_ROOT/$(canon_arch "$1")-CHECKSUM.SHA256" 131 } 132 133 download_path() { 134 echo "$DL_ROOT/$(image_file "$1")" 135 } 136 137 host_is_darwin_arm64() { 138 [ "$(uname -s 2>/dev/null)" = "Darwin" ] && 139 { [ "$(uname -m 2>/dev/null)" = "arm64" ] || 140 [ "$(uname -m 2>/dev/null)" = "aarch64" ]; } 141 } 142 143 qemu_accel_args() { 144 local arch="$1" accel="${KIT_FREEBSD_ACCEL:-}" 145 if [ -z "$accel" ] && [ "$(canon_arch "$arch")" = "aarch64" ] && 146 host_is_darwin_arm64; then 147 accel=hvf 148 fi 149 [ -n "$accel" ] && printf '%s\n%s\n' -accel "$accel" 150 } 151 152 firmware_code() { 153 case "$(canon_arch "$1")" in 154 amd64) echo "$QEMU_SHARE/edk2-x86_64-code.fd" ;; 155 aarch64) echo "$QEMU_SHARE/edk2-aarch64-code.fd" ;; 156 riscv64) echo "$QEMU_SHARE/edk2-riscv-code.fd" ;; 157 esac 158 } 159 160 firmware_vars_template() { 161 case "$(canon_arch "$1")" in 162 amd64) echo "$QEMU_SHARE/edk2-i386-vars.fd" ;; 163 aarch64) echo "$QEMU_SHARE/edk2-arm-vars.fd" ;; 164 riscv64) echo "$QEMU_SHARE/edk2-riscv-vars.fd" ;; 165 esac 166 } 167 168 firmware_vars() { 169 echo "$VM_ROOT/firmware/$(canon_arch "$1")-vars.fd" 170 } 171 172 ensure_ssh_key() { 173 if [ ! -f "$SSH_KEY" ]; then 174 mkdir -p "$(dirname "$SSH_KEY")" 175 ssh-keygen -q -t ed25519 -N "" -f "$SSH_KEY" 176 fi 177 } 178 179 fetch_arch() { 180 local arch="$1" dir file url cksum 181 arch="$(canon_arch "$arch")" 182 dir="$(arch_dir "$arch")" 183 file="$(image_file "$arch")" 184 url="$BASE_URL/$dir/Latest/$file" 185 mkdir -p "$DL_ROOT" 186 printf 'fetch checksum: %s\n' "$BASE_URL/$dir/Latest/CHECKSUM.SHA256" 187 curl -fL --retry 3 --retry-delay 2 -o "$(checksum_path "$arch")" \ 188 "$BASE_URL/$dir/Latest/CHECKSUM.SHA256" 189 printf 'fetch image: %s\n' "$url" 190 curl -fL --continue-at - --retry 3 --retry-delay 2 \ 191 -o "$(download_path "$arch")" "$url" 192 cksum="$(awk -v f="$file" '$2 == "(" f ")" && $3 == "=" { print $4 }' \ 193 "$(checksum_path "$arch")")" 194 [ -n "$cksum" ] || die "checksum entry missing for $file" 195 verify_sha256 "$arch" "$cksum" 196 } 197 198 verify_sha256() { 199 local arch="$1" expect="$2" got 200 got="$(shasum -a 256 "$(download_path "$arch")" | awk '{ print $1 }')" 201 [ "$got" = "$expect" ] || 202 die "checksum mismatch for $(image_file "$arch"): got $got want $expect" 203 printf 'verified: %s\n' "$(image_file "$arch")" 204 } 205 206 # Produce a ready-to-run, provisioned disk: restore the cached golden disk if we 207 # have one, otherwise download/expand the pristine image and provision it. The 208 # heavy lifting lives in ensure_disk (defined below). 209 prepare_arch() { 210 ensure_disk "$1" 211 } 212 213 seed_arch() { 214 local arch="$1" seed_dir seed key 215 arch="$(canon_arch "$arch")" 216 cloudinit_supported "$arch" || 217 die "$arch does not have a FreeBSD BASIC-CLOUDINIT VM image" 218 ensure_ssh_key 219 mkdir -p "$VM_ROOT/seeds" 220 seed_dir="$VM_ROOT/seeds/$(canon_arch "$arch")" 221 seed="$(seed_path "$arch")" 222 key="$(cat "$SSH_KEY.pub")" 223 rm -rf "$seed_dir" 224 mkdir -p "$seed_dir" 225 cat > "$seed_dir/meta-data" <<EOF 226 local-hostname: kit-freebsd-$arch 227 EOF 228 # nuageinit (FreeBSD's native cloud-init) processes this #cloud-config. 229 # runcmd only re-asserts sshd_enable (idempotent): sshd is already enabled in 230 # the image rc.conf, so the base rc starts it exactly once. We deliberately do 231 # NOT "service sshd start" here -- that races the base rc start and dies with 232 # "Bind to port 22 ... Address already in use". 233 cat > "$seed_dir/user-data" <<EOF 234 #cloud-config 235 users: 236 - name: $SSH_USER 237 gecos: Kit FreeBSD Test User 238 groups: wheel 239 homedir: /home/$SSH_USER 240 shell: /bin/sh 241 sudo: ALL=(ALL) NOPASSWD:ALL 242 ssh_authorized_keys: 243 - "$key" 244 ssh_authorized_keys: 245 - "$key" 246 network: 247 version: 2 248 ethernets: 249 vtnet0: 250 dhcp4: true 251 runcmd: 252 - sysrc sshd_enable=YES 253 EOF 254 rm -f "$seed" 255 hdiutil makehybrid -quiet -iso -joliet -default-volume-name cidata \ 256 -o "$seed" "$seed_dir" 257 printf 'seed ready: %s\n' "$seed" 258 } 259 260 ensure_firmware_vars() { 261 local arch="$1" code vars tmpl 262 code="$(firmware_code "$arch")" 263 vars="$(firmware_vars "$arch")" 264 tmpl="$(firmware_vars_template "$arch")" 265 [ -f "$code" ] || die "missing QEMU firmware code: $code" 266 [ -f "$tmpl" ] || die "missing QEMU firmware vars template: $tmpl" 267 mkdir -p "$(dirname "$vars")" 268 # Always start from the pristine template. The EDK2 firmware persists its 269 # boot order / NVRAM into this vars file; once it drifts (e.g. after a run 270 # with a different device set) it can boot the built-in UEFI Shell instead 271 # of chainloading the disk, which looks exactly like a hang. Resetting per 272 # run keeps the boot path deterministic; nothing here relies on NVRAM 273 # persisting across runs. 274 cp "$tmpl" "$vars" 275 } 276 277 append_firmware_args() { 278 local arch="$1" code vars 279 ensure_firmware_vars "$arch" 280 code="$(firmware_code "$arch")" 281 vars="$(firmware_vars "$arch")" 282 QEMU_ARGS=("${QEMU_ARGS[@]}" 283 -drive "if=pflash,format=raw,readonly=on,file=$code" 284 -drive "if=pflash,format=raw,file=$vars") 285 } 286 287 # append_common_args ARCH DISK PORT [RESTRICT] [SERIALSPEC] 288 # RESTRICT non-empty -> isolate the guest from the outside network. DHCP and 289 # the SSH hostfwd still work, but outbound connections fail fast. Used while 290 # provisioning so firstboot_freebsd_update cannot run its slow networked 291 # update (and force a reboot) on the first boot. 292 # SERIALSPEC defaults to "mon:stdio" (interactive foreground). Provisioning 293 # passes "file:<log>" so a backgrounded boot can be watched. 294 append_common_args() { 295 local arch="$1" disk="$2" port="$3" restrict="${4:-}" serialspec="${5:-mon:stdio}" 296 local mem="${KIT_FREEBSD_MEM:-2048}" cpus="${KIT_FREEBSD_CPUS:-2}" 297 local netopt="user,id=net0,hostfwd=tcp:127.0.0.1:$port-:22" 298 [ -n "$restrict" ] && netopt="user,id=net0,restrict=on,hostfwd=tcp:127.0.0.1:$port-:22" 299 QEMU_ARGS=("${QEMU_ARGS[@]}" 300 -m "$mem" 301 -smp "$cpus" 302 -drive "if=virtio,format=qcow2,file=$disk" 303 -netdev "$netopt" 304 -device "virtio-net-pci,netdev=net0" 305 -device "virtio-rng-pci" 306 -display none 307 -serial "$serialspec") 308 } 309 310 append_seed_args() { 311 local arch="$1" seed 312 if cloudinit_supported "$arch"; then 313 seed="$(seed_path "$arch")" 314 [ -f "$seed" ] || seed_arch "$arch" 315 QEMU_ARGS=("${QEMU_ARGS[@]}" 316 -drive "if=virtio,format=raw,media=cdrom,readonly=on,file=$seed") 317 fi 318 } 319 320 # Machine + firmware + accel selection, shared by run and provision. Appends to 321 # an already-initialized QEMU_ARGS. 322 append_machine_args() { 323 local arch="$1" accel_args 324 case "$arch" in 325 amd64) 326 # Drop the default q35 VGA so FreeBSD selects the serial console as 327 # primary. Otherwise it picks the (headless) VGA as primary console and 328 # all userland/cloud-init output plus the login getty go to a head we 329 # never display, making a slow TCG boot indistinguishable from a hang. 330 # 331 # QEMU's default x86_64 TCG model is "qemu64" (~x86-64-v1, no 332 # POPCNT/SSE4.2/BMI). kit targets a modern x86_64 baseline and emits e.g. 333 # `popcnt` for @popcount, which #UDs (SIGILL -> exit 132) on qemu64. Add the 334 # missing baseline-ish features explicitly: "-cpu max" enables enough that 335 # the FreeBSD/EDK2 boot fails under TCG, so extend qemu64 minimally instead. 336 QEMU_ARGS=("${QEMU_ARGS[@]}" -machine q35 -vga none \ 337 -cpu "qemu64,+popcnt,+sse4.1,+sse4.2") 338 append_firmware_args "$arch" 339 ;; 340 aarch64) 341 QEMU_ARGS=("${QEMU_ARGS[@]}" -machine virt) 342 accel_args="$(qemu_accel_args "$arch" | tr '\n' ' ')" 343 if [ -n "$accel_args" ]; then 344 # shellcheck disable=SC2206 345 QEMU_ARGS=("${QEMU_ARGS[@]}" $accel_args -cpu host) 346 else 347 QEMU_ARGS=("${QEMU_ARGS[@]}" -cpu cortex-a72) 348 fi 349 append_firmware_args "$arch" 350 ;; 351 riscv64) 352 # acpi=off forces the edk2-riscv firmware to hand FreeBSD a flattened 353 # device tree (FDT) instead of ACPI tables. FreeBSD/riscv64 is FDT-only; 354 # with ACPI advertised, its loader finds "no valid device tree blob" and 355 # the kernel boots blind (no console/disk). 356 QEMU_ARGS=("${QEMU_ARGS[@]}" -machine virt,acpi=off -cpu rv64) 357 append_firmware_args "$arch" 358 ;; 359 esac 360 } 361 362 # A "golden" disk is one whose first boot is already done: the login user and 363 # sshd are provisioned and the firstboot services have run. The build/ marker 364 # records that the working disk is golden; the DL_ROOT copy is the durable 365 # cache that survives `make clean` (which wipes build/). 366 provisioned_marker() { 367 echo "$VM_ROOT/images/freebsd-$(canon_arch "$1").provisioned" 368 } 369 370 golden_cache_path() { 371 echo "$DL_ROOT/freebsd-$(canon_arch "$1")-golden.qcow2" 372 } 373 374 # Save the (clean, provisioned) working disk to the durable golden cache, 375 # compressed so the cache stays small. 376 cache_golden() { 377 local arch="$1" disk golden 378 arch="$(canon_arch "$arch")" 379 disk="$(disk_path "$arch")" 380 golden="$(golden_cache_path "$arch")" 381 mkdir -p "$DL_ROOT" 382 printf 'cache golden disk: %s -> %s\n' "$disk" "$golden" 383 qemu-img convert -O qcow2 -c "$disk" "$golden.tmp" 384 mv "$golden.tmp" "$golden" 385 } 386 387 # Boot a freshly-expanded cloud-init disk once, offline, so nuageinit creates 388 # the login user and starts sshd while firstboot_freebsd_update fails fast for 389 # lack of a route to the update servers (its slow networked update would 390 # otherwise run on the first boot and force a reboot -- painful under TCG). 391 # Once the guest reaches its login prompt the disk is golden. 392 provision_nuageinit() { 393 local arch="$1" disk port qemu log qpid t0 w 394 arch="$(canon_arch "$arch")" 395 disk="$(disk_path "$arch")" 396 port="$(ssh_port "$arch")" 397 qemu="$(qemu_bin "$arch")" 398 command -v "$qemu" >/dev/null 2>&1 || die "$qemu not found on PATH" 399 [ -f "$disk" ] || die "no disk to provision: $disk (run prepare first)" 400 [ -f "$(seed_path "$arch")" ] || seed_arch "$arch" 401 ensure_ssh_key 402 log="$VM_ROOT/images/freebsd-$arch.provision.log" 403 rm -f "$log" 404 QEMU_ARGS=() 405 append_machine_args "$arch" 406 append_common_args "$arch" "$disk" "$port" restrict "file:$log" 407 append_seed_args "$arch" 408 printf 'provision: booting %s offline to create user+sshd (log: %s)\n' "$arch" "$log" 409 "$qemu" "${QEMU_ARGS[@]}" >/dev/null 2>&1 & 410 qpid=$! 411 t0=$(date +%s) 412 while kill -0 "$qpid" 2>/dev/null; do 413 grep -q "login:" "$log" 2>/dev/null && break 414 if [ $(( $(date +%s) - t0 )) -gt "${KIT_FREEBSD_PROVISION_TIMEOUT:-1800}" ]; then 415 kill "$qpid" 2>/dev/null; wait "$qpid" 2>/dev/null || true 416 die "provision timed out for $arch after ${KIT_FREEBSD_PROVISION_TIMEOUT:-1800}s (see $log)" 417 fi 418 sleep 3 419 done 420 kill -0 "$qpid" 2>/dev/null || die "provision VM for $arch exited before login (see $log)" 421 printf 'provision: %s reached login; shutting down cleanly\n' "$arch" 422 # sshd's banner is delayed ~30s under restrict (it reverse-DNS-resolves the 423 # client with no route out), so allow a generous connect timeout. A forced 424 # stop would also recover (UFS+SUJ journals), but a clean halt avoids an fsck. 425 sleep 5 426 timeout 150 ssh -i "$SSH_KEY" -p "$port" \ 427 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ 428 -o LogLevel=ERROR -o ConnectTimeout=90 \ 429 "$SSH_USER@127.0.0.1" 'sync; shutdown -p now' >/dev/null 2>&1 || true 430 w=0 431 while kill -0 "$qpid" 2>/dev/null && [ "$w" -lt 90 ]; do sleep 1; w=$((w+1)); done 432 if kill -0 "$qpid" 2>/dev/null; then 433 kill "$qpid" 2>/dev/null; wait "$qpid" 2>/dev/null || true 434 fi 435 } 436 437 # Write the expect program that drives a first-boot setup over the serial 438 # console. Kept quoted (no shell expansion); it reads user/pubkey/timeout from 439 # the environment so the pubkey's `/ + =` never collide with shell quoting. 440 write_serial_provision_expect() { 441 cat > "$1" <<'EXP' 442 set timeout $env(KIT_PROV_TIMEOUT) 443 set user $env(KIT_PROV_USER) 444 set pubkey $env(KIT_PROV_PUBKEY) 445 spawn {*}$argv 446 set qpid [exp_pid] 447 proc bail {msg code} { 448 puts "\nprovision-serial: $msg" 449 catch { exec kill -9 $::qpid } 450 exit $code 451 } 452 expect { 453 timeout { bail "timed out waiting for login prompt" 2 } 454 "login: " 455 } 456 send "root\r" 457 expect { 458 timeout { bail "timed out after sending root" 2 } 459 "Login incorrect" { bail "root login refused (image expects a password)" 3 } 460 -re {Password:} { send "\r"; exp_continue } 461 -re {[#%$] $} {} 462 } 463 # Drop to /bin/sh and set a unique prompt. The `@@K""IT@@` we type evaluates to 464 # @@KIT@@ in the prompt but the typed echo contains the quotes, so expecting the 465 # bare marker only ever matches a real prompt, never the command echo. 466 send "/bin/sh\r" 467 send "PS1=@@K\"\"IT@@\r" 468 expect { 469 timeout { bail "timed out establishing shell prompt" 2 } 470 "@@KIT@@" 471 } 472 proc step {cmd} { 473 send "$cmd\r" 474 expect { 475 timeout { bail "timed out running: $cmd" 2 } 476 "@@KIT@@" 477 } 478 } 479 step "pw useradd -n $user -m -G wheel -s /bin/sh" 480 step "mkdir -p /home/$user/.ssh" 481 step "printf '%s\n' '$pubkey' > /home/$user/.ssh/authorized_keys" 482 step "chown -R $user:$user /home/$user/.ssh" 483 step "chmod 700 /home/$user/.ssh" 484 step "chmod 600 /home/$user/.ssh/authorized_keys" 485 step "sysrc sshd_enable=YES" 486 step "sysrc ifconfig_vtnet0=DHCP" 487 step "service sshd keygen" 488 send "sync; shutdown -p now\r" 489 expect { 490 timeout { bail "powered down but qemu still running; killed" 0 } 491 eof { exit 0 } 492 } 493 EXP 494 } 495 496 # Provision an image that has no cloud-init seed (FreeBSD publishes no 497 # BASIC-CLOUDINIT riscv64 image). Drive the serial console with expect: log in 498 # as root, create the SSH user, enable sshd + DHCP, then power off. 499 provision_serial() { 500 local arch="$1" disk port qemu expf rc 501 arch="$(canon_arch "$arch")" 502 disk="$(disk_path "$arch")" 503 port="$(ssh_port "$arch")" 504 qemu="$(qemu_bin "$arch")" 505 command -v expect >/dev/null 2>&1 || 506 die "expect not found on PATH (needed to provision $arch over the serial console)" 507 command -v "$qemu" >/dev/null 2>&1 || die "$qemu not found on PATH" 508 [ -f "$disk" ] || die "no disk to provision: $disk (run prepare first)" 509 ensure_ssh_key 510 local mem="${KIT_FREEBSD_MEM:-2048}" cpus="${KIT_FREEBSD_CPUS:-2}" 511 QEMU_ARGS=() 512 append_machine_args "$arch" 513 QEMU_ARGS=("${QEMU_ARGS[@]}" 514 -m "$mem" 515 -smp "$cpus" 516 -drive "if=virtio,format=qcow2,file=$disk" 517 -netdev "user,id=net0,hostfwd=tcp:127.0.0.1:$port-:22" 518 -device "virtio-net-pci,netdev=net0" 519 -device "virtio-rng-pci" 520 -display none 521 -serial stdio 522 -monitor none) 523 expf="$VM_ROOT/images/freebsd-$arch.provision.exp" 524 write_serial_provision_expect "$expf" 525 printf 'provision: booting %s to configure sshd over the serial console (slow under TCG)...\n' "$arch" 526 KIT_PROV_USER="$SSH_USER" \ 527 KIT_PROV_PUBKEY="$(cat "$SSH_KEY.pub")" \ 528 KIT_PROV_TIMEOUT="${KIT_FREEBSD_PROVISION_TIMEOUT:-1800}" \ 529 expect "$expf" "$qemu" "${QEMU_ARGS[@]}" 530 rc=$? 531 rm -f "$expf" 532 [ "$rc" -eq 0 ] || die "serial provisioning failed for $arch (expect rc=$rc)" 533 } 534 535 # Provision a freshly-expanded disk into a golden one (nuageinit for cloud-init 536 # images, serial-console otherwise), then mark + cache it. 537 provision_arch() { 538 local arch="$1" 539 arch="$(canon_arch "$arch")" 540 if cloudinit_supported "$arch"; then 541 provision_nuageinit "$arch" 542 else 543 provision_serial "$arch" 544 fi 545 touch "$(provisioned_marker "$arch")" 546 cache_golden "$arch" 547 printf 'provisioned: %s\n' "$(disk_path "$arch")" 548 } 549 550 # Make build/ hold a provisioned (golden) working disk for arch, preferring the 551 # cheapest source: an already-provisioned working disk, then the durable golden 552 # cache, then a from-scratch expand+provision. 553 ensure_disk() { 554 local arch="$1" disk marker golden src 555 arch="$(canon_arch "$arch")" 556 disk="$(disk_path "$arch")" 557 marker="$(provisioned_marker "$arch")" 558 golden="$(golden_cache_path "$arch")" 559 mkdir -p "$VM_ROOT/images" 560 [ -f "$disk" ] && [ -f "$marker" ] && return 0 561 if [ -f "$golden" ]; then 562 printf 'restore golden disk: %s -> %s\n' "$golden" "$disk" 563 qemu-img convert -O qcow2 "$golden" "$disk.tmp" 564 mv "$disk.tmp" "$disk" 565 touch "$marker" 566 return 0 567 fi 568 fetch_arch "$arch" 569 src="$(download_path "$arch")" 570 printf 'expand image: %s -> %s\n' "$src" "$disk" 571 xz -dc "$src" > "$disk.tmp" 572 mv "$disk.tmp" "$disk" 573 rm -f "$marker" 574 provision_arch "$arch" 575 } 576 577 run_arch() { 578 local arch="$1" disk port qemu 579 shift 580 arch="$(canon_arch "$arch")" 581 disk="$(disk_path "$arch")" 582 port="$(ssh_port "$arch")" 583 qemu="$(qemu_bin "$arch")" 584 command -v "$qemu" >/dev/null 2>&1 || die "$qemu not found on PATH" 585 ensure_disk "$arch" 586 QEMU_ARGS=() 587 append_machine_args "$arch" 588 append_common_args "$arch" "$disk" "$port" 589 append_seed_args "$arch" 590 printf 'ssh forward: 127.0.0.1:%s -> %s:22\n' "$port" "$arch" 591 printf 'run: %s\n' "$qemu" 592 exec "$qemu" "${QEMU_ARGS[@]}" "$@" 593 } 594 595 ssh_args() { 596 local arch="$1" port 597 port="$(ssh_port "$arch")" 598 printf '%s\n' -i "$SSH_KEY" \ 599 -p "$port" \ 600 -o StrictHostKeyChecking=no \ 601 -o UserKnownHostsFile=/dev/null \ 602 -o LogLevel=ERROR \ 603 -o ConnectTimeout=5 604 } 605 606 wait_ssh() { 607 local arch="$1" port args 608 arch="$(canon_arch "$arch")" 609 port="$(ssh_port "$arch")" 610 if [ ! -f "$(provisioned_marker "$arch")" ] && [ ! -f "$(golden_cache_path "$arch")" ]; then 611 die "$arch is not provisioned yet; run 'prepare $arch' or 'provision $arch' first" 612 fi 613 [ -f "$SSH_KEY" ] || die "missing SSH key: $SSH_KEY" 614 printf 'waiting for ssh on 127.0.0.1:%s as %s\n' "$port" "$SSH_USER" 615 # shellcheck disable=SC2207 616 args=($(ssh_args "$arch")) 617 for _ in $(jot 120 1 120 2>/dev/null || seq 1 120); do 618 if ssh "${args[@]}" "$SSH_USER@127.0.0.1" 'uname -a' 2>/dev/null; then 619 return 0 620 fi 621 sleep 2 622 done 623 die "ssh did not become ready for $arch" 624 } 625 626 ssh_arch() { 627 local arch="$1" args 628 shift 629 arch="$(canon_arch "$arch")" 630 # shellcheck disable=SC2207 631 args=($(ssh_args "$arch")) 632 exec ssh "${args[@]}" "$SSH_USER@127.0.0.1" "$@" 633 } 634 635 scp_arch() { 636 local arch="$1" src="$2" dst="$3" port args 637 arch="$(canon_arch "$arch")" 638 port="$(ssh_port "$arch")" 639 # shellcheck disable=SC2207 640 args=($(ssh_args "$arch")) 641 exec scp -P "$port" -i "$SSH_KEY" \ 642 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \ 643 "$src" "$SSH_USER@127.0.0.1:$dst" 644 } 645 646 # run_batch ARCH STAGEDIR RESULTSDIR 647 # Run a whole staging dir in a single SSH session and bring per-binary results 648 # back. Pure transport: tar the stagedir in on stdin, the guest extracts it and 649 # runs its run-remote.sh entry script (authored by the caller; it writes 650 # res/<id>.{rc,out,err} per binary), then tars res/ back on stdout, which we 651 # extract flat into RESULTSDIR. One VM round-trip per arch. The VM must already 652 # be reachable (the caller boots it and waits for SSH). 653 run_batch_arch() { 654 local arch="$1" stagedir="$2" resultsdir="$3" args 655 arch="$(canon_arch "$arch")" 656 [ -d "$stagedir" ] || die "run-batch: no such staging dir: $stagedir" 657 [ -f "$stagedir/run-remote.sh" ] || die "run-batch: missing $stagedir/run-remote.sh" 658 [ -n "$resultsdir" ] || die "run-batch: results dir required" 659 command -v tar >/dev/null 2>&1 || die "run-batch: tar not found on host" 660 mkdir -p "$resultsdir" 661 # shellcheck disable=SC2207 662 args=($(ssh_args "$arch")) 663 tar -C "$stagedir" -cf - . | ssh "${args[@]}" "$SSH_USER@127.0.0.1" \ 664 'd=$(mktemp -d /tmp/kit-vm.XXXXXX) || exit 1; tar -C "$d" -xf - || exit 1; ( cd "$d" && sh run-remote.sh ) </dev/null >/dev/null 2>&1; tar -C "$d/res" -cf - . 2>/dev/null; rm -rf "$d"' \ 665 | tar -C "$resultsdir" -xf - 2>/dev/null || true 666 } 667 668 doctor() { 669 printf 'host: %s/%s\n' "$(uname -s 2>/dev/null)" "$(uname -m 2>/dev/null)" 670 printf 'vm root: %s\n' "$VM_ROOT" 671 printf 'download cache: %s\n' "$DL_ROOT" 672 for tool in curl shasum xz hdiutil ssh-keygen ssh qemu-img \ 673 qemu-system-x86_64 qemu-system-aarch64 qemu-system-riscv64; do 674 if command -v "$tool" >/dev/null 2>&1; then 675 printf ' OK %s (%s)\n' "$tool" "$(command -v "$tool")" 676 else 677 printf ' MISSING %s\n' "$tool" 678 fi 679 done 680 printf 'ssh key: %s\n' "$([ -f "$SSH_KEY" ] && echo "$SSH_KEY" || echo "$SSH_KEY (absent)")" 681 for arch in amd64 aarch64 riscv64; do 682 printf ' %-7s cloudinit=%s ssh_port=%s golden=%s\n' \ 683 "$arch" \ 684 "$(cloudinit_supported "$arch" && echo yes || echo no)" \ 685 "$(ssh_port "$arch")" \ 686 "$([ -f "$(golden_cache_path "$arch")" ] && echo cached || echo none)" 687 done 688 } 689 690 cmd="${1:-}" 691 case "$cmd" in 692 doctor) doctor ;; 693 fetch) [ $# -eq 2 ] || { usage; exit 2; }; fetch_arch "$2" ;; 694 prepare) [ $# -eq 2 ] || { usage; exit 2; }; prepare_arch "$2" ;; 695 seed) [ $# -eq 2 ] || { usage; exit 2; }; seed_arch "$2" ;; 696 provision) [ $# -eq 2 ] || { usage; exit 2; }; provision_arch "$2" ;; 697 run) [ $# -ge 2 ] || { usage; exit 2; }; arch="$2"; shift 2; run_arch "$arch" "$@" ;; 698 wait-ssh) [ $# -eq 2 ] || { usage; exit 2; }; wait_ssh "$2" ;; 699 ssh) [ $# -ge 2 ] || { usage; exit 2; }; arch="$2"; shift 2; ssh_arch "$arch" "$@" ;; 700 scp) [ $# -eq 4 ] || { usage; exit 2; }; scp_arch "$2" "$3" "$4" ;; 701 run-batch) [ $# -eq 4 ] || { usage; exit 2; }; run_batch_arch "$2" "$3" "$4" ;; 702 -h|--help|help|"") usage ;; 703 *) usage; exit 2 ;; 704 esac