#!/bin/sh set -eu repo_root=${PROJECT_ROOT:-$(pwd)} script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd) fruix_cmd=$repo_root/bin/fruix vm_id=${VM_ID:-90490f2e-e8fc-4b7a-388e-5c26f0157289} iso_sr_id=${ISO_SR_ID:-537a6219-8452-7cb5-8d56-5eed6910c7a2} os_template=${OS_TEMPLATE:-$script_dir/phase18-installer-target-operating-system.scm.in} system_name=${SYSTEM_NAME:-phase18-target-operating-system} store_dir=${STORE_DIR:-/frx/store} install_target_device=${INSTALL_TARGET_DEVICE:-/dev/ada0} installer_root_size=${INSTALLER_ROOT_SIZE:-} base_name=${BASE_NAME:-phase18-installer-iso-target} base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-installer-iso-target} base_release=${BASE_RELEASE:-15.0-STABLE} base_branch=${BASE_BRANCH:-stable/15} source_name=${SOURCE_NAME:-stable15-installer-iso-target-source} source_ref=${SOURCE_REF:-stable/15} source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89} declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-installer-iso-target} 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} iso_http_port=${ISO_HTTP_PORT:-$(jot -r 1 18080 18999)} keep_imported_iso=${KEEP_IMPORTED_ISO:-0} [ -x "$fruix_cmd" ] || { echo "fruix command is not executable: $fruix_cmd" >&2 exit 1 } [ -f "$os_template" ] || { echo "missing operating-system template: $os_template" >&2 exit 1 } [ -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 } command -v node >/dev/null 2>&1 || { echo "node is required to serve the ISO for XO import" >&2 exit 1 } command -v xo-cli >/dev/null 2>&1 || { echo "xo-cli is required" >&2 exit 1 } command -v jq >/dev/null 2>&1 || { echo "jq is required" >&2 exit 1 } cleanup=0 if [ -n "${WORKDIR:-}" ]; then workdir=$WORKDIR mkdir -p "$workdir" else workdir=$(mktemp -d /tmp/fruix-phase18-installer-iso-xcpng.XXXXXX) cleanup=1 fi if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then cleanup=0 fi target_os_file=$workdir/phase18-installer-iso-target-operating-system.scm installer_out=$workdir/installer-iso.txt metadata_file=$workdir/phase18-installer-iso-xcpng-metadata.txt vm_info_json=$workdir/vm-info.json vdi_info_json=$workdir/vdi-info.json installer_log_file=$workdir/installer.log installer_gpart_file=$workdir/installer-gpart.txt installer_kern_disks_file=$workdir/installer-kern-disks.txt target_install_metadata_file=$workdir/target-install.scm server_script=$workdir/serve-iso.mjs server_log=$workdir/serve-iso.log arp_scan_log=$workdir/arp-scan.log server_pid= imported_iso_id= imported_iso_name= guest_ip= vm_mac= cleanup_workdir() { if [ -n "$server_pid" ]; then kill "$server_pid" >/dev/null 2>&1 || true fi xo-cli vm.ejectCd id=$vm_id >/dev/null 2>&1 || true if [ -n "$imported_iso_id" ] && [ "$keep_imported_iso" -ne 1 ]; then xo-cli vdi.delete id=$imported_iso_id >/dev/null 2>&1 || true fi if [ "$cleanup" -eq 1 ]; then rm -rf "$workdir" fi } trap cleanup_workdir EXIT INT TERM root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") sed \ -e "s|__BASE_NAME__|$base_name|g" \ -e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \ -e "s|__BASE_RELEASE__|$base_release|g" \ -e "s|__BASE_BRANCH__|$base_branch|g" \ -e "s|__SOURCE_NAME__|$source_name|g" \ -e "s|__SOURCE_REF__|$source_ref|g" \ -e "s|__SOURCE_COMMIT__|$source_commit|g" \ -e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|g" \ -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ "$os_template" > "$target_os_file" action_env() { sudo env \ HOME="$HOME" \ GUILE_AUTO_COMPILE=0 \ FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \ GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \ GUILE_BIN="${GUILE_BIN:-/tmp/guile-freebsd-validate-install/bin/guile}" \ GUILE_EXTRA_PREFIX="${GUILE_EXTRA_PREFIX:-/tmp/guile-gnutls-freebsd-validate-install}" \ SHEPHERD_PREFIX="${SHEPHERD_PREFIX:-/tmp/shepherd-freebsd-validate-install}" \ "$@" } if [ -n "$installer_root_size" ]; then action_env "$fruix_cmd" system installer-iso "$target_os_file" \ --system "$system_name" \ --store "$store_dir" \ --install-target-device "$install_target_device" \ --root-size "$installer_root_size" >"$installer_out" else action_env "$fruix_cmd" system installer-iso "$target_os_file" \ --system "$system_name" \ --store "$store_dir" \ --install-target-device "$install_target_device" >"$installer_out" fi field() { sed -n "s/^$1=//p" "$installer_out" | tail -n 1 } iso_store_path=$(field iso_store_path) installer_iso_image=$(field iso_image) installer_boot_efi_image=$(field boot_efi_image) installer_root_image=$(field root_image) installer_closure_path=$(field installer_closure_path) target_closure_path=$(field target_closure_path) installer_host_name=$(field installer_host_name) install_target_device_out=$(field install_target_device) installer_state_path=$(field installer_state_path) installer_log_path=$(field installer_log_path) iso_volume_label=$(field iso_volume_label) root_size_out=$(field root_size) freebsd_source_kind_out=$(field freebsd_source_kind) freebsd_source_ref_out=$(field freebsd_source_ref) freebsd_source_commit_out=$(field freebsd_source_commit) freebsd_source_file=$(field freebsd_source_file) freebsd_source_materializations_file=$(field freebsd_source_materializations_file) materialized_source_store_count=$(field materialized_source_store_count) materialized_source_stores=$(field materialized_source_stores) host_base_store_count=$(field host_base_store_count) native_base_store_count=$(field native_base_store_count) native_base_stores=$(field native_base_stores) store_item_count=$(field store_item_count) target_store_item_count=$(field target_store_item_count) installer_store_item_count=$(field installer_store_item_count) store_layout_file=$(field store_layout_file) [ -d "$iso_store_path" ] || { echo "missing installer ISO store path: $iso_store_path" >&2; exit 1; } [ -f "$installer_iso_image" ] || { echo "missing installer ISO image: $installer_iso_image" >&2; exit 1; } [ -f "$installer_boot_efi_image" ] || { echo "missing installer EFI boot image: $installer_boot_efi_image" >&2; exit 1; } [ -f "$installer_root_image" ] || { echo "missing installer root image: $installer_root_image" >&2; exit 1; } [ -n "$installer_closure_path" ] || { echo "missing installer closure path" >&2; exit 1; } [ -n "$target_closure_path" ] || { echo "missing target closure path" >&2; exit 1; } [ "$install_target_device_out" = "$install_target_device" ] || { echo "unexpected install target device: $install_target_device_out" >&2; exit 1; } [ "$installer_host_name" = fruix-freebsd-installer ] || { echo "unexpected installer host name: $installer_host_name" >&2; exit 1; } [ -n "$iso_volume_label" ] || { echo "missing ISO volume label" >&2; exit 1; } [ "$freebsd_source_kind_out" = git ] || { echo "unexpected source kind: $freebsd_source_kind_out" >&2; exit 1; } [ "$freebsd_source_ref_out" = "$source_ref" ] || { echo "unexpected source ref: $freebsd_source_ref_out" >&2; exit 1; } [ "$freebsd_source_commit_out" = "$source_commit" ] || { echo "unexpected source commit: $freebsd_source_commit_out" >&2; exit 1; } [ "$materialized_source_store_count" = 1 ] || { echo "unexpected materialized source store count: $materialized_source_store_count" >&2; exit 1; } [ "$host_base_store_count" = 0 ] || { echo "expected zero host base stores, got: $host_base_store_count" >&2; exit 1; } [ "$native_base_store_count" = 3 ] || { echo "expected three native base stores, got: $native_base_store_count" >&2; exit 1; } [ -f "$freebsd_source_file" ] || { echo "missing freebsd source file: $freebsd_source_file" >&2; exit 1; } [ -f "$freebsd_source_materializations_file" ] || { echo "missing source materializations file: $freebsd_source_materializations_file" >&2; exit 1; } [ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; } case "$materialized_source_stores" in /frx/store/*-freebsd-source-$source_name) : ;; *) echo "unexpected materialized source store path: $materialized_source_stores" >&2; exit 1 ;; esac [ "$store_item_count" -ge "$target_store_item_count" ] || { echo "combined store item count smaller than target store item count" >&2; exit 1; } [ "$installer_store_item_count" -ge 1 ] || { echo "expected installer store items" >&2; exit 1; } 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) secondary_vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="1") | .VDI' | head -n 1) [ -n "$vdi_id" ] || { echo "failed to discover primary 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; } vif_id=$(jq -r '.[0].VIFs[0]' "$vm_info_json") if [ -n "$vif_id" ] && [ "$vif_id" != null ]; then vm_mac=$(xo-cli list-objects type=VIF | jq -r '.[] | select(.id=="'$vif_id'") | .MAC' | tr 'A-Z' 'a-z') else vm_mac= fi [ -n "$vm_mac" ] || { echo "failed to discover VM MAC address" >&2; exit 1; } 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%.*} cat >"$server_script" <<'EOF' import { createServer } from 'node:http' import { createReadStream, statSync } from 'node:fs' const file = process.argv[2] const port = Number(process.argv[3]) const size = statSync(file).size createServer((req, res) => { if (req.url !== '/installer.iso') { res.statusCode = 404 res.end('not found\n') return } res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': size, 'Content-Disposition': 'attachment; filename="installer.iso"' }) createReadStream(file).pipe(res) }).listen(port, '0.0.0.0', () => { console.log(`listening ${port}`) }) EOF node "$server_script" "$installer_iso_image" "$iso_http_port" >"$server_log" 2>&1 & server_pid=$! for _ in $(jot 50 1 50); do if grep -q 'listening' "$server_log"; then break fi sleep 1 if ! kill -0 "$server_pid" >/dev/null 2>&1; then echo "temporary ISO HTTP server failed to start" >&2 cat "$server_log" >&2 || true exit 1 fi done iso_import_url="http://$host_ip:$iso_http_port/installer.iso" imported_iso_name="fruix-installer-iso-ada0-$(date +%s).iso" imported_iso_id=$(xo-cli disk.import name="$imported_iso_name" sr="$iso_sr_id" type=iso url="$iso_import_url") kill "$server_pid" >/dev/null 2>&1 || true server_pid= refresh_guest_ip() { guest_ip=$(arp -an | awk -v mac="$vm_mac" 'tolower($4)==mac {gsub(/[()]/,"",$2); print $2; exit}') } ping_sweep() { : >"$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 } ssh_guest() { ssh -i "$root_ssh_private_key_file" \ -o BatchMode=yes \ -o StrictHostKeyChecking=no \ -o UserKnownHostsFile=/dev/null \ -o LogLevel=ERROR \ -o ConnectTimeout=5 \ root@"$guest_ip" "$@" } wait_for_guest_command() { probe=$1 attempts=$2 delay=$3 guest_ip= for attempt in $(jot "$attempts" 1 "$attempts"); do refresh_guest_ip if [ -z "$guest_ip" ]; then ping_sweep refresh_guest_ip fi if [ -n "$guest_ip" ]; then if ssh_guest "$probe" >/dev/null 2>&1; then return 0 fi fi sleep "$delay" done return 1 } xo-cli vm.stop id=$vm_id force=true >/dev/null 2>&1 || true xo-cli vm.insertCd id=$vm_id cd_id=$imported_iso_id force=true >"$workdir/insert-cd.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-installer.out" wait_for_guest_command 'test -e /var/lib/fruix/installer/state' 90 5 || { echo "installer ISO guest never became reachable over SSH" >&2 exit 1 } installer_state=missing for attempt in $(jot 180 1 180); do if ssh_guest 'test -e /var/lib/fruix/installer/state' >/dev/null 2>&1; then installer_state=$(ssh_guest "cat '$installer_state_path' 2>/dev/null || echo missing") [ "$installer_state" = done ] && break fi sleep 5 done installer_run_current_system=$(ssh_guest 'readlink /run/current-system') installer_sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') installer_log=$(ssh_guest "cat '$installer_log_path' 2>/dev/null || true") installer_target_device=$(ssh_guest 'cat /var/lib/fruix/installer/target-device') installer_kern_disks=$(ssh_guest 'sysctl -n kern.disks') installer_gpart=$(ssh_guest 'gpart show ada0') installer_esp_fstype=$(ssh_guest 'fstyp /dev/ada0p1') installer_root_fstype=$(ssh_guest 'fstyp /dev/ada0p2') printf '%s\n' "$installer_log" >"$installer_log_file" printf '%s\n' "$installer_kern_disks" >"$installer_kern_disks_file" printf '%s\n' "$installer_gpart" >"$installer_gpart_file" [ "$installer_state" = done ] || { echo "installer ISO environment did not finish installation: $installer_state" >&2; exit 1; } [ "$installer_run_current_system" = "/frx/store/$(basename "$installer_closure_path")" ] || { echo "unexpected installer current-system target: $installer_run_current_system" >&2; exit 1; } [ "$installer_sshd_status" = running ] || { echo "installer sshd is not running" >&2; exit 1; } [ "$installer_target_device" = "$install_target_device" ] || { echo "unexpected installer target device in guest: $installer_target_device" >&2; exit 1; } [ "$installer_esp_fstype" = msdosfs ] || { echo "unexpected target ESP filesystem: $installer_esp_fstype" >&2; exit 1; } [ "$installer_root_fstype" = ufs ] || { echo "unexpected target root filesystem: $installer_root_fstype" >&2; exit 1; } case "$installer_log" in *fruix-installer:done*) : ;; *) echo "installer log does not show completion" >&2; exit 1 ;; esac case "$installer_kern_disks" in *cd0*ada0*) : ;; *) echo "unexpected installer kern.disks output: $installer_kern_disks" >&2; exit 1 ;; esac case "$installer_gpart" in *ada0*GPT*) : ;; *) echo "unexpected gpart output for ada0" >&2; exit 1 ;; esac xo-cli vm.ejectCd id=$vm_id >"$workdir/eject-cd.out" xo-cli vm.restart id=$vm_id force=true >"$workdir/vm-restart-target.out" wait_for_guest_command 'test -f /var/lib/fruix/ready' 120 5 || { echo "installed target never became reachable over SSH" >&2 exit 1 } target_run_current_system=$(ssh_guest 'readlink /run/current-system') target_shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped') target_sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') target_activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ') target_install_metadata=$(ssh_guest 'cat /var/lib/fruix/install.scm') printf '%s\n' "$target_install_metadata" >"$target_install_metadata_file" [ "$target_run_current_system" = "/frx/store/$(basename "$target_closure_path")" ] || { echo "unexpected booted target current-system: $target_run_current_system" >&2; exit 1; } [ "$target_shepherd_status" = running ] || { echo "fruix-shepherd is not running in booted target" >&2; exit 1; } [ "$target_sshd_status" = running ] || { echo "sshd is not running in booted target" >&2; exit 1; } case "$target_install_metadata" in *"$target_closure_path"*) : ;; *) echo "booted target metadata does not record target closure path" >&2; exit 1 ;; esac case "$target_install_metadata" in *"$materialized_source_stores"*) : ;; *) echo "booted target metadata does not record materialized source store" >&2; exit 1 ;; esac case "$target_install_metadata" in *"$install_target_device"*) : ;; *) echo "booted target metadata does not record install target device" >&2; exit 1 ;; esac case "$target_activate_log" in *fruix-activate:done*) : ;; *) echo "booted target activation log does not show success" >&2; exit 1 ;; esac cat >"$metadata_file" <