Prototype Shepherd PID 1 boot on FreeBSD
This commit is contained in:
@@ -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
|
||||
|
||||
178
docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md
Normal file
178
docs/reports/postphase10-shepherd-pid1-qemu-freebsd.md
Normal 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
|
||||
@@ -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)))
|
||||
|
||||
76
tests/system/phase11-shepherd-pid1-operating-system.scm.in
Normal file
76
tests/system/phase11-shepherd-pid1-operating-system.scm.in
Normal 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__")))
|
||||
170
tests/system/run-phase11-shepherd-pid1-qemu.sh
Executable file
170
tests/system/run-phase11-shepherd-pid1-qemu.sh
Executable 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"
|
||||
Reference in New Issue
Block a user