Enable Fruix FreeBSD guest SSH boot on XCP-ng

This commit is contained in:
2026-04-02 07:34:51 +02:00
parent d465264b5e
commit 4b69118d06
9 changed files with 988 additions and 83 deletions

View File

@@ -25,6 +25,9 @@
"/tmp/shepherd-freebsd-validate-install"))
(define metadata-file
(string-append workdir "/phase8-system-image-metadata.txt"))
(define disk-capacity
(let ((value (getenv "DISK_CAPACITY")))
(and value (not (string-null? value)) value)))
(define (trim-trailing-newlines str)
(let loop ((len (string-length str)))
@@ -48,16 +51,30 @@
(primitive-load os-file)
(validate-operating-system phase7-operating-system)
(let* ((image-a (materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(image-b (materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix))
(let* ((image-a (if disk-capacity
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity)
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix)))
(image-b (if disk-capacity
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix
#:disk-capacity disk-capacity)
(materialize-bhyve-image phase7-operating-system
#:store-dir store-dir
#:guile-prefix guile-prefix
#:guile-extra-prefix guile-extra-prefix
#:shepherd-prefix shepherd-prefix)))
(image-store-path (assoc-ref image-a 'image-store-path))
(image-store-path-rebuild (assoc-ref image-b 'image-store-path))
(disk-image (assoc-ref image-a 'disk-image))
@@ -85,6 +102,7 @@
(format port "esp_image=~a~%" esp-image)
(format port "root_image=~a~%" root-image)
(format port "closure_path=~a~%" closure-path)
(format port "disk_capacity=~a~%" (or disk-capacity "<default>"))
(format port "store_item_count=~a~%" (length store-items))
(format port "raw_sha256=~a~%" raw-sha256)
(format port "image_size_bytes=~a~%" image-size-bytes)

View File

@@ -46,7 +46,8 @@
#:services '(shepherd ready-marker)
#:loader-entries '(("autoboot_delay" . "1")
("console" . "comconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "YES")
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "NO"))
#:ready-marker "/var/lib/fruix/ready"))

View File

@@ -0,0 +1,77 @@
(use-modules (fruix system freebsd)
(fruix packages freebsd))
(define phase7-operating-system
(operating-system
#:host-name "fruix-freebsd"
#:kernel freebsd-kernel
#:bootloader freebsd-bootloader
#:base-packages (list freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-libc
freebsd-rc-scripts
freebsd-sh
freebsd-bash)
#:groups (list (user-group #:name "wheel" #:gid 0 #:system? #t)
(user-group #:name "sshd" #:gid 22 #:system? #t)
(user-group #:name "_dhcp" #:gid 65 #:system? #t)
(user-group #:name "operator" #:gid 1000 #:system? #f))
#:users (list (user-account #:name "root"
#:uid 0
#:group "wheel"
#:comment "Charlie &"
#:home "/root"
#:shell "/bin/sh"
#:system? #t)
(user-account #:name "sshd"
#:uid 22
#:group "sshd"
#:comment "Secure Shell Daemon"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "_dhcp"
#:uid 65
#:group "_dhcp"
#:comment "dhcp programs"
#:home "/var/empty"
#:shell "/usr/sbin/nologin"
#:system? #t)
(user-account #:name "operator"
#:uid 1000
#:group "operator"
#:supplementary-groups '("wheel")
#:comment "Fruix Operator"
#:home "/home/operator"
#:shell "/bin/sh"
#:system? #f))
#:file-systems (list (file-system #:device "/dev/gpt/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "tmpfs"
#:mount-point "/tmp"
#:type "tmpfs"
#:options "rw,size=64m"))
#:services '(shepherd ready-marker sshd)
#:loader-entries '(("autoboot_delay" . "1")
("boot_multicons" . "YES")
("boot_serial" . "YES")
("console" . "comconsole,vidconsole"))
#:rc-conf-entries '(("clear_tmp_enable" . "NO")
("hostid_enable" . "NO")
("sendmail_enable" . "NONE")
("sshd_enable" . "YES")
("ifconfig_xn0" . "SYNCDHCP")
("ifconfig_em0" . "SYNCDHCP")
("ifconfig_vtnet0" . "SYNCDHCP"))
#:ready-marker "/var/lib/fruix/ready"
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))

View File

@@ -5,11 +5,12 @@ project_root=${PROJECT_ROOT:-$(pwd)}
guix_source_dir=${GUIX_SOURCE_DIR:-"$HOME/repos/guix"}
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
runner_scm=$script_dir/materialize-phase8-system-image.scm
os_file=$script_dir/phase7-minimal-operating-system.scm
os_file=${OS_FILE:-$script_dir/phase7-minimal-operating-system.scm}
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}
store_dir=${STORE_DIR:-/frx/store}
disk_capacity=${DISK_CAPACITY:-}
metadata_target=${METADATA_OUT:-}
if [ ! -x "$guile_bin" ]; then
@@ -84,6 +85,7 @@ sudo env \
WORKDIR="$workdir" \
OS_FILE="$os_file" \
STORE_DIR="$store_dir" \
DISK_CAPACITY="$disk_capacity" \
GUILE_PREFIX="$guile_prefix" \
GUILE_EXTRA_PREFIX="$guile_extra_prefix" \
SHEPHERD_PREFIX="$shepherd_prefix" \
@@ -95,6 +97,7 @@ disk_image=$(sed -n 's/^disk_image=//p' "$build_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$build_metadata")
raw_sha256=$(sed -n 's/^raw_sha256=//p' "$build_metadata")
image_size_bytes=$(sed -n 's/^image_size_bytes=//p' "$build_metadata")
disk_capacity_reported=$(sed -n 's/^disk_capacity=//p' "$build_metadata")
store_item_count=$(sed -n 's/^store_item_count=//p' "$build_metadata")
closure_base=$(basename "$closure_path")
@@ -131,7 +134,7 @@ rc_script_target=$(readlink "$mnt_root/usr/local/etc/rc.d/fruix-shepherd")
[ "$rc_script_target" = /run/current-system/usr/local/etc/rc.d/fruix-shepherd ] || { echo "unexpected fruix_shepherd rc target: $rc_script_target" >&2; exit 1; }
loader_conf_image=$mnt_root/frx/store/$closure_base/boot/loader.conf
rc_conf_image=$mnt_root/frx/store/$closure_base/etc/rc.conf
grep -F 'console="comconsole"' "$loader_conf_image" >/dev/null || { echo "loader.conf is missing serial console config" >&2; exit 1; }
grep -F 'comconsole' "$loader_conf_image" >/dev/null || { echo "loader.conf is missing serial console config" >&2; exit 1; }
grep -F 'hostname="fruix-freebsd"' "$rc_conf_image" >/dev/null || { echo "rc.conf is missing hostname" >&2; exit 1; }
cat >"$metadata_file" <<EOF
@@ -144,6 +147,7 @@ closure_path=$closure_path
closure_base=$closure_base
raw_sha256=$raw_sha256
image_size_bytes=$image_size_bytes
disk_capacity=$disk_capacity_reported
store_item_count=$store_item_count
gpart_log=$gpart_log
esp_fstype=$esp_fstype

View File

@@ -0,0 +1,204 @@
#!/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}
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
}
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")
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_authorized_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_authorized_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' ' ')
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')
activate_preview=$(ssh_guest 'head -n 5 /run/current-system/activate' | tr '\n' ' ')
[ "$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; }
[ "$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" <<EOF
workdir=$workdir
vm_id=$vm_id
vdi_id=$vdi_id
vdi_size=$vdi_size
disk_capacity=$disk_capacity
requested_disk_capacity=${requested_disk_capacity:-<auto>}
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
uname_output=$uname_output
operator_home_listing=$operator_home_listing
activate_preview=$activate_preview
boot_backend=xcp-ng-xo-cli
operator_access=ssh-root-key
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"