Prototype Shepherd PID 1 boot on FreeBSD

This commit is contained in:
2026-04-02 13:29:17 +02:00
parent c62c89b078
commit f5ffd111ee
5 changed files with 655 additions and 14 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 <operating-system>
(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)))

View File

@@ -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__")))

View File

@@ -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" <<EOF
workdir=$workdir
phase11_os_file=$phase11_os_file
phase8_log=$phase8_log
phase8_metadata=$phase8_metadata
image_store_path=$image_store_path
disk_image=$disk_image
closure_path=$closure_path
closure_base=$closure_base
raw_sha256=$raw_sha256
serial_log=$serial_log
qemu_pidfile=$qemu_pidfile
ssh_port=$ssh_port
ready_marker=$ready_marker
run_current_system_target=$run_current_system_target
pid1_command=$pid1_command
pid1_binary=$pid1_binary
shepherd_pid=$shepherd_pid
shepherd_socket=$shepherd_socket
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
uname_output=$uname_output
operator_home_listing=$operator_home_listing
boot_backend=qemu-uefi-tcg
init_mode=shepherd-pid1
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase11-shepherd-pid1-qemu\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"