From 4b69118d0666ce05657599a5b5ac77f56d1b319f Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Thu, 2 Apr 2026 07:34:51 +0200 Subject: [PATCH] Enable Fruix FreeBSD guest SSH boot on XCP-ng --- docs/PROGRESS.md | 63 ++++ docs/reports/phase9-xcpng-ssh-boot-freebsd.md | 213 ++++++++++++ modules/fruix/packages/freebsd.scm | 139 +++++++- modules/fruix/system/freebsd.scm | 326 ++++++++++++++---- .../materialize-phase8-system-image.scm | 38 +- .../phase7-minimal-operating-system.scm | 3 +- .../phase9-minimal-operating-system.scm.in | 77 +++++ tests/system/run-phase8-system-image.sh | 8 +- tests/system/run-phase9-xcpng-boot.sh | 204 +++++++++++ 9 files changed, 988 insertions(+), 83 deletions(-) create mode 100644 docs/reports/phase9-xcpng-ssh-boot-freebsd.md create mode 100644 tests/system/phase9-minimal-operating-system.scm.in create mode 100755 tests/system/run-phase9-xcpng-boot.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index cec44b6..ebac5c1 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2154,3 +2154,66 @@ Next recommended step: - Fruix at the product boundary - `/frx` as the canonical store root - stable upstream-derived internal names unless there is strong architectural value in renaming them + +## 2026-04-02 — Phase 9 checkpoint: XCP-ng guest reached DHCP and SSH + +Completed work: + +- added a dedicated Phase 9 XCP-ng operating-system template: + - `tests/system/phase9-minimal-operating-system.scm.in` +- added an XCP-ng boot/import/validation harness: + - `tests/system/run-phase9-xcpng-boot.sh` +- extended the staged FreeBSD runtime and system-generation layers so the guest can complete enough of real boot for network access: + - `modules/fruix/packages/freebsd.scm` + - `modules/fruix/system/freebsd.scm` +- updated the integrated image-generation path for Phase 9 use cases: + - `tests/system/materialize-phase8-system-image.scm` + - `tests/system/run-phase8-system-image.sh` +- wrote the checkpoint report: + - `docs/reports/phase9-xcpng-ssh-boot-freebsd.md` + +Important findings: + +- a decisive local QEMU/TCG serial-boot pass exposed the first real early-boot blocker: + - the generated `fstab` was wrong for pseudo-filesystems, so `rc` tried to fsck `devfs` and aborted boot +- after fixing `fstab`, later serial logs exposed additional FreeBSD base runtime gaps that only appear during real boot, including missing commands, runtime directories, and base config files used by `rc`, DHCP, logging, and service startup +- the staged image now includes the minimum currently known set of FreeBSD runtime pieces needed to: + - run `rc` + - obtain DHCP + - generate SSH host keys + - start `sshd` +- public-key SSH login initially still failed because the minimal guest did not stage a complete PAM runtime/config path; for the current Phase 9 prototype track, the generated `sshd_config` now uses: + - `UsePAM no` +- the current XCP-ng validation path succeeded against the operator-approved VM and existing VDI only: + - VM `90490f2e-e8fc-4b7a-388e-5c26f0157289` + - VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` +- the successful XCP-ng boot obtained: + - guest IP `192.168.213.62` +- successful SSH validation on the real guest confirmed: + - `hostname=fruix-freebsd` + - `sshd` is reachable with the injected root key + - networking is configured on the Xen NIC + +Current assessment: + +- this checkpoint establishes the first real network-reachable Fruix boot on the active FreeBSD/XCP-ng track +- the generated image now boots far enough for DHCP and SSH, which closes the earlier uncertainty about whether the Phase 8 image could become a remotely usable guest at all +- Phase 9 is still not complete because the Fruix-specific readiness path remains blocked: + - `fruix-shepherd` does not start + - `/var/lib/fruix/ready` is still missing + - Guile still crashes in the guest with `signal 11` +- therefore the current state is: + - kernel boot: yes + - root mount: yes + - DHCP: yes + - SSH: yes + - Shepherd/ready marker: not yet + +Next recommended step: + +1. continue the in-guest Guile crash investigation so `fruix-shepherd` can start on the booted guest +2. once Shepherd is stable, rerun `tests/system/run-phase9-xcpng-boot.sh` to validate the full ready-marker path end-to-end +3. then close Phase 9 with updated report/progress entries for: + - deterministic boot readiness + - in-guest Shepherd validation + - minimal operator usability diff --git a/docs/reports/phase9-xcpng-ssh-boot-freebsd.md b/docs/reports/phase9-xcpng-ssh-boot-freebsd.md new file mode 100644 index 0000000..940dff1 --- /dev/null +++ b/docs/reports/phase9-xcpng-ssh-boot-freebsd.md @@ -0,0 +1,213 @@ +# Phase 9 checkpoint: XCP-ng boot reached DHCP and SSH on FreeBSD + +Date: 2026-04-02 + +## Goal + +Advance Phase 9 from a static image-generation milestone to a real booted Fruix guest on the active FreeBSD/XCP-ng track, using the operator-approved VM: + +- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289` +- existing target VDI: `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` + +The immediate objective for this checkpoint was narrower than full Phase 9 completion: + +- boot the generated image under XCP-ng, +- obtain DHCP, +- and reach SSH access with the injected root key. + +## Summary + +This checkpoint succeeded. + +The current Fruix FreeBSD image now: + +- boots on the target XCP-ng VM, +- mounts the generated root filesystem, +- completes enough of FreeBSD `rc` startup to configure networking, +- obtains a DHCP lease on the Xen NIC, +- starts `sshd`, +- and accepts root public-key authentication over the network. + +Validated guest details from the successful XCP-ng boot: + +- guest IP: `192.168.213.62` +- hostname: `fruix-freebsd` +- kernel string: + - `FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64` + +Representative successful SSH validation output: + +```text +FreeBSD fruix-freebsd 15.0-STABLE FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64 +fruix-freebsd +192.168.213.62 +``` + +Successful XCP-ng work directory: + +- `/tmp/phase9-xcpng-ssh-1775097470` + +## Important boot/debugging findings + +The first decisive breakthrough came from running the generated image locally under QEMU/TCG with serial capture. That made the previously opaque early-boot failure visible. + +### 1. The original early boot abort was not an XCP-ng image-format problem anymore + +After the earlier switch from raw uploads to dynamic VHD uploads, the remaining boot failure was inside the guest boot process, not in the XO import path. + +### 2. FreeBSD `fstab` handling for pseudo-filesystems was wrong + +The serial log showed that boot aborted during filesystem checks because the generated `fstab` gave non-zero fsck fields to non-UFS mounts such as `devfs`. + +Representative failure: + +```text +Starting file system checks: +/dev/gpt/fruix-root: FILE SYSTEM CLEAN; SKIPPING CHECKS +/dev/gpt/fruix-root: clean, ... +fsck: exec fsck_devfs for devfs in /sbin:/usr/sbin: No such file or directory +Unknown error 1; help! +ERROR: ABORTING BOOT (sending SIGTERM to parent)! +``` + +The fix was to generate fsck pass fields only for UFS entries and emit `0 0` for pseudo-filesystems. + +### 3. The minimal image was still missing many base files and commands expected by `rc` + +Once `rc` ran further, QEMU serial logs exposed a long tail of missing runtime pieces that had not been visible from the earlier static validations alone. + +Examples included: + +- missing base commands: + - `dd` + - `expr` + - `rmdir` + - `sort` + - `mktemp` + - `egrep` + - `fsync` + - `kldload` + - `kldstat` + - `devfs` + - `devctl` + - `newsyslog` + - `ip6addrctl` +- missing base config files: + - `/etc/network.subr` + - `/etc/devd.conf` + - `/etc/newsyslog.conf` + - `/etc/syslog.conf` +- missing runtime directories: + - `/var/db` + - `/var/cron` +- missing libraries needed by later boot helpers: + - `libgeom.so.5` + - `libdevctl.so.5` + - `libcap_net.so.1` + - C++ runtime pieces used by `devd` + +These were staged into the current FreeBSD package layer and linked into the generated rootfs. + +### 4. SSH auth initially failed because the image relied on PAM without a complete PAM runtime/configuration + +`sshd` would start, but root public-key authentication still failed. A direct in-guest debug run showed: + +```text +PAM: initialisation failed +``` + +For the minimal Phase 9 guest, the practical fix was to make the generated `sshd_config` use: + +- `UsePAM no` + +while still keeping key-only login enabled. + +That was sufficient to unlock real SSH access on both the local QEMU debug guest and the XCP-ng guest. + +## Current code-level outcomes + +The current checkpoint work materially expanded the minimal FreeBSD runtime staged into Fruix images. + +Highlights: + +- `modules/fruix/packages/freebsd.scm` + - added dedicated runtime packages for: + - `freebsd-networking` + - `freebsd-openssh` + - expanded staged base runtime coverage substantially for `rc`, networking, and SSH + - added required config files and shared libraries used during real boot +- `modules/fruix/system/freebsd.scm` + - added root authorized-key support to the operating-system model + - generated static account databases and supporting files: + - `/etc/passwd` + - `/etc/master.passwd` + - `/etc/group` + - `/etc/login.conf` + - `/etc/ttys` + - activation now runs: + - `cap_mkdb` + - `pwd_mkdb` + - activation creates required directories and SSH host keys + - generated `sshd_config` now disables PAM for the current minimal key-only Phase 9 path + - `fstab` generation now avoids fsck pass numbers for pseudo-filesystems + - rootfs generation now links the additional `/etc` files needed by real boot +- `tests/system/phase9-minimal-operating-system.scm.in` + - enables DHCP on the relevant NIC names for the current tracks: + - `xn0` + - `em0` + - `vtnet0` + - injects the root authorized key + - includes the SSH/network runtime packages and required system users/groups +- `tests/system/run-phase8-system-image.sh` + - now accepts `OS_FILE` + - now accepts/passes `DISK_CAPACITY` + - serial-console validation was relaxed from an exact loader string to a `comconsole` presence check + +## Verified current state + +The current validated Phase 9 state is: + +- XCP-ng VHD upload path works against the existing VDI +- the guest boots far enough for normal `rc` networking and `sshd` +- DHCP works on the Xen NIC +- SSH key injection works +- root login over SSH works + +This means the project has crossed an important Phase 9 boundary: + +- the first boot validation no longer depends on local bhyve serial automation, +- and the real XCP-ng target can now be exercised over the network. + +## Remaining blocker + +Phase 9 is not complete yet because the Fruix-specific readiness path still fails. + +Current remaining blocker: + +- Guile still crashes in the guest +- therefore `fruix-shepherd` does not start +- therefore `/var/lib/fruix/ready` is still absent + +Representative guest evidence: + +```text +pid 262 (guile), jid 0, uid 0: exited on signal 11 (core dumped) +``` + +Over SSH on the real XCP-ng guest: + +- `sshd` is running +- DHCP is active +- `fruix-shepherd` is stopped +- `/var/lib/fruix/ready` is missing + +A retrieved core dump and local `lldb` analysis show the Guile crash occurs extremely early during initialization, in the locale/string conversion path while building Guile load/build info. This remains the next debugging target. + +## Assessment + +This checkpoint satisfies a meaningful Phase 9 intermediate milestone on the active FreeBSD/XCP-ng track: + +- the generated Fruix image now boots as a network-reachable FreeBSD guest, +- and minimal operator access via SSH is working. + +However, the full Fruix boot milestone is still blocked by in-guest Guile/Shepherd failure, so the overall Phase 9 milestone remains open. diff --git a/modules/fruix/packages/freebsd.scm b/modules/fruix/packages/freebsd.scm index b5fe021..84c33a9 100644 --- a/modules/fruix/packages/freebsd.scm +++ b/modules/fruix/packages/freebsd.scm @@ -18,6 +18,8 @@ freebsd-bootloader freebsd-rc-scripts freebsd-runtime + freebsd-networking + freebsd-openssh freebsd-userland freebsd-clang-toolchain freebsd-gmake @@ -95,9 +97,16 @@ and the userland C headers needed for development profiles." #:install-plan '((file "/lib/libc.so.7" "lib/libc.so.7") (file "/lib/libsys.so.7" "lib/libsys.so.7") + (file "/lib/libthr.so.3" "lib/libthr.so.3") (file "/lib/libutil.so.10" "lib/libutil.so.10") (file "/lib/libxo.so.0" "lib/libxo.so.0") + (file "/lib/libgeom.so.5" "lib/libgeom.so.5") + (file "/lib/libc++.so.1" "lib/libc++.so.1") + (file "/lib/libcxxrt.so.1" "lib/libcxxrt.so.1") + (file "/lib/libgcc_s.so.1" "lib/libgcc_s.so.1") (file "/lib/libm.so.5" "lib/libm.so.5") + (file "/lib/libelf.so.2" "lib/libelf.so.2") + (file "/lib/libkvm.so.7" "lib/libkvm.so.7") (file "/lib/lib80211.so.1" "lib/lib80211.so.1") (file "/lib/libjail.so.1" "lib/libjail.so.1") (file "/lib/libnv.so.1" "lib/libnv.so.1") @@ -105,6 +114,31 @@ and the userland C headers needed for development profiles." (file "/lib/libbsdxml.so.4" "lib/libbsdxml.so.4") (file "/lib/libcrypt.so.5" "lib/libcrypt.so.5") (file "/lib/libmd.so.7" "lib/libmd.so.7") + (file "/lib/libedit.so.8" "lib/libedit.so.8") + (file "/lib/libtinfow.so.9" "lib/libtinfow.so.9") + (file "/lib/libcasper.so.1" "lib/libcasper.so.1") + (file "/lib/libcap_syslog.so.1" "lib/libcap_syslog.so.1") + (file "/lib/libcap_fileargs.so.1" "lib/libcap_fileargs.so.1") + (file "/lib/libcap_net.so.1" "lib/libcap_net.so.1") + (file "/lib/libufs.so.8" "lib/libufs.so.8") + (file "/usr/lib/libdevinfo.so.7" "usr/lib/libdevinfo.so.7") + (file "/usr/lib/libdevctl.so.5" "usr/lib/libdevctl.so.5") + (file "/lib/libz.so.6" "lib/libz.so.6") + (file "/lib/libcrypto.so.35" "lib/libcrypto.so.35") + (file "/usr/lib/libssl.so.35" "usr/lib/libssl.so.35") + (file "/usr/lib/libdl.so.1" "usr/lib/libdl.so.1") + (file "/usr/lib/libpam.so.6" "usr/lib/libpam.so.6") + (file "/usr/lib/libbsm.so.3" "usr/lib/libbsm.so.3") + (file "/usr/lib/libblocklist.so.0" "usr/lib/libblocklist.so.0") + (file "/usr/lib/libregex.so.1" "usr/lib/libregex.so.1") + (file "/usr/lib/libprivatessh.so.5" "usr/lib/libprivatessh.so.5") + (file "/usr/lib/libprivateldns.so.5" "usr/lib/libprivateldns.so.5") + (file "/usr/lib/libwrap.so.6" "usr/lib/libwrap.so.6") + (file "/usr/lib/libgssapi_krb5.so.122" "usr/lib/libgssapi_krb5.so.122") + (file "/usr/lib/libkrb5.so.122" "usr/lib/libkrb5.so.122") + (file "/usr/lib/libk5crypto.so.122" "usr/lib/libk5crypto.so.122") + (file "/usr/lib/libcom_err.so.122" "usr/lib/libcom_err.so.122") + (file "/usr/lib/libkrb5support.so.122" "usr/lib/libkrb5support.so.122") (file "/libexec/ld-elf.so.1" "libexec/ld-elf.so.1")))) (define freebsd-bootloader @@ -155,16 +189,45 @@ userland commands needed for development and build experiments." #:license 'bsd-2 #:install-plan '((file "/bin/cat" "bin/cat") + (file "/bin/chflags" "bin/chflags") + (file "/bin/chmod" "bin/chmod") (file "/bin/cp" "bin/cp") + (file "/bin/date" "bin/date") + (file "/bin/dd" "bin/dd") (file "/bin/echo" "bin/echo") + (file "/bin/expr" "bin/expr") (file "/bin/ln" "bin/ln") (file "/bin/ls" "bin/ls") (file "/bin/mkdir" "bin/mkdir") (file "/bin/mv" "bin/mv") + (file "/bin/ps" "bin/ps") (file "/bin/pwd" "bin/pwd") + (file "/bin/rmdir" "bin/rmdir") (file "/bin/rm" "bin/rm") + (file "/bin/sleep" "bin/sleep") + (file "/bin/stty" "bin/stty") + (file "/bin/sync" "bin/sync") + (file "/usr/bin/awk" "usr/bin/awk") + (file "/usr/bin/basename" "usr/bin/basename") + (file "/usr/bin/cap_mkdb" "usr/bin/cap_mkdb") + (file "/usr/bin/cut" "usr/bin/cut") + (file "/usr/bin/dirname" "usr/bin/dirname") + (file "/usr/bin/egrep" "usr/bin/egrep") + (file "/usr/bin/env" "usr/bin/env") (file "/usr/bin/find" "bin/find") + (file "/usr/bin/fsync" "usr/bin/fsync") + (file "/usr/bin/grep" "usr/bin/grep") + (file "/usr/bin/mktemp" "usr/bin/mktemp") + (file "/usr/bin/head" "usr/bin/head") + (file "/usr/bin/install" "usr/bin/install") + (file "/usr/bin/limits" "usr/bin/limits") + (file "/usr/bin/logger" "usr/bin/logger") + (file "/usr/bin/readlink" "usr/bin/readlink") + (file "/usr/bin/sed" "usr/bin/sed") + (file "/usr/bin/sort" "usr/bin/sort") (file "/usr/bin/tar" "bin/tar") + (file "/usr/bin/tr" "usr/bin/tr") + (file "/usr/bin/uname" "usr/bin/uname") (file "/usr/bin/xargs" "bin/xargs")))) (define freebsd-rc-scripts @@ -183,6 +246,10 @@ files needed by the first Fruix system-closure experiments." '((file "/etc/rc" "etc/rc") (file "/etc/rc.subr" "etc/rc.subr") (file "/etc/rc.shutdown" "etc/rc.shutdown") + (file "/etc/devd.conf" "etc/devd.conf") + (file "/etc/network.subr" "etc/network.subr") + (file "/etc/newsyslog.conf" "etc/newsyslog.conf") + (file "/etc/syslog.conf" "etc/syslog.conf") (directory "/etc/rc.d" "etc/rc.d") (directory "/etc/defaults" "etc/defaults")))) @@ -200,17 +267,81 @@ commands needed by the first declarative Fruix system and activation payload experiments." #:license 'bsd-2 #:install-plan - '((file "/sbin/init" "sbin/init") - (file "/sbin/mount" "sbin/mount") + '((file "/sbin/adjkerntz" "sbin/adjkerntz") + (file "/sbin/devd" "sbin/devd") + (file "/sbin/devmatch" "sbin/devmatch") + (file "/sbin/dmesg" "sbin/dmesg") + (file "/sbin/fsck" "sbin/fsck") + (file "/sbin/fsck_ufs" "sbin/fsck_ufs") + (file "/sbin/gpart" "sbin/gpart") + (file "/sbin/init" "sbin/init") (file "/sbin/ifconfig" "sbin/ifconfig") + (file "/sbin/md5" "sbin/md5") + (file "/sbin/mount" "sbin/mount") + (file "/sbin/rcorder" "sbin/rcorder") (file "/sbin/reboot" "sbin/reboot") + (file "/sbin/sha256" "sbin/sha256") (file "/sbin/shutdown" "sbin/shutdown") + (file "/sbin/swapon" "sbin/swapon") + (file "/sbin/sysctl" "sbin/sysctl") + (file "/usr/sbin/chown" "usr/sbin/chown") + (file "/usr/sbin/cron" "usr/sbin/cron") + (file "/usr/sbin/devctl" "usr/sbin/devctl") + (file "/usr/sbin/nologin" "usr/sbin/nologin") + (file "/usr/sbin/pwd_mkdb" "usr/sbin/pwd_mkdb") (file "/usr/sbin/service" "usr/sbin/service") - (file "/usr/sbin/pw" "usr/sbin/pw") + (file "/usr/sbin/ip6addrctl" "usr/sbin/ip6addrctl") + (file "/usr/sbin/newsyslog" "usr/sbin/newsyslog") + (file "/usr/sbin/syslogd" "usr/sbin/syslogd") + (file "/usr/sbin/utx" "usr/sbin/utx") (file "/usr/bin/id" "usr/bin/id") + (file "/sbin/kldload" "sbin/kldload") + (file "/sbin/kldstat" "sbin/kldstat") + (file "/sbin/devfs" "sbin/devfs") + (file "/bin/freebsd-version" "bin/freebsd-version") (file "/bin/hostname" "bin/hostname") (file "/bin/kenv" "bin/kenv")))) +(define freebsd-networking + (freebsd-package + #:name "freebsd-networking" + #:version freebsd-release + #:build-system 'copy-build-system + #:inputs (list freebsd-libc freebsd-runtime freebsd-sh) + #:home-page "https://www.freebsd.org/" + #:synopsis "Prototype package for FreeBSD network runtime tools" + #:description + "Prototype package definition that stages the minimal FreeBSD networking +runtime needed for DHCP-based boot validation in virtual machines." + #:license 'bsd-2 + #:install-plan + '((file "/sbin/dhclient" "sbin/dhclient") + (file "/sbin/dhclient-script" "sbin/dhclient-script") + (file "/sbin/route" "sbin/route") + (file "/usr/bin/netstat" "usr/bin/netstat") + (file "/usr/sbin/arp" "usr/sbin/arp")))) + +(define freebsd-openssh + (freebsd-package + #:name "freebsd-openssh" + #:version freebsd-release + #:build-system 'copy-build-system + #:inputs (list freebsd-libc freebsd-runtime freebsd-sh) + #:home-page "https://www.freebsd.org/" + #:synopsis "Prototype package for the FreeBSD OpenSSH runtime" + #:description + "Prototype package definition that stages the FreeBSD OpenSSH server and +client tools needed for first-boot operator access on the Fruix prototype +track." + #:license 'bsd-2 + #:install-plan + '((file "/usr/sbin/sshd" "usr/sbin/sshd") + (file "/usr/bin/ssh" "usr/bin/ssh") + (file "/usr/bin/ssh-keygen" "usr/bin/ssh-keygen") + (file "/usr/libexec/sshd-auth" "usr/libexec/sshd-auth") + (file "/usr/libexec/sshd-session" "usr/libexec/sshd-session") + (file "/etc/ssh/moduli" "etc/ssh/moduli")))) + (define freebsd-clang-toolchain (freebsd-package #:name "freebsd-clang-toolchain" @@ -354,6 +485,8 @@ library for profile experiments." freebsd-libc freebsd-rc-scripts freebsd-runtime + freebsd-networking + freebsd-openssh freebsd-userland freebsd-sh freebsd-bash)) diff --git a/modules/fruix/system/freebsd.scm b/modules/fruix/system/freebsd.scm index 0bb17f0..d8d5b96 100644 --- a/modules/fruix/system/freebsd.scm +++ b/modules/fruix/system/freebsd.scm @@ -45,6 +45,7 @@ operating-system-loader-entries operating-system-rc-conf-entries operating-system-ready-marker + operating-system-root-authorized-keys validate-operating-system operating-system-closure-spec operating-system-image-spec @@ -96,7 +97,7 @@ (define-record-type (make-operating-system host-name kernel bootloader base-packages users groups file-systems services loader-entries rc-conf-entries - ready-marker) + ready-marker root-authorized-keys) operating-system? (host-name operating-system-host-name) (kernel operating-system-kernel) @@ -108,7 +109,8 @@ (services operating-system-services) (loader-entries operating-system-loader-entries) (rc-conf-entries operating-system-rc-conf-entries) - (ready-marker operating-system-ready-marker)) + (ready-marker operating-system-ready-marker) + (root-authorized-keys operating-system-root-authorized-keys)) (define* (operating-system #:key (host-name "fruix-freebsd") @@ -153,10 +155,11 @@ (rc-conf-entries '(("clear_tmp_enable" . "YES") ("sendmail_enable" . "NONE") ("sshd_enable" . "NO"))) - (ready-marker "/var/lib/fruix/ready")) + (ready-marker "/var/lib/fruix/ready") + (root-authorized-keys '())) (make-operating-system host-name kernel bootloader base-packages users groups file-systems services loader-entries rc-conf-entries - ready-marker)) + ready-marker root-authorized-keys)) (define default-minimal-operating-system (operating-system)) @@ -292,11 +295,41 @@ (hash-set! cache (package-cache-key package) output-path) output-path)))) -(define (prefix-manifest-string source-path) - (string-append "prefix-source=" source-path "\n" (path-signature source-path))) +(define prefix-materializer-version "2") -(define (materialize-prefix source-path name version store-dir) - (let* ((manifest (prefix-manifest-string source-path)) +(define (prefix-manifest-string source-path extra-files) + (string-append + "prefix-materializer-version=" prefix-materializer-version "\n" + "prefix-source=" source-path "\n" + (path-signature source-path) + (if (null? extra-files) + "" + (string-append + "\nextra-files=\n" + (string-join + (map (lambda (entry) + (string-append (cdr entry) "\n" (path-signature (car entry)))) + extra-files) + "\n"))))) + +(define (copy-extra-node source destination) + (let ((kind (stat:type (lstat source)))) + (mkdir-p (dirname destination)) + (case kind + ((symlink) + (unless (or (file-exists? destination) + (false-if-exception (readlink destination))) + (let ((target (readlink source))) + (symlink target destination) + (unless (string-prefix? "/" target) + (copy-extra-node (string-append (dirname source) "/" target) + (string-append (dirname destination) "/" target)))))) + (else + (unless (file-exists? destination) + (copy-node source destination)))))) + +(define* (materialize-prefix source-path name version store-dir #:key (extra-files '())) + (let* ((manifest (prefix-manifest-string source-path extra-files)) (hash (string-hash manifest)) (output-path (string-append store-dir "/" hash "-" name "-" version))) (unless (file-exists? output-path) @@ -305,6 +338,10 @@ (copy-node (string-append source-path "/" entry) (string-append output-path "/" entry))) (directory-entries source-path)) + (for-each (lambda (entry) + (copy-extra-node (car entry) + (string-append output-path "/" (cdr entry)))) + extra-files) (write-file (string-append output-path "/.fruix-package") manifest)) output-path)) @@ -352,8 +389,18 @@ "\n") "\n")) +(define (rc-conf-entry-value os key) + (let ((entry (assoc key (operating-system-rc-conf-entries os)))) + (and entry (cdr entry)))) + +(define (sshd-enabled? os) + (let ((value (rc-conf-entry-value os "sshd_enable"))) + (and value + (member (string-upcase value) '("YES" "TRUE" "1"))))) + (define (render-rc.conf os) (let* ((entries (append `(("hostname" . ,(operating-system-host-name os)) + ("fruix_activate_enable" . "YES") ("fruix_shepherd_enable" . "YES")) (operating-system-rc-conf-entries os)))) (string-append @@ -386,6 +433,23 @@ "\n") "\n"))) +(define (render-master-passwd os) + (let ((groups (operating-system-groups os))) + (string-append + (string-join + (map (lambda (account) + (format #f "~a:*:~a:~a::0:0:~a:~a:~a" + (user-account-name account) + (user-account-uid account) + (or (group-name->gid groups (user-account-group account)) + (error "unknown primary group" (user-account-group account))) + (user-account-comment account) + (user-account-home account) + (user-account-shell account))) + (operating-system-users os)) + "\n") + "\n"))) + (define (render-group os) (let ((users (operating-system-users os))) (string-append @@ -404,17 +468,25 @@ "\n") "\n"))) +(define (fstab-fsck-fields fs) + (if (string=? (file-system-type fs) "ufs") + (if (string=? (file-system-mount-point fs) "/") + '(1 1) + '(2 2)) + '(0 0))) + (define (render-fstab os) (string-append (string-join (map (lambda (fs) - (format #f "~a\t~a\t~a\t~a\t~a\t~a" - (file-system-device fs) - (file-system-mount-point fs) - (file-system-type fs) - (file-system-options fs) - (if (string=? (file-system-mount-point fs) "/") 1 0) - (if (file-system-needed-for-boot? fs) 1 2))) + (let ((checks (fstab-fsck-fields fs))) + (format #f "~a\t~a\t~a\t~a\t~a\t~a" + (file-system-device fs) + (file-system-mount-point fs) + (file-system-type fs) + (file-system-options fs) + (first checks) + (second checks)))) (operating-system-file-systems os)) "\n") "\n")) @@ -431,43 +503,85 @@ (define (render-motd os) (string-append "Welcome to Fruix on FreeBSD (" (operating-system-host-name os) ")\n")) +(define (render-login-conf) + (string-append + "default:\\\n" + "\t:path=/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin:\\\n" + "\t:umask=022:\\\n" + "\t:charset=UTF-8:\\\n" + "\t:lang=C.UTF-8:\n" + "daemon:\\\n" + "\t:path=/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin:\\\n" + "\t:tc=default:\n" + "root:\\\n" + "\t:ignorenologin:\\\n" + "\t:tc=default:\n")) + +(define (render-ttys) + (string-append + "console\tnone\tunknown\toff secure\n" + "ttyu0\tnone\tvt100\toff secure\n" + "xc0\tnone\txterm\toff secure\n")) + +(define (render-root-authorized-keys os) + (if (null? (operating-system-root-authorized-keys os)) + "" + (string-append + (string-join (operating-system-root-authorized-keys os) "\n") + "\n"))) + +(define (render-sshd-config os) + (string-append + "Port 22\n" + "PermitRootLogin yes\n" + "PasswordAuthentication no\n" + "KbdInteractiveAuthentication no\n" + "ChallengeResponseAuthentication no\n" + "UsePAM no\n" + "PubkeyAuthentication yes\n" + "AuthorizedKeysFile .ssh/authorized_keys\n" + "PidFile /var/run/sshd.pid\n" + "UseDNS no\n")) + (define (render-activation-script os) (let* ((users (operating-system-users os)) (groups (operating-system-groups os)) - (non-root-groups (filter (lambda (group) - (> (user-group-gid group) 0)) - groups)) - (non-root-users (filter (lambda (account) - (> (user-account-uid account) 0)) - users))) + (home-setup + (string-join + (map (lambda (account) + (let ((name (user-account-name account)) + (uid (user-account-uid account)) + (gid (or (group-name->gid groups (user-account-group account)) + (error "unknown primary group" (user-account-group account)))) + (home (user-account-home account)) + (system? (user-account-system? account))) + (string-append + "mkdir -p " home "\n" + (if (or (string=? name "root") system?) + "" + (format #f "if [ -x /usr/sbin/chown ]; then /usr/sbin/chown ~a:~a ~a 2>/dev/null || true; fi\n" + uid gid home))))) + users) + "")) + (ssh-section + (string-append + "mkdir -p /var/empty /etc/ssh /root/.ssh\n" + "chmod 700 /root/.ssh\n" + (if (null? (operating-system-root-authorized-keys os)) + "" + "if [ -f /run/current-system/root/.ssh/authorized_keys ]; then cp /run/current-system/root/.ssh/authorized_keys /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys; fi\n") + (if (sshd-enabled? os) + "if [ -x /usr/bin/ssh-keygen ]; then /usr/bin/ssh-keygen -A; fi\n" + "")))) (string-append "#!/bin/sh\n" "set -eu\n" - "mkdir -p /var/lib/fruix /var/log /var/run /root /home /tmp\n" + "mkdir -p /var/cron /var/db /var/lib/fruix /var/log /var/run /root /home /tmp\n" "chmod 1777 /tmp\n" - (string-join - (append - (map (lambda (group) - (format #f "pw groupadd ~a -g ~a 2>/dev/null || true" - (user-group-name group) - (user-group-gid group))) - non-root-groups) - (map (lambda (account) - (let ((group (user-account-group account)) - (supplementary (user-account-supplementary-groups account))) - (format #f "pw useradd ~a -u ~a -g ~a~a -d ~a -m -s ~a -c '~a' 2>/dev/null || true" - (user-account-name account) - (user-account-uid account) - group - (if (null? supplementary) - "" - (string-append " -G " (string-join supplementary ","))) - (user-account-home account) - (user-account-shell account) - (user-account-comment account)))) - non-root-users)) - "\n") - "\n"))) + "if [ -x /usr/bin/cap_mkdb ] && [ -f /etc/login.conf ]; then /usr/bin/cap_mkdb /etc/login.conf; fi\n" + "if [ -x /usr/sbin/pwd_mkdb ] && [ -f /etc/master.passwd ]; then /usr/sbin/pwd_mkdb -p /etc/master.passwd; fi\n" + home-setup + ssh-section))) (define (render-shepherd-config os) (let ((ready-marker (operating-system-ready-marker os))) @@ -502,6 +616,26 @@ " #:respawn? #f)))\n\n" "(start-service (lookup-service 'fruix-ready))\n"))) +(define (render-activation-rc-script) + (string-append + "#!/bin/sh\n" + "# PROVIDE: fruix_activate\n" + "# REQUIRE: FILESYSTEMS\n" + "# BEFORE: LOGIN sshd fruix_shepherd\n" + "# KEYWORD: shutdown\n\n" + ". /etc/rc.subr\n\n" + "name=fruix_activate\n" + "rcvar=fruix_activate_enable\n" + ": ${fruix_activate_enable:=YES}\n" + "start_cmd=fruix_activate_start\n" + "stop_cmd=:\n\n" + "fruix_activate_start()\n" + "{\n" + " /run/current-system/activate\n" + "}\n\n" + "load_rc_config $name\n" + "run_rc_command \"$1\"\n")) + (define (render-rc-script shepherd-store guile-store guile-extra-store) (let ((ld-library-path (string-append guile-extra-store "/lib:" guile-store "/lib:/usr/local/lib")) @@ -514,7 +648,7 @@ (string-append "#!/bin/sh\n" "# PROVIDE: fruix_shepherd\n" - "# REQUIRE: FILESYSTEMS\n" + "# REQUIRE: FILESYSTEMS fruix_activate\n" "# BEFORE: LOGIN\n" "# KEYWORD: shutdown\n\n" ". /etc/rc.subr\n\n" @@ -535,7 +669,7 @@ " GUILE_LOAD_PATH='" guile-load-path "' \\\n" " GUILE_LOAD_COMPILED_PATH='" guile-load-compiled-path "' \\\n" " GUILE_EXTENSIONS_PATH='" guile-extensions-path "' \\\n" - " " shepherd-store "/bin/shepherd -I -s \"$socket\" -c \"$config\" --pid=\"$pidfile\" -l \"$logfile\" >/var/log/shepherd-bootstrap.out 2>&1 &\n" + " " guile-store "/bin/guile --no-auto-compile " shepherd-store "/bin/shepherd -I -s \"$socket\" -c \"$config\" --pid=\"$pidfile\" -l \"$logfile\" >/var/log/shepherd-bootstrap.out 2>&1 &\n" " for _try in 1 2 3 4 5 6 7 8 9 10; do\n" " [ -f \"$pidfile\" ] && [ -S \"$socket\" ] && return 0\n" " sleep 1\n" @@ -548,7 +682,7 @@ " GUILE_LOAD_PATH='" guile-load-path "' \\\n" " GUILE_LOAD_COMPILED_PATH='" guile-load-compiled-path "' \\\n" " GUILE_EXTENSIONS_PATH='" guile-extensions-path "' \\\n" - " " shepherd-store "/bin/herd -s \"$socket\" stop root >/dev/null 2>&1 || true\n" + " " guile-store "/bin/guile --no-auto-compile " shepherd-store "/bin/herd -s \"$socket\" stop root >/dev/null 2>&1 || true\n" " for _try in 1 2 3 4 5 6 7 8 9 10; do\n" " [ ! -f \"$pidfile\" ] && return 0\n" " sleep 1\n" @@ -565,16 +699,26 @@ "run_rc_command \"$1\"\n"))) (define (operating-system-generated-files os) - `(("boot/loader.conf" . ,(render-loader-conf (operating-system-loader-entries os))) - ("etc/rc.conf" . ,(render-rc.conf os)) - ("etc/fstab" . ,(render-fstab os)) - ("etc/hosts" . ,(render-hosts os)) - ("etc/passwd" . ,(render-passwd os)) - ("etc/group" . ,(render-group os)) - ("etc/shells" . ,(render-shells os)) - ("etc/motd" . ,(render-motd os)) - ("activate" . ,(render-activation-script os)) - ("shepherd/init.scm" . ,(render-shepherd-config os)))) + (append + `(("boot/loader.conf" . ,(render-loader-conf (operating-system-loader-entries os))) + ("etc/rc.conf" . ,(render-rc.conf os)) + ("etc/fstab" . ,(render-fstab os)) + ("etc/hosts" . ,(render-hosts os)) + ("etc/passwd" . ,(render-passwd os)) + ("etc/master.passwd" . ,(render-master-passwd os)) + ("etc/group" . ,(render-group os)) + ("etc/login.conf" . ,(render-login-conf)) + ("etc/shells" . ,(render-shells os)) + ("etc/motd" . ,(render-motd os)) + ("etc/ttys" . ,(render-ttys)) + ("activate" . ,(render-activation-script os)) + ("shepherd/init.scm" . ,(render-shepherd-config os))) + (if (sshd-enabled? os) + `(("etc/ssh/sshd_config" . ,(render-sshd-config os))) + '()) + (if (null? (operating-system-root-authorized-keys os)) + '() + `(("root/.ssh/authorized_keys" . ,(render-root-authorized-keys os)))))) (define (operating-system-closure-spec os) (validate-operating-system os) @@ -649,11 +793,31 @@ (base-package-stores (map (lambda (package) (materialize-freebsd-package package store-dir cache)) (operating-system-base-packages os))) - (guile-store (materialize-prefix guile-prefix "fruix-guile-runtime" "3.0" store-dir)) - (guile-extra-store (materialize-prefix guile-extra-prefix "fruix-guile-extra" "3.0" store-dir)) + (guile-runtime-extra-files + '(("/usr/local/lib/libgc-threaded.so.1" . "lib/libgc-threaded.so.1") + ("/usr/local/lib/libffi.so.8" . "lib/libffi.so.8") + ("/usr/local/lib/libintl.so.8" . "lib/libintl.so.8") + ("/usr/local/lib/libunistring.so.5" . "lib/libunistring.so.5") + ("/usr/local/lib/libiconv.so.2" . "lib/libiconv.so.2") + ("/usr/local/lib/libgmp.so.10" . "lib/libgmp.so.10"))) + (guile-extra-runtime-files + '(("/usr/local/lib/libevent-2.1.so.7" . "lib/libevent-2.1.so.7") + ("/usr/local/lib/libgnutls.so.30" . "lib/libgnutls.so.30") + ("/usr/local/lib/libp11-kit.so.0" . "lib/libp11-kit.so.0") + ("/usr/local/lib/libidn2.so.0" . "lib/libidn2.so.0") + ("/usr/local/lib/libtasn1.so.6" . "lib/libtasn1.so.6") + ("/usr/local/lib/libhogweed.so.6" . "lib/libhogweed.so.6") + ("/usr/local/lib/libnettle.so.8" . "lib/libnettle.so.8"))) + (guile-store (materialize-prefix guile-prefix "fruix-guile-runtime" "3.0" store-dir + #:extra-files guile-runtime-extra-files)) + (guile-extra-store (materialize-prefix guile-extra-prefix "fruix-guile-extra" "3.0" store-dir + #:extra-files (append guile-runtime-extra-files + guile-extra-runtime-files))) (shepherd-store (materialize-prefix shepherd-prefix "fruix-shepherd-runtime" "1.0.9" store-dir)) (generated-files (append (operating-system-generated-files os) - `(("usr/local/etc/rc.d/fruix-shepherd" + `(("usr/local/etc/rc.d/fruix-activate" + . ,(render-activation-rc-script)) + ("usr/local/etc/rc.d/fruix-shepherd" . ,(render-rc-script shepherd-store guile-store guile-extra-store))))) (references (append (list kernel-store bootloader-store guile-store guile-extra-store shepherd-store) base-package-stores)) @@ -692,6 +856,7 @@ (write-file (string-append closure-path "/" (car entry)) (cdr entry))) generated-files) (chmod (string-append closure-path "/activate") #o555) + (chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-activate") #o555) (chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd") #o555) (write-file (string-append closure-path "/parameters.scm") (object->string (operating-system-closure-spec os))) @@ -731,8 +896,8 @@ (mkdir-p rootfs) (for-each (lambda (dir) (mkdir-p (string-append rootfs dir))) - '("/run" "/boot" "/etc" "/usr" "/usr/local" "/usr/local/etc" - "/usr/local/etc/rc.d" "/var" "/var/lib" "/var/lib/fruix" + '("/run" "/boot" "/etc" "/etc/ssh" "/usr" "/usr/local" "/usr/local/etc" + "/usr/local/etc/rc.d" "/var" "/var/cron" "/var/db" "/var/lib" "/var/lib/fruix" "/var/log" "/var/run" "/tmp" "/dev" "/root" "/home")) (chmod (string-append rootfs "/tmp") #o1777) (symlink-force closure-path (string-append rootfs "/run/current-system")) @@ -744,19 +909,26 @@ (for-each (lambda (dir) (symlink-force (string-append "/run/current-system/profile/usr/" dir) (string-append rootfs "/usr/" dir))) - '("bin" "sbin" "libexec")) + '("bin" "lib" "sbin" "libexec")) (for-each (lambda (path) (symlink-force (string-append "/run/current-system/profile/etc/" path) (string-append rootfs "/etc/" path))) - '("rc" "rc.subr" "rc.shutdown" "rc.d" "defaults")) + '("rc" "rc.subr" "rc.shutdown" "rc.d" "defaults" + "devd.conf" "network.subr" "newsyslog.conf" "syslog.conf")) (for-each (lambda (path) (symlink-force (string-append "/run/current-system/etc/" path) (string-append rootfs "/etc/" path))) - '("rc.conf" "fstab" "hosts" "passwd" "group" "shells" "motd")) + '("rc.conf" "fstab" "hosts" "passwd" "master.passwd" "group" + "login.conf" "shells" "motd" "ttys")) + (when (file-exists? (string-append closure-path "/etc/ssh/sshd_config")) + (symlink-force "/run/current-system/etc/ssh/sshd_config" + (string-append rootfs "/etc/ssh/sshd_config"))) (for-each (lambda (path) (symlink-force (string-append "/run/current-system/boot/" path) (string-append rootfs "/boot/" path))) '("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf")) + (symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate" + (string-append rootfs "/usr/local/etc/rc.d/fruix-activate")) (symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-shepherd" (string-append rootfs "/usr/local/etc/rc.d/fruix-shepherd")) `((rootfs . ,rootfs) @@ -771,6 +943,7 @@ (partition-scheme 'gpt) (efi-size "64m") (root-size "256m") + (disk-capacity #f) (efi-partition-label "efiboot") (root-partition-label "fruix-root") (serial-console "comconsole")) @@ -780,6 +953,7 @@ (partition-scheme . ,partition-scheme) (efi-size . ,efi-size) (root-size . ,root-size) + (disk-capacity . ,disk-capacity) (efi-partition-label . ,efi-partition-label) (root-partition-label . ,root-partition-label) (serial-console . ,serial-console) @@ -830,6 +1004,19 @@ (define (mktemp-directory pattern) (command-output "mktemp" "-d" pattern)) +(define image-builder-version "2") + +(define (resize-gpt-image image disk-capacity) + (when disk-capacity + (run-command "truncate" "-s" disk-capacity image) + (let ((md (command-output "mdconfig" "-a" "-t" "vnode" "-f" image))) + (dynamic-wind + (lambda () #t) + (lambda () + (run-command "gpart" "recover" (string-append "/dev/" md))) + (lambda () + (run-command "mdconfig" "-d" "-u" (string-drop md 2))))))) + (define* (materialize-bhyve-image os #:key (store-dir "/frx/store") @@ -838,6 +1025,7 @@ (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") (efi-size "64m") (root-size "256m") + (disk-capacity #f) (efi-partition-label "efiboot") (root-partition-label "fruix-root") (serial-console "comconsole")) @@ -850,12 +1038,15 @@ (image-spec (operating-system-image-spec os #:efi-size efi-size #:root-size root-size + #:disk-capacity disk-capacity #:efi-partition-label efi-partition-label #:root-partition-label root-partition-label #:serial-console serial-console)) (store-items (store-reference-closure (list closure-path))) (manifest (string-append - "image-spec=\n" + "image-builder-version=\n" + image-builder-version + "\nimage-spec=\n" (object->string image-spec) "closure-path=\n" closure-path @@ -905,6 +1096,7 @@ "-p" (string-append "efi/" efi-partition-label ":=" temp-esp) "-p" (string-append "freebsd-ufs/" root-partition-label ":=" temp-root) "-o" temp-disk) + (resize-gpt-image temp-disk disk-capacity) (mkdir-p temp-output) (copy-regular-file temp-disk (string-append temp-output "/disk.img")) (copy-regular-file temp-esp (string-append temp-output "/esp.img")) diff --git a/tests/system/materialize-phase8-system-image.scm b/tests/system/materialize-phase8-system-image.scm index a71b4af..767d675 100644 --- a/tests/system/materialize-phase8-system-image.scm +++ b/tests/system/materialize-phase8-system-image.scm @@ -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 "")) (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) diff --git a/tests/system/phase7-minimal-operating-system.scm b/tests/system/phase7-minimal-operating-system.scm index 7bd9326..f7c2e02 100644 --- a/tests/system/phase7-minimal-operating-system.scm +++ b/tests/system/phase7-minimal-operating-system.scm @@ -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")) diff --git a/tests/system/phase9-minimal-operating-system.scm.in b/tests/system/phase9-minimal-operating-system.scm.in new file mode 100644 index 0000000..b9bb769 --- /dev/null +++ b/tests/system/phase9-minimal-operating-system.scm.in @@ -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__"))) diff --git a/tests/system/run-phase8-system-image.sh b/tests/system/run-phase8-system-image.sh index d5a6d6d..81bae56 100755 --- a/tests/system/run-phase8-system-image.sh +++ b/tests/system/run-phase8-system-image.sh @@ -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" <&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" <} +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"