Files
fruix/tests/system/run-phase18-installer-iso-xcpng.sh

456 lines
19 KiB
Bash
Executable File

#!/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" <<EOF
workdir=$workdir
vm_id=$vm_id
primary_vdi_id=$vdi_id
secondary_vdi_id=$secondary_vdi_id
vdi_size=$vdi_size
iso_sr_id=$iso_sr_id
imported_iso_id=$imported_iso_id
imported_iso_name=$imported_iso_name
guest_ip=$guest_ip
target_os_file=$target_os_file
installer_iso_store_path=$iso_store_path
installer_iso_image=$installer_iso_image
installer_boot_efi_image=$installer_boot_efi_image
installer_root_image=$installer_root_image
install_target_device=$install_target_device
installer_root_size=$root_size_out
iso_volume_label=$iso_volume_label
freebsd_source_kind=$freebsd_source_kind_out
freebsd_source_ref=$freebsd_source_ref_out
freebsd_source_commit=$freebsd_source_commit_out
freebsd_source_file=$freebsd_source_file
freebsd_source_materializations_file=$freebsd_source_materializations_file
materialized_source_store_count=$materialized_source_store_count
materialized_source_store=$materialized_source_stores
installer_closure_path=$installer_closure_path
target_closure_path=$target_closure_path
native_base_store_count=$native_base_store_count
native_base_stores=$native_base_stores
store_item_count=$store_item_count
target_store_item_count=$target_store_item_count
installer_store_item_count=$installer_store_item_count
installer_state_path=$installer_state_path
installer_log_path=$installer_log_path
installer_state=$installer_state
installer_run_current_system=$installer_run_current_system
installer_sshd_status=$installer_sshd_status
installer_target_device=$installer_target_device
installer_kern_disks=$installer_kern_disks
installer_log_file=$installer_log_file
installer_gpart_file=$installer_gpart_file
installer_kern_disks_file=$installer_kern_disks_file
target_esp_fstype=$installer_esp_fstype
target_root_fstype=$installer_root_fstype
target_run_current_system=$target_run_current_system
target_shepherd_status=$target_shepherd_status
target_sshd_status=$target_sshd_status
target_install_metadata_file=$target_install_metadata_file
boot_backend=xcp-ng-xo-cli
installer_iso_boot=ok
installer_iso_install=ok
installed_target_boot=ok
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase18-installer-iso-xcpng\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"