diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 78986d2..d608d1f 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2421,3 +2421,68 @@ Next recommended step: 1. begin the next post-Phase-10 cleanup/polish pass outside the plan milestones 2. prioritize replacing the current Guile / Shepherd compatibility-prefix shims with a more native store-path-aware runtime arrangement 3. consider adding richer deploy/vm-oriented `fruix` commands beyond the now-canonical `system build/rootfs/image` path + +## 2026-04-02 — Post-Phase-10: local Shepherd-as-PID-1 prototype booted on FreeBSD + +Completed work: + +- began the next post-Phase-10 runtime-integration pass by exploring a Shepherd-as-PID-1 boot mode for Fruix on FreeBSD +- compared the approach with Guix's root Shepherd design in: + - `~/repos/guix/gnu/services/shepherd.scm` +- wrote the subphase report: + - `docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md` +- extended the declarative FreeBSD operating-system model in: + - `modules/fruix/system/freebsd.scm` + - added an `init-mode` field with: + - `freebsd-init+rc.d-shepherd` + - `shepherd-pid1` + - generated loader configuration now sets: + - `init_exec="/run/current-system/boot/fruix-pid1"` + when `init-mode` is `shepherd-pid1` + - generated systems in PID 1 mode now include: + - `boot/fruix-pid1` + - the generated activation script now treats `cap_mkdb` / `pwd_mkdb` as best-effort so immutable store-backed config files do not abort this early boot path +- added a dedicated Shepherd-PID-1 operating-system template: + - `tests/system/phase11-shepherd-pid1-operating-system.scm.in` +- added a dedicated local QEMU/UEFI validation harness: + - `tests/system/run-phase11-shepherd-pid1-qemu.sh` + +Important findings: + +- FreeBSD's `init(8)` already has a suitable handoff mechanism for this experiment via: + - `init_exec` +- compared with Guix, the current Fruix implementation is still much more imperative, but it now follows the same broad direction: + - boot into Shepherd directly as PID 1 rather than merely starting Shepherd late from rc.d +- the first PID 1 attempt failed because the generated Shepherd config imported a repo-side module: + - `(fruix shepherd freebsd)` + that was not present inside the guest runtime; the fix was to inline the small helper procedures needed by the generated config itself +- the early PID 1 path also exposed that store-backed `/etc/login.conf` and `/etc/master.passwd` updates must be best-effort rather than fatal on this bootstrap path +- for the current locally built runtime artifacts, the compatibility-prefix shims are still needed; this subphase did not eliminate them yet, but it did remove the larger `rc.d` boot-manager dependency from the local prototype path + +Validation: + +- `tests/system/run-phase11-shepherd-pid1-qemu.sh` now passes +- passing run workdir: + - `/tmp/pid1-qemu6-1775128407` +- validated local guest state included: + - `ready_marker=ready` + - `shepherd_pid=1` + - `shepherd_socket=present` + - `shepherd_status=running` + - `sshd_status=running` + - `boot_backend=qemu-uefi-tcg` + - `init_mode=shepherd-pid1` + +Current assessment: + +- Fruix now has a working local FreeBSD prototype where Shepherd itself is PID 1 +- this is not yet the new mainline boot path, but it proves that the project can move beyond the earlier `freebsd-init+rc.d-shepherd` bridge architecture +- the PID 1 process image appears as Guile because Shepherd is launched as a Guile script, but the decisive validation point is that: + - `/var/run/shepherd.pid` contains `1` +- this subphase was validated locally under QEMU/TCG + UEFI; the next meaningful test is the real XCP-ng VM + +Next recommended step: + +1. try the `shepherd-pid1` image on the real XCP-ng VM +2. if it boots there too, decide whether to keep `shepherd-pid1` as an experimental selectable boot mode or advance it further toward the main Fruix boot path +3. continue reducing the remaining Guile / Shepherd compatibility-prefix shims now that the broader `rc.d` boot-manager dependency has been locally bypassed diff --git a/docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md b/docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md new file mode 100644 index 0000000..32d7382 --- /dev/null +++ b/docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md @@ -0,0 +1,178 @@ +# Post-Phase-10: local Shepherd-as-PID-1 boot prototype on FreeBSD + +Date: 2026-04-02 + +## Goal + +Begin the next post-Phase-10 runtime-integration pass by exploring two related directions: + +- reduce reliance on the `freebsd-init + rc.d + shepherd` bridge, and +- compare Fruix's boot path with how Guix boots Shepherd as PID 1. + +The concrete goal for this subphase was not yet a full real-VM migration of the main boot track, but a first validated local prototype where the generated FreeBSD image boots with Shepherd as PID 1 under QEMU/UEFI. + +## Comparison with Guix + +Guix's system model treats Shepherd as a first-class root service. + +In `~/repos/guix/gnu/services/shepherd.scm`, the key pattern is: + +- `shepherd-root-service-type` extends the boot service graph +- `shepherd-boot-gexp` ultimately does an `execl` of Shepherd as PID 1 +- higher-level system services extend that root Shepherd instance declaratively + +In other words, Guix does not merely start Shepherd from a late init script; it composes the system boot graph around Shepherd directly. + +Fruix is not at that level of native service composition yet, but this subphase adopts the same basic architectural direction: + +- boot into a generated Shepherd config directly, +- let Shepherd own the service graph from PID 1, +- and keep the imperative compatibility/bootstrap logic as small as possible. + +## Implementation + +### 1. Added an explicit init-mode to the FreeBSD operating-system model + +`modules/fruix/system/freebsd.scm` now has an `init-mode` field on the declarative operating-system record. + +Supported values are currently: + +- `freebsd-init+rc.d-shepherd` +- `shepherd-pid1` + +The existing boot path remains the default. + +### 2. Added a generated PID 1 launcher for the `shepherd-pid1` mode + +For `shepherd-pid1`, the generated system now contains: + +- `boot/fruix-pid1` + +and the generated loader configuration adds: + +- `init_exec="/run/current-system/boot/fruix-pid1"` + +That means FreeBSD `init(8)` directly `exec`s the generated Fruix launcher as its very first action, replacing itself as PID 1. + +The launcher currently performs the minimum bootstrap steps needed before turning control over to Shepherd: + +- remount `/` read-write on this very-early path +- mount declared non-root filesystems such as: + - `devfs` on `/dev` + - `tmpfs` on `/tmp` +- set the hostname +- run `/run/current-system/activate` +- export the Guile/Shepherd runtime environment +- `exec` Shepherd directly + +Because Shepherd is a Guile script, the actual PID 1 process image is the Guile interpreter running Shepherd. The important validation point is that Shepherd's own pidfile records PID 1 and the service socket is owned by that process. + +### 3. Generated a different Shepherd config for PID 1 mode + +For `shepherd-pid1`, the generated `shepherd/init.scm` now includes the minimal helper procedures it needs inline, rather than importing the repo-side `(fruix shepherd freebsd)` module at runtime. + +This avoids depending on checkout-only Scheme modules being present in the guest. + +The PID 1 config currently starts a minimal service graph: + +- `fruix-logger` +- `netif` through FreeBSD `service(8)` +- `sshd` through FreeBSD `service(8)` +- `fruix-ready` + +So this prototype still uses some FreeBSD rc scripts as service implementations, but now under Shepherd control rather than under `/etc/rc` as the primary boot manager. + +### 4. Made activation more store-friendly for this early-boot path + +The generated activation script now treats: + +- `cap_mkdb /etc/login.conf` +- `pwd_mkdb -p /etc/master.passwd` + +as best-effort operations. + +That matters because Fruix currently symlinks these files from the immutable system closure, and on the very early PID 1 path they should not be allowed to abort the whole boot. + +## Validation + +### New PID 1 template + +Added: + +- `tests/system/phase11-shepherd-pid1-operating-system.scm.in` + +This declares the same minimal FreeBSD Fruix guest shape as the current Phase 9 system, but with: + +- `#:init-mode 'shepherd-pid1` + +### New local QEMU validation harness + +Added: + +- `tests/system/run-phase11-shepherd-pid1-qemu.sh` + +This harness: + +- builds the image through the canonical `fruix system image` path +- boots it locally with QEMU/TCG + UEFI +- injects the root SSH key +- waits for the ready marker over forwarded SSH +- verifies that Shepherd is running and that Shepherd's pidfile says PID 1 + +### Successful run + +Passing validation run: + +- `PASS phase11-shepherd-pid1-qemu` +- workdir: `/tmp/pid1-qemu6-1775128407` + +Key validated results: + +```text +ready_marker=ready +run_current_system_target=/frx/store/8b44506c37da85cebf265c813ed3a9d2a42408b077ac85854e7d6209d2f910ec-fruix-system-fruix-freebsd +shepherd_pid=1 +shepherd_socket=present +shepherd_status=running +sshd_status=running +pid1_command=[guile] +boot_backend=qemu-uefi-tcg +init_mode=shepherd-pid1 +``` + +The important detail is: + +- `shepherd_pid=1` + +which shows that the running Shepherd instance in the guest is the system's PID 1 process. + +## Assessment + +This is a meaningful architectural step beyond the earlier `rc.d` bridge milestone. + +Fruix now has a validated local boot path where: + +- the generated image boots on FreeBSD, +- the generated launcher becomes PID 1 via `init_exec`, +- Shepherd itself owns PID 1, +- networking and SSH come up under Shepherd-managed service ordering, +- and the ready marker still appears. + +## Remaining limitations + +This is still a prototype, not yet the replacement for the main boot path. + +Notable current limitations: + +- the PID 1 path still relies on a small generated shell launcher before entering Shepherd +- some early boot/runtime actions are still expressed imperatively there +- the Guile/Shepherd local-runtime compatibility-prefix shims are not eliminated yet; they remain part of activation for the currently locally built runtime artifacts +- this subphase validated the path locally under QEMU/TCG, not yet on the real XCP-ng VM + +## Recommended next step + +Use this validated local PID 1 prototype as the base for the next subphase: + +1. try the `shepherd-pid1` image on the real XCP-ng VM +2. if that succeeds, decide whether `shepherd-pid1` should become a selectable supported boot mode rather than just a prototype +3. continue reducing the remaining compatibility-prefix shims by moving the Guile/Shepherd runtime artifacts toward a more native store-aware arrangement diff --git a/modules/fruix/system/freebsd.scm b/modules/fruix/system/freebsd.scm index bc0c0fe..5f1e6ea 100644 --- a/modules/fruix/system/freebsd.scm +++ b/modules/fruix/system/freebsd.scm @@ -44,6 +44,7 @@ operating-system-services operating-system-loader-entries operating-system-rc-conf-entries + operating-system-init-mode operating-system-ready-marker operating-system-root-authorized-keys validate-operating-system @@ -97,7 +98,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 root-authorized-keys) + init-mode ready-marker root-authorized-keys) operating-system? (host-name operating-system-host-name) (kernel operating-system-kernel) @@ -109,6 +110,7 @@ (services operating-system-services) (loader-entries operating-system-loader-entries) (rc-conf-entries operating-system-rc-conf-entries) + (init-mode operating-system-init-mode) (ready-marker operating-system-ready-marker) (root-authorized-keys operating-system-root-authorized-keys)) @@ -155,11 +157,12 @@ (rc-conf-entries '(("clear_tmp_enable" . "YES") ("sendmail_enable" . "NONE") ("sshd_enable" . "NO"))) + (init-mode 'freebsd-init+rc.d-shepherd) (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 root-authorized-keys)) + init-mode ready-marker root-authorized-keys)) (define default-minimal-operating-system (operating-system)) @@ -364,7 +367,8 @@ (file-systems (operating-system-file-systems os)) (user-names (map user-account-name users)) (group-names (map user-group-name groups)) - (mount-points (map file-system-mount-point file-systems))) + (mount-points (map file-system-mount-point file-systems)) + (init-mode (operating-system-init-mode os))) (when (string-null? host-name) (error "operating-system host-name must not be empty")) (let ((dups (duplicate-elements user-names))) @@ -379,13 +383,24 @@ (error "operating-system must declare a root user")) (unless (member "wheel" group-names) (error "operating-system must declare a wheel group")) + (unless (member init-mode '(freebsd-init+rc.d-shepherd shepherd-pid1)) + (error "unsupported operating-system init-mode" init-mode)) #t)) -(define (render-loader-conf loader-entries) +(define (pid1-init-mode? os) + (eq? (operating-system-init-mode os) 'shepherd-pid1)) + +(define (effective-loader-entries os) + (append (if (pid1-init-mode? os) + '(("init_exec" . "/run/current-system/boot/fruix-pid1")) + '()) + (operating-system-loader-entries os))) + +(define (render-loader-conf os) (string-append (string-join (map (lambda (entry) (format #f "~a=\"~a\"" (car entry) (cdr entry))) - loader-entries) + (effective-loader-entries os)) "\n") "\n")) @@ -595,17 +610,122 @@ "set -eu\n" "mkdir -p /var/cron /var/db /var/lib/fruix /var/log /var/run /root /home /tmp\n" "chmod 1777 /tmp\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" + "if [ -x /usr/bin/cap_mkdb ] && [ -f /etc/login.conf ]; then /usr/bin/cap_mkdb /etc/login.conf || true; fi\n" + "if [ -x /usr/sbin/pwd_mkdb ] && [ -f /etc/master.passwd ]; then /usr/sbin/pwd_mkdb -p /etc/master.passwd || true; fi\n" home-setup compat-prefixes ssh-section))) +(define (pid1-mount-commands os) + (string-join + (filter-map (lambda (fs) + (and (not (string=? "/" (file-system-mount-point fs))) + (string-append + "mkdir -p '" (file-system-mount-point fs) "'\n" + "/sbin/mount -t '" (file-system-type fs) + "' -o '" (file-system-options fs) + "' '" (file-system-device fs) + "' '" (file-system-mount-point fs) + "' >/dev/null 2>&1 || true\n"))) + (operating-system-file-systems os)) + "")) + +(define (render-pid1-script os shepherd-store guile-store guile-extra-store) + (let ((ld-library-path (string-append guile-extra-store "/lib:" + guile-store "/lib:/usr/local/lib")) + (guile-system-path + (string-append guile-store "/share/guile/3.0:" + guile-store "/share/guile/site/3.0:" + guile-store "/share/guile/site:" + guile-store "/share/guile")) + (guile-load-path (string-append shepherd-store "/share/guile/site/3.0:" + guile-extra-store "/share/guile/site/3.0")) + (guile-system-compiled-path + (string-append guile-store "/lib/guile/3.0/ccache:" + guile-store "/lib/guile/3.0/site-ccache")) + (guile-load-compiled-path + (string-append shepherd-store "/lib/guile/3.0/site-ccache:" + guile-extra-store "/lib/guile/3.0/site-ccache")) + (guile-system-extensions-path (string-append guile-store "/lib/guile/3.0/extensions")) + (guile-extensions-path (string-append guile-extra-store "/lib/guile/3.0/extensions"))) + (string-append + "#!/bin/sh\n" + "set -eu\n" + "PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\n" + "/sbin/mount -u -o rw / >/dev/null 2>&1 || true\n" + (pid1-mount-commands os) + "/bin/hostname '" (operating-system-host-name os) "' >/dev/null 2>&1 || true\n" + "/run/current-system/activate\n" + "export GUILE_AUTO_COMPILE=0\n" + "export LANG='C.UTF-8'\n" + "export LC_ALL='C.UTF-8'\n" + "export LD_LIBRARY_PATH='" ld-library-path "'\n" + "export GUILE_SYSTEM_PATH='" guile-system-path "'\n" + "export GUILE_LOAD_PATH='" guile-load-path "'\n" + "export GUILE_SYSTEM_COMPILED_PATH='" guile-system-compiled-path "'\n" + "export GUILE_LOAD_COMPILED_PATH='" guile-load-compiled-path "'\n" + "export GUILE_SYSTEM_EXTENSIONS_PATH='" guile-system-extensions-path "'\n" + "export GUILE_EXTENSIONS_PATH='" guile-extensions-path "'\n" + "exec " guile-store "/bin/guile --no-auto-compile " shepherd-store "/bin/shepherd -I -s /var/run/shepherd.sock -c /run/current-system/shepherd/init.scm --pid=/var/run/shepherd.pid -l /var/log/shepherd.log\n"))) + (define (render-shepherd-config os) - (let ((ready-marker (operating-system-ready-marker os))) + (let* ((ready-marker (operating-system-ready-marker os)) + (pid1? (pid1-init-mode? os)) + (start-sshd? (and pid1? (or (sshd-enabled? os) + (member 'sshd (operating-system-services os))))) + (ready-requirements (if start-sshd? + "'(fruix-logger sshd)" + "'(fruix-logger)")) + (pid1-helpers + (if pid1? + (string-append + "(define (run-command program . args)\n" + " (let ((status (apply system* program args)))\n" + " (unless (zero? status)\n" + " (error \"command failed\" (cons program args) status))\n" + " #t))\n\n" + "(define* (freebsd-rc-service provision script-name\n" + " #:key\n" + " (requirement '())\n" + " (documentation\n" + " \"Manage a FreeBSD rc.d service through 'service'.\"))\n" + " (service provision\n" + " #:documentation documentation\n" + " #:requirement requirement\n" + " #:start (lambda _\n" + " (run-command \"/usr/sbin/service\" script-name \"onestart\")\n" + " #t)\n" + " #:stop (lambda _\n" + " (run-command \"/usr/sbin/service\" script-name \"onestop\")\n" + " #f)\n" + " #:respawn? #f))\n\n") + "")) + (pid1-services + (if pid1? + (string-append + (if start-sshd? + " (freebsd-rc-service '(netif) \"netif\"\n" + "") + (if start-sshd? + " #:requirement '(fruix-logger)\n" + "") + (if start-sshd? + " #:documentation \"Bring up FreeBSD networking from rc.conf.\")\n" + "") + (if start-sshd? + " (freebsd-rc-service '(sshd) \"sshd\"\n" + "") + (if start-sshd? + " #:requirement '(netif)\n" + "") + (if start-sshd? + " #:documentation \"Start OpenSSH under Shepherd PID 1.\")\n" + "")) + ""))) (string-append "(use-modules (shepherd service)\n" - " (ice-9 ftw))\n\n" + " (ice-9 ftw)\n" + " (ice-9 popen))\n\n" "(define ready-marker \"" ready-marker "\")\n\n" "(define (mkdir-p* dir)\n" " (unless (or (string=? dir \"\")\n" @@ -615,6 +735,7 @@ " (mkdir dir)))\n\n" "(define (ensure-parent-directory file)\n" " (mkdir-p* (dirname file)))\n\n" + pid1-helpers "(register-services\n" " (list\n" " (service '(fruix-logger)\n" @@ -627,9 +748,10 @@ " #t)\n" " #:stop (lambda _ #f)\n" " #:respawn? #f)\n" + pid1-services " (service '(fruix-ready)\n" " #:documentation \"Write the Fruix ready marker.\"\n" - " #:requirement '(fruix-logger)\n" + " #:requirement " ready-requirements "\n" " #:start (lambda _\n" " (ensure-parent-directory ready-marker)\n" " (call-with-output-file ready-marker\n" @@ -739,9 +861,34 @@ "load_rc_config $name\n" "run_rc_command \"$1\"\n"))) +(define (operating-system-generated-file-names os) + (append + '("boot/loader.conf" + "etc/rc.conf" + "etc/fstab" + "etc/hosts" + "etc/passwd" + "etc/master.passwd" + "etc/group" + "etc/login.conf" + "etc/shells" + "etc/motd" + "etc/ttys" + "activate" + "shepherd/init.scm") + (if (pid1-init-mode? os) + '("boot/fruix-pid1") + '()) + (if (sshd-enabled? os) + '("etc/ssh/sshd_config") + '()) + (if (null? (operating-system-root-authorized-keys os)) + '() + '("root/.ssh/authorized_keys")))) + (define* (operating-system-generated-files os #:key guile-store guile-extra-store shepherd-store) (append - `(("boot/loader.conf" . ,(render-loader-conf (operating-system-loader-entries os))) + `(("boot/loader.conf" . ,(render-loader-conf os)) ("etc/rc.conf" . ,(render-rc.conf os)) ("etc/fstab" . ,(render-fstab os)) ("etc/hosts" . ,(render-hosts os)) @@ -757,6 +904,9 @@ #:guile-extra-store guile-extra-store #:shepherd-store shepherd-store)) ("shepherd/init.scm" . ,(render-shepherd-config os))) + (if (pid1-init-mode? os) + `(("boot/fruix-pid1" . ,(render-pid1-script os shepherd-store guile-store guile-extra-store))) + '()) (if (sshd-enabled? os) `(("etc/ssh/sshd_config" . ,(render-sshd-config os))) '()) @@ -784,8 +934,8 @@ (needed-for-boot? . ,(file-system-needed-for-boot? fs)))) (operating-system-file-systems os))) (services . ,(operating-system-services os)) - (generated-files . ,(map car (operating-system-generated-files os))) - (init-mode . freebsd-init+rc.d-shepherd) + (generated-files . ,(operating-system-generated-file-names os)) + (init-mode . ,(operating-system-init-mode os)) (ready-marker . ,(operating-system-ready-marker os)))) (define (same-file-contents? a b) @@ -905,6 +1055,8 @@ (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) + (when (file-exists? (string-append closure-path "/boot/fruix-pid1")) + (chmod (string-append closure-path "/boot/fruix-pid1") #o555)) (write-file (string-append closure-path "/parameters.scm") (object->string (operating-system-closure-spec os))) (write-file (string-append closure-path "/.references") @@ -1007,7 +1159,7 @@ (efi-partition-label . ,efi-partition-label) (root-partition-label . ,root-partition-label) (serial-console . ,serial-console) - (init-mode . freebsd-init+rc.d-shepherd))) + (init-mode . ,(operating-system-init-mode os)))) (define (path-basename path) (let ((parts (filter (lambda (part) (not (string-null? part))) diff --git a/tests/system/phase11-shepherd-pid1-operating-system.scm.in b/tests/system/phase11-shepherd-pid1-operating-system.scm.in new file mode 100644 index 0000000..65ca18c --- /dev/null +++ b/tests/system/phase11-shepherd-pid1-operating-system.scm.in @@ -0,0 +1,76 @@ +(use-modules (fruix system freebsd) + (fruix packages freebsd)) + +(define phase11-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_vtnet0" . "SYNCDHCP")) + #:init-mode 'shepherd-pid1 + #:ready-marker "/var/lib/fruix/ready" + #:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__"))) diff --git a/tests/system/run-phase11-shepherd-pid1-qemu.sh b/tests/system/run-phase11-shepherd-pid1-qemu.sh new file mode 100755 index 0000000..40c2648 --- /dev/null +++ b/tests/system/run-phase11-shepherd-pid1-qemu.sh @@ -0,0 +1,170 @@ +#!/bin/sh +set -eu + +repo_root=${PROJECT_ROOT:-$(pwd)} +os_template=$repo_root/tests/system/phase11-shepherd-pid1-operating-system.scm.in +system_name=${SYSTEM_NAME:-phase11-operating-system} +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} +ssh_port=${QEMU_SSH_PORT:-10022} +disk_capacity=${DISK_CAPACITY:-5g} +cleanup=0 + +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase11-pid1-qemu.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +phase11_os_file=$workdir/phase11-shepherd-pid1-operating-system.scm +phase8_log=$workdir/phase8-system-image.log +phase8_metadata=$workdir/phase8-system-image-metadata.txt +serial_log=$workdir/serial.log +qemu_pidfile=$workdir/qemu.pid +metadata_file=$workdir/phase11-shepherd-pid1-qemu-metadata.txt +uefi_vars=$workdir/QEMU_UEFI_VARS.fd + +cleanup_workdir() { + if [ -f "$qemu_pidfile" ]; then + sudo kill "$(sudo cat "$qemu_pidfile")" >/dev/null 2>&1 || true + fi + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" 2>/dev/null || sudo 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 +} +[ -f "$root_ssh_private_key_file" ] || { + echo "missing root SSH private key file: $root_ssh_private_key_file" >&2 + exit 1 +} +command -v qemu-system-x86_64 >/dev/null 2>&1 || { + echo "qemu-system-x86_64 is required" >&2 + exit 1 +} +[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || { + echo "missing QEMU UEFI firmware" >&2 + exit 1 +} +cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$uefi_vars" +root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") +sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$os_template" > "$phase11_os_file" + +KEEP_WORKDIR=1 WORKDIR="$workdir/phase8-build" OS_FILE="$phase11_os_file" SYSTEM_NAME="$system_name" 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") + +sudo qemu-system-x86_64 \ + -machine q35,accel=tcg \ + -cpu max \ + -m 2048 \ + -smp 2 \ + -display none \ + -serial "file:$serial_log" \ + -monitor none \ + -pidfile "$qemu_pidfile" \ + -daemonize \ + -drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \ + -drive if=pflash,format=raw,file="$uefi_vars" \ + -drive if=virtio,format=raw,file="$disk_image" \ + -netdev user,id=net0,hostfwd=tcp::${ssh_port}-:22 \ + -device virtio-net-pci,netdev=net0 + +ssh_guest() { + ssh -p "$ssh_port" -i "$root_ssh_private_key_file" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=5 \ + root@127.0.0.1 "$@" +} + +for attempt in $(jot 120 1 120); do + if ssh_guest 'test -f /var/lib/fruix/ready' >/dev/null 2>&1; then + break + fi + sleep 2 +done + +ready_marker=$(ssh_guest 'cat /var/lib/fruix/ready') +run_current_system_target=$(ssh_guest 'readlink /run/current-system') +pid1_command=$(ssh_guest 'ps -p 1 -o command= | sed "s/^ *//"') +pid1_binary=$(ssh_guest 'procstat -b 1 2>/dev/null | awk "NR==2 {print \$2}"') +shepherd_pid=$(ssh_guest 'cat /var/run/shepherd.pid') +shepherd_socket=$(ssh_guest 'test -S /var/run/shepherd.sock && echo present || echo missing') +shepherd_status=$(ssh_guest 'test -f /var/run/shepherd.pid && kill -0 "$(cat /var/run/shepherd.pid)" >/dev/null 2>&1 && echo running || echo stopped') +sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped') +logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ') +uname_output=$(ssh_guest 'uname -sr') +operator_home_listing=$(ssh_guest 'ls -d /home/operator') + +[ "$ready_marker" = ready ] || { echo "unexpected ready marker contents: $ready_marker" >&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 +} +[ "$shepherd_pid" = 1 ] || { + echo "shepherd is not PID 1: shepherd.pid=$shepherd_pid pid1_command=$pid1_command pid1_binary=$pid1_binary" >&2 + exit 1 +} +[ "$shepherd_socket" = present ] || { echo "shepherd socket is missing" >&2; exit 1; } +[ "$shepherd_status" = running ] || { echo "shepherd is not running" >&2; exit 1; } +[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; } +[ "$operator_home_listing" = /home/operator ] || { echo "operator home missing" >&2; exit 1; } + +cat >"$metadata_file" <