diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index e20e6ec..4083d7e 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -24,6 +24,8 @@ Fruix currently has: - `fruix system install` - a bootable Fruix-managed installer environment: - `fruix system installer` +- a bootable Fruix-managed installer ISO: + - `fruix system installer-iso` Validated boot modes still are: @@ -36,45 +38,44 @@ The validated Phase 18 installation work currently uses: ## Latest completed achievement -### 2026-04-04 — Phase 18.2 completed +### 2026-04-04 — Phase 18.3 completed -Fruix now boots a minimal installer environment and installs a target system from inside it. +Fruix now builds a bootable installer ISO, boots it, installs from it, and boots the installed target successfully. Highlights: - added in `modules/fruix/system/freebsd.scm`: - - `installer-operating-system` - - `operating-system-installer-image-spec` - - `materialize-installer-image` + - `operating-system-installer-iso-spec` + - `materialize-installer-iso` - added CLI support in `scripts/fruix.scm`: - - `fruix system installer` - - `--install-target-device DEVICE` -- the installer image now carries: - - its own installer closure + - `fruix system installer-iso` +- the installer ISO now carries: + - a UEFI El Torito boot image + - `/boot/root.img` as the installer mdroot payload + - the installer closure - the selected target closure - - the target store closure - - a staged target rootfs payload + - the target runtime store closure needed for installation - in-guest installer state/log/scripts -- validated workflow: - - boot installer image in QEMU/UEFI/TCG - - reach installer over SSH - - install target system onto second disk from inside the guest - - boot the installed target successfully +- validated workflows: + - local QEMU/UEFI/TCG boot, install, and installed-target reboot + - real XCP-ng VM boot, install, and installed-target reboot +- a platform-specific installer detail is now recorded in-tree: + - QEMU ISO path installs onto `/dev/vtbd0` + - XCP-ng ISO path installs onto `/dev/ada0` Validation: -- `PASS phase18-installer-environment` -- regression re-checks: - - `PASS phase18-system-install` - - `PASS phase17-source-revisions-qemu` +- `PASS phase18-installer-iso` +- `PASS phase18-installer-iso-xcpng` Report: -- `docs/reports/phase18-installer-environment-freebsd.md` +- `docs/reports/phase18-installer-iso-freebsd.md` -Commit: +Commits: -- `1d00907` — `Add Fruix bootable installer environment` +- `1970c5c` — `system: add UEFI installer ISO builder` +- `604ad82` — `system: validate UEFI installer ISO boot path` ## Recent major milestones diff --git a/docs/reports/phase18-installer-iso-freebsd.md b/docs/reports/phase18-installer-iso-freebsd.md index feba27e..45d3835 100644 --- a/docs/reports/phase18-installer-iso-freebsd.md +++ b/docs/reports/phase18-installer-iso-freebsd.md @@ -163,7 +163,7 @@ installer_iso_install=ok installed_target_boot=ok ``` -Notable ISO-specific validation detail: +Notable QEMU-specific ISO validation detail: - unlike the disk-image-style installer environment from Phase 18.2, the ISO boots from `cd0`, so the target virtio disk appears as: - `/dev/vtbd0` @@ -194,6 +194,75 @@ The harness verified all of the following: - `sshd` is running - activation completed successfully +### Real XCP-ng validation + +Added: + +- `tests/system/run-phase18-installer-iso-xcpng.sh` + +This harness validates the same installer-iso workflow on the approved real XCP-ng path: + +- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289` +- ISO SR: `537a6219-8452-7cb5-8d56-5eed6910c7a2` +- target VDIs: + - `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` + - `7061d761-3639-4bec-87f7-2ba1af924eaa` + +Because the current `xo-cli disk.import @=/path/to.iso` path returned an HTTP 500 error in this environment, the harness imports the ISO into the ISO SR via a temporary local HTTP URL, then inserts the resulting ISO VDI into the VM's CD drive. + +Passing validation: + +- `PASS phase18-installer-iso-xcpng` + +Validated result summary: + +```text +vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289 +iso_id= +guest_ip=192.168.213.62 +installer_state=done +installer_target_device=/dev/ada0 +kern_disks=cd0 ada1 ada0 +installer_run_current_system=/frx/store/16969e825dbb65b5c27180030d4a7d98821893460fb3dccdc863ff6156ed61e0-fruix-system-fruix-freebsd-installer +installer_sshd_status=running +target_run_current_system=/frx/store/a98d3af6a1afbc4a927d47cea6458d5a70747b051ed994e5d9ff1ae79c4f2b42-fruix-system-fruix-freebsd +target_sshd_status=running +target_shepherd_status=running +``` + +Important XCP-ng-specific details: + +- the installer ISO still boots from: + - `cd0` +- on this Xen HVM path, the primary target disk is exposed through Xen block front as `xbd0` and appears to FreeBSD as: + - `/dev/ada0` +- therefore the XCP-ng installer-iso path must target: + - `/dev/ada0` + rather than QEMU's: + - `/dev/vtbd0` +- the visible EFI console can appear to stop at: + - `console vidconsole is unavailable` + but boot still continues and the installer becomes reachable over SSH; that message was not the actual failure mode on XCP-ng + +The harness verified all of the following on the real VM path: + +1. `fruix system installer-iso` builds a bootable ISO with `--install-target-device /dev/ada0` +2. the ISO can be imported into the operator-approved ISO SR and attached to the approved VM +3. the VM boots the Fruix installer ISO successfully under UEFI +4. the installer environment becomes reachable over SSH +5. inside the installer guest: + - `kern.disks` includes `cd0` and `ada0` + - `/run/current-system` points at the installer closure + - the installer reaches `state=done` +6. the installed target on `ada0` is partitioned and formatted correctly +7. after ejecting the ISO and rebooting, the installed target boots successfully on the same XCP-ng VM +8. after target boot: + - `/run/current-system` points at the target closure + - shepherd is running + - `sshd` is running + - activation completed successfully + - `/var/lib/fruix/install.scm` still records the materialized source store provenance + ## Result Phase 18.3 is complete. @@ -202,4 +271,7 @@ Fruix now has a validated bootable UEFI installer ISO on FreeBSD that can: - boot into a Fruix-managed installer environment from ISO media - perform the non-interactive installation flow onto a target disk -- and boot the installed target successfully +- boot the installed target successfully +- and do so on both: + - local `QEMU/UEFI/TCG` + - the approved real `XCP-ng` VM path diff --git a/tests/system/run-phase18-installer-iso-xcpng.sh b/tests/system/run-phase18-installer-iso-xcpng.sh new file mode 100755 index 0000000..e26f5fe --- /dev/null +++ b/tests/system/run-phase18-installer-iso-xcpng.sh @@ -0,0 +1,455 @@ +#!/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" <