#!/bin/sh set -eu repo_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289 metadata_target=${METADATA_OUT:-} root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub} root_ssh_private_key_file=${ROOT_SSH_PRIVATE_KEY_FILE:-$HOME/.ssh/id_ed25519} requested_disk_capacity=${DISK_CAPACITY:-} cleanup=0 if [ -n "${WORKDIR:-}" ]; then workdir=$WORKDIR mkdir -p "$workdir" else workdir=$(mktemp -d /tmp/fruix-phase9-xcpng.XXXXXX) cleanup=1 fi if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then cleanup=0 fi phase9_os_template=$repo_root/tests/system/phase9-minimal-operating-system.scm.in phase9_os_file=$workdir/phase9-minimal-operating-system.scm phase8_log=$workdir/phase8-system-image.log phase8_metadata=$workdir/phase8-system-image-metadata.txt arp_scan_log=$workdir/arp-scan.log ssh_stdout=$workdir/ssh.out ssh_stderr=$workdir/ssh.err metadata_file=$workdir/phase9-xcpng-boot-metadata.txt vdi_info_json=$workdir/vdi-info.json vm_info_json=$workdir/vm-info.json upload_image=$workdir/disk.vhd cleanup_workdir() { if [ "$cleanup" -eq 1 ]; then rm -rf "$workdir" fi } trap cleanup_workdir EXIT INT TERM [ -f "$root_authorized_key_file" ] || { echo "missing root authorized key file: $root_authorized_key_file" >&2 exit 1 } [ -f "$root_ssh_private_key_file" ] || { echo "missing root SSH private key file: $root_ssh_private_key_file" >&2 exit 1 } root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") # Discover the existing target VDI attached as disk 0 for the operator-provided VM. xo-cli list-objects id=$vm_id >"$vm_info_json" vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="0") | .VDI' | head -n 1) [ -n "$vdi_id" ] || { echo "failed to discover target VDI for VM $vm_id" >&2; exit 1; } xo-cli list-objects type=VDI | jq '[.[] | select(.id=="'$vdi_id'")]' >"$vdi_info_json" vdi_size=$(jq -r '.[0].size' "$vdi_info_json") [ -n "$vdi_size" ] || { echo "failed to discover VDI size for $vdi_id" >&2; exit 1; } if [ -n "$requested_disk_capacity" ] && [ "$requested_disk_capacity" != "$vdi_size" ]; then echo "existing XCP-ng import path requires an image that matches the target VDI size; use DISK_CAPACITY=$vdi_size or leave it unset" >&2 exit 1 fi disk_capacity=$vdi_size requested_disk_bytes=$vdi_size sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$phase9_os_template" > "$phase9_os_file" KEEP_WORKDIR=1 WORKDIR=$workdir/phase8-build OS_FILE=$phase9_os_file DISK_CAPACITY=$disk_capacity \ METADATA_OUT=$phase8_metadata "$repo_root/tests/system/run-phase8-system-image.sh" \ >"$phase8_log" 2>&1 disk_image=$(sed -n 's/^disk_image=//p' "$phase8_metadata") closure_path=$(sed -n 's/^closure_path=//p' "$phase8_metadata") closure_base=$(basename "$closure_path") raw_sha256=$(sed -n 's/^raw_sha256=//p' "$phase8_metadata") image_store_path=$(sed -n 's/^image_store_path=//p' "$phase8_metadata") guile_store=$(grep 'fruix-guile-runtime-3.0$' "$closure_path/.references" | head -n 1) guile_extra_store=$(grep 'fruix-guile-extra-3.0$' "$closure_path/.references" | head -n 1) shepherd_store=$(grep 'fruix-shepherd-runtime-1.0.9$' "$closure_path/.references" | head -n 1) command -v qemu-img >/dev/null 2>&1 || { echo "qemu-img is required to convert the raw Fruix image to XCP-ng-compatible VHD" >&2 exit 1 } qemu-img convert -f raw -O vpc -o subformat=dynamic,force_size=on "$disk_image" "$upload_image" upload_sha256=$(sha256 -q "$upload_image") upload_size_bytes=$(stat -f '%z' "$upload_image") xo-cli vm.stop id=$vm_id force=true >/dev/null 2>&1 || true xo-cli disk.importContent id=$vdi_id @=$upload_image >"$workdir/disk-import.out" xo-cli vm.setBootOrder vm=$vm_id order=dcn >"$workdir/set-boot-order.out" xo-cli vm.start id=$vm_id >"$workdir/vm-start.out" # Wait for the VM to obtain an address and accept SSH using the injected key. vm_mac=$(jq -r '.[0].VIFs[0]' "$vm_info_json") if [ -n "$vm_mac" ] && [ "$vm_mac" != null ]; then vm_mac=$(xo-cli list-objects type=VIF | jq -r '.[] | select(.id=="'$vm_mac'") | .MAC' | tr 'A-Z' 'a-z') else vm_mac= fi host_interface=$(route -n get default | awk '/interface:/{print $2; exit}') host_ip=$(ifconfig "$host_interface" | awk '/inet /{print $2; exit}') subnet_prefix=${host_ip%.*} ssh_guest() { ssh -i "$root_ssh_private_key_file" \ -o BatchMode=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=5 \ root@"$guest_ip" "$@" } guest_ip= for attempt in $(jot 90 1 90); do : >"$arp_scan_log" for host in $(jot 254 1 254); do ip=$subnet_prefix.$host ( ping -c 1 -W 1000 "$ip" >/dev/null 2>&1 && echo "$ip" >>"$arp_scan_log" ) & done wait if [ -n "$vm_mac" ]; then guest_ip=$(arp -an | awk -v mac="$vm_mac" 'tolower($4)==mac {gsub(/[()]/,"",$2); print $2; exit}') fi if [ -n "$guest_ip" ]; then if ssh -i "$root_ssh_private_key_file" \ -o BatchMode=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=3 \ root@"$guest_ip" 'test -f /var/lib/fruix/ready' >"$ssh_stdout" 2>"$ssh_stderr"; then break fi fi sleep 5 done [ -n "$guest_ip" ] || { echo "guest IP was not discovered; manual console inspection is likely required" >&2 exit 1 } ready_marker=$(ssh_guest 'cat /var/lib/fruix/ready') run_current_system_target=$(ssh_guest 'readlink /run/current-system') rc_conf_hostname=$(ssh_guest 'grep "^hostname=" /etc/rc.conf | cut -d"\"" -f2') shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped') logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ') shepherd_bootstrap_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd-bootstrap.out 2>/dev/null || true" | tr '\n' ' ') shepherd_log_tail=$(ssh_guest "awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }' /var/log/shepherd.log 2>/dev/null || true" | tr '\n' ' ') guest_dmesg_tail=$(ssh_guest "dmesg | awk 'BEGIN { c = 20 } { lines[NR % c] = \$0 } END { start = (NR > c ? NR - c + 1 : 1); for (i = start; i <= NR; i++) print lines[i % c] }'" | tr '\n' ' ') sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') uname_output=$(ssh_guest 'uname -sr') operator_home_listing=$(ssh_guest 'ls -d /home/operator') compat_prefix_shims=$(ssh_guest 'for p in /tmp/guile-freebsd-validate-install /tmp/guile-gnutls-freebsd-validate-install /tmp/shepherd-freebsd-validate-install; do if [ -e "$p" ] || [ -L "$p" ]; then echo present; exit 0; fi; done; echo absent') guile_module_smoke=$(ssh_guest "env LANG='C.UTF-8' LC_ALL='C.UTF-8' LD_LIBRARY_PATH='$guile_extra_store/lib:$guile_store/lib:/usr/local/lib' GUILE_SYSTEM_PATH='$guile_store/share/guile/3.0:$guile_store/share/guile/site/3.0:$guile_store/share/guile/site:$guile_store/share/guile' GUILE_LOAD_PATH='$shepherd_store/share/guile/site/3.0:$guile_extra_store/share/guile/site/3.0' GUILE_SYSTEM_COMPILED_PATH='$guile_store/lib/guile/3.0/ccache:$guile_store/lib/guile/3.0/site-ccache' GUILE_LOAD_COMPILED_PATH='$shepherd_store/lib/guile/3.0/site-ccache:$guile_extra_store/lib/guile/3.0/site-ccache' GUILE_SYSTEM_EXTENSIONS_PATH='$guile_store/lib/guile/3.0/extensions' GUILE_EXTENSIONS_PATH='$guile_extra_store/lib/guile/3.0/extensions' '$guile_store/bin/guile' --no-auto-compile -c '(use-modules (fibers config) (gnutls) (shepherd config)) (display \"ok\") (newline)'") activate_preview=$(ssh_guest 'head -n 5 /run/current-system/activate' | tr '\n' ' ') activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ') login_conf_kind=$(ssh_guest 'if [ -L /etc/login.conf ]; then echo symlink; elif [ -f /etc/login.conf ]; then echo regular; else echo missing; fi') login_conf_db=$(ssh_guest 'test -f /etc/login.conf.db && echo present || echo missing') pwd_dbs=$(ssh_guest 'if [ -f /etc/pwd.db ] && [ -f /etc/spwd.db ]; then echo present; else echo missing; fi') [ "$ready_marker" = ready ] || { echo "unexpected ready marker contents: $ready_marker" >&2; exit 1; } [ "$shepherd_status" = running ] || { echo "fruix_shepherd is not running" >&2; exit 1; } [ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; } [ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims are still present in /tmp" >&2; exit 1; } [ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed: $guile_module_smoke" >&2; exit 1; } [ "$login_conf_kind" = regular ] || { echo "/etc/login.conf is not a regular file in guest: $login_conf_kind" >&2; exit 1; } [ "$login_conf_db" = present ] || { echo "/etc/login.conf.db is missing in guest" >&2; exit 1; } [ "$pwd_dbs" = present ] || { echo "pwd.db/spwd.db are missing in guest" >&2; exit 1; } case "$activate_log" in *fruix-activate:done*) : ;; *) echo "activation log does not show successful completion: $activate_log" >&2; exit 1 ;; esac [ "$run_current_system_target" = "/frx/store/$closure_base" ] || { echo "unexpected /run/current-system target in guest: $run_current_system_target" >&2 exit 1 } [ "$rc_conf_hostname" = fruix-freebsd ] || { echo "unexpected guest hostname config: $rc_conf_hostname" >&2; exit 1; } [ "$operator_home_listing" = /home/operator ] || { echo "operator home missing" >&2; exit 1; } cat >"$metadata_file" <} requested_disk_bytes=$requested_disk_bytes phase9_os_file=$phase9_os_file phase8_log=$phase8_log phase8_metadata=$phase8_metadata image_store_path=$image_store_path disk_image=$disk_image upload_image=$upload_image upload_format=vhd-dynamic upload_sha256=$upload_sha256 upload_size_bytes=$upload_size_bytes closure_path=$closure_path closure_base=$closure_base raw_sha256=$raw_sha256 guest_ip=$guest_ip vm_mac=$vm_mac ready_marker=$ready_marker run_current_system_target=$run_current_system_target shepherd_status=$shepherd_status sshd_status=$sshd_status logger_log=$logger_log shepherd_bootstrap_tail=$shepherd_bootstrap_tail shepherd_log_tail=$shepherd_log_tail guest_dmesg_tail=$guest_dmesg_tail uname_output=$uname_output operator_home_listing=$operator_home_listing compat_prefix_shims=$compat_prefix_shims guile_module_smoke=$guile_module_smoke activate_preview=$activate_preview activate_log=$activate_log login_conf_kind=$login_conf_kind login_conf_db=$login_conf_db pwd_dbs=$pwd_dbs boot_backend=xcp-ng-xo-cli operator_access=ssh-root-key root_authorized_key_file=$root_authorized_key_file root_ssh_private_key_file=$root_ssh_private_key_file EOF if [ -n "$metadata_target" ]; then mkdir -p "$(dirname "$metadata_target")" cp "$metadata_file" "$metadata_target" fi printf 'PASS phase9-xcpng-boot\n' printf 'Work directory: %s\n' "$workdir" printf 'Metadata file: %s\n' "$metadata_file" if [ -n "$metadata_target" ]; then printf 'Copied metadata to: %s\n' "$metadata_target" fi printf '%s\n' '--- metadata ---' cat "$metadata_file"