Enable Fruix FreeBSD guest SSH boot on XCP-ng

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

View File

@@ -2154,3 +2154,66 @@ Next recommended step:
- Fruix at the product boundary
- `/frx` as the canonical store root
- stable upstream-derived internal names unless there is strong architectural value in renaming them
## 2026-04-02 — Phase 9 checkpoint: XCP-ng guest reached DHCP and SSH
Completed work:
- added a dedicated Phase 9 XCP-ng operating-system template:
- `tests/system/phase9-minimal-operating-system.scm.in`
- added an XCP-ng boot/import/validation harness:
- `tests/system/run-phase9-xcpng-boot.sh`
- extended the staged FreeBSD runtime and system-generation layers so the guest can complete enough of real boot for network access:
- `modules/fruix/packages/freebsd.scm`
- `modules/fruix/system/freebsd.scm`
- updated the integrated image-generation path for Phase 9 use cases:
- `tests/system/materialize-phase8-system-image.scm`
- `tests/system/run-phase8-system-image.sh`
- wrote the checkpoint report:
- `docs/reports/phase9-xcpng-ssh-boot-freebsd.md`
Important findings:
- a decisive local QEMU/TCG serial-boot pass exposed the first real early-boot blocker:
- the generated `fstab` was wrong for pseudo-filesystems, so `rc` tried to fsck `devfs` and aborted boot
- after fixing `fstab`, later serial logs exposed additional FreeBSD base runtime gaps that only appear during real boot, including missing commands, runtime directories, and base config files used by `rc`, DHCP, logging, and service startup
- the staged image now includes the minimum currently known set of FreeBSD runtime pieces needed to:
- run `rc`
- obtain DHCP
- generate SSH host keys
- start `sshd`
- public-key SSH login initially still failed because the minimal guest did not stage a complete PAM runtime/config path; for the current Phase 9 prototype track, the generated `sshd_config` now uses:
- `UsePAM no`
- the current XCP-ng validation path succeeded against the operator-approved VM and existing VDI only:
- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
- the successful XCP-ng boot obtained:
- guest IP `192.168.213.62`
- successful SSH validation on the real guest confirmed:
- `hostname=fruix-freebsd`
- `sshd` is reachable with the injected root key
- networking is configured on the Xen NIC
Current assessment:
- this checkpoint establishes the first real network-reachable Fruix boot on the active FreeBSD/XCP-ng track
- the generated image now boots far enough for DHCP and SSH, which closes the earlier uncertainty about whether the Phase 8 image could become a remotely usable guest at all
- Phase 9 is still not complete because the Fruix-specific readiness path remains blocked:
- `fruix-shepherd` does not start
- `/var/lib/fruix/ready` is still missing
- Guile still crashes in the guest with `signal 11`
- therefore the current state is:
- kernel boot: yes
- root mount: yes
- DHCP: yes
- SSH: yes
- Shepherd/ready marker: not yet
Next recommended step:
1. continue the in-guest Guile crash investigation so `fruix-shepherd` can start on the booted guest
2. once Shepherd is stable, rerun `tests/system/run-phase9-xcpng-boot.sh` to validate the full ready-marker path end-to-end
3. then close Phase 9 with updated report/progress entries for:
- deterministic boot readiness
- in-guest Shepherd validation
- minimal operator usability

View File

@@ -0,0 +1,213 @@
# Phase 9 checkpoint: XCP-ng boot reached DHCP and SSH on FreeBSD
Date: 2026-04-02
## Goal
Advance Phase 9 from a static image-generation milestone to a real booted Fruix guest on the active FreeBSD/XCP-ng track, using the operator-approved VM:
- VM: `90490f2e-e8fc-4b7a-388e-5c26f0157289`
- existing target VDI: `0f1f90d3-48ca-4fa2-91d8-fc6339b95743`
The immediate objective for this checkpoint was narrower than full Phase 9 completion:
- boot the generated image under XCP-ng,
- obtain DHCP,
- and reach SSH access with the injected root key.
## Summary
This checkpoint succeeded.
The current Fruix FreeBSD image now:
- boots on the target XCP-ng VM,
- mounts the generated root filesystem,
- completes enough of FreeBSD `rc` startup to configure networking,
- obtains a DHCP lease on the Xen NIC,
- starts `sshd`,
- and accepts root public-key authentication over the network.
Validated guest details from the successful XCP-ng boot:
- guest IP: `192.168.213.62`
- hostname: `fruix-freebsd`
- kernel string:
- `FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64`
Representative successful SSH validation output:
```text
FreeBSD fruix-freebsd 15.0-STABLE FreeBSD 15.0-STABLE stable/15-n282801-29dce45d8c50 GENERIC amd64
fruix-freebsd
192.168.213.62
```
Successful XCP-ng work directory:
- `/tmp/phase9-xcpng-ssh-1775097470`
## Important boot/debugging findings
The first decisive breakthrough came from running the generated image locally under QEMU/TCG with serial capture. That made the previously opaque early-boot failure visible.
### 1. The original early boot abort was not an XCP-ng image-format problem anymore
After the earlier switch from raw uploads to dynamic VHD uploads, the remaining boot failure was inside the guest boot process, not in the XO import path.
### 2. FreeBSD `fstab` handling for pseudo-filesystems was wrong
The serial log showed that boot aborted during filesystem checks because the generated `fstab` gave non-zero fsck fields to non-UFS mounts such as `devfs`.
Representative failure:
```text
Starting file system checks:
/dev/gpt/fruix-root: FILE SYSTEM CLEAN; SKIPPING CHECKS
/dev/gpt/fruix-root: clean, ...
fsck: exec fsck_devfs for devfs in /sbin:/usr/sbin: No such file or directory
Unknown error 1; help!
ERROR: ABORTING BOOT (sending SIGTERM to parent)!
```
The fix was to generate fsck pass fields only for UFS entries and emit `0 0` for pseudo-filesystems.
### 3. The minimal image was still missing many base files and commands expected by `rc`
Once `rc` ran further, QEMU serial logs exposed a long tail of missing runtime pieces that had not been visible from the earlier static validations alone.
Examples included:
- missing base commands:
- `dd`
- `expr`
- `rmdir`
- `sort`
- `mktemp`
- `egrep`
- `fsync`
- `kldload`
- `kldstat`
- `devfs`
- `devctl`
- `newsyslog`
- `ip6addrctl`
- missing base config files:
- `/etc/network.subr`
- `/etc/devd.conf`
- `/etc/newsyslog.conf`
- `/etc/syslog.conf`
- missing runtime directories:
- `/var/db`
- `/var/cron`
- missing libraries needed by later boot helpers:
- `libgeom.so.5`
- `libdevctl.so.5`
- `libcap_net.so.1`
- C++ runtime pieces used by `devd`
These were staged into the current FreeBSD package layer and linked into the generated rootfs.
### 4. SSH auth initially failed because the image relied on PAM without a complete PAM runtime/configuration
`sshd` would start, but root public-key authentication still failed. A direct in-guest debug run showed:
```text
PAM: initialisation failed
```
For the minimal Phase 9 guest, the practical fix was to make the generated `sshd_config` use:
- `UsePAM no`
while still keeping key-only login enabled.
That was sufficient to unlock real SSH access on both the local QEMU debug guest and the XCP-ng guest.
## Current code-level outcomes
The current checkpoint work materially expanded the minimal FreeBSD runtime staged into Fruix images.
Highlights:
- `modules/fruix/packages/freebsd.scm`
- added dedicated runtime packages for:
- `freebsd-networking`
- `freebsd-openssh`
- expanded staged base runtime coverage substantially for `rc`, networking, and SSH
- added required config files and shared libraries used during real boot
- `modules/fruix/system/freebsd.scm`
- added root authorized-key support to the operating-system model
- generated static account databases and supporting files:
- `/etc/passwd`
- `/etc/master.passwd`
- `/etc/group`
- `/etc/login.conf`
- `/etc/ttys`
- activation now runs:
- `cap_mkdb`
- `pwd_mkdb`
- activation creates required directories and SSH host keys
- generated `sshd_config` now disables PAM for the current minimal key-only Phase 9 path
- `fstab` generation now avoids fsck pass numbers for pseudo-filesystems
- rootfs generation now links the additional `/etc` files needed by real boot
- `tests/system/phase9-minimal-operating-system.scm.in`
- enables DHCP on the relevant NIC names for the current tracks:
- `xn0`
- `em0`
- `vtnet0`
- injects the root authorized key
- includes the SSH/network runtime packages and required system users/groups
- `tests/system/run-phase8-system-image.sh`
- now accepts `OS_FILE`
- now accepts/passes `DISK_CAPACITY`
- serial-console validation was relaxed from an exact loader string to a `comconsole` presence check
## Verified current state
The current validated Phase 9 state is:
- XCP-ng VHD upload path works against the existing VDI
- the guest boots far enough for normal `rc` networking and `sshd`
- DHCP works on the Xen NIC
- SSH key injection works
- root login over SSH works
This means the project has crossed an important Phase 9 boundary:
- the first boot validation no longer depends on local bhyve serial automation,
- and the real XCP-ng target can now be exercised over the network.
## Remaining blocker
Phase 9 is not complete yet because the Fruix-specific readiness path still fails.
Current remaining blocker:
- Guile still crashes in the guest
- therefore `fruix-shepherd` does not start
- therefore `/var/lib/fruix/ready` is still absent
Representative guest evidence:
```text
pid 262 (guile), jid 0, uid 0: exited on signal 11 (core dumped)
```
Over SSH on the real XCP-ng guest:
- `sshd` is running
- DHCP is active
- `fruix-shepherd` is stopped
- `/var/lib/fruix/ready` is missing
A retrieved core dump and local `lldb` analysis show the Guile crash occurs extremely early during initialization, in the locale/string conversion path while building Guile load/build info. This remains the next debugging target.
## Assessment
This checkpoint satisfies a meaningful Phase 9 intermediate milestone on the active FreeBSD/XCP-ng track:
- the generated Fruix image now boots as a network-reachable FreeBSD guest,
- and minimal operator access via SSH is working.
However, the full Fruix boot milestone is still blocked by in-guest Guile/Shepherd failure, so the overall Phase 9 milestone remains open.

View File

@@ -18,6 +18,8 @@
freebsd-bootloader
freebsd-rc-scripts
freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-clang-toolchain
freebsd-gmake
@@ -95,9 +97,16 @@ and the userland C headers needed for development profiles."
#:install-plan
'((file "/lib/libc.so.7" "lib/libc.so.7")
(file "/lib/libsys.so.7" "lib/libsys.so.7")
(file "/lib/libthr.so.3" "lib/libthr.so.3")
(file "/lib/libutil.so.10" "lib/libutil.so.10")
(file "/lib/libxo.so.0" "lib/libxo.so.0")
(file "/lib/libgeom.so.5" "lib/libgeom.so.5")
(file "/lib/libc++.so.1" "lib/libc++.so.1")
(file "/lib/libcxxrt.so.1" "lib/libcxxrt.so.1")
(file "/lib/libgcc_s.so.1" "lib/libgcc_s.so.1")
(file "/lib/libm.so.5" "lib/libm.so.5")
(file "/lib/libelf.so.2" "lib/libelf.so.2")
(file "/lib/libkvm.so.7" "lib/libkvm.so.7")
(file "/lib/lib80211.so.1" "lib/lib80211.so.1")
(file "/lib/libjail.so.1" "lib/libjail.so.1")
(file "/lib/libnv.so.1" "lib/libnv.so.1")
@@ -105,6 +114,31 @@ and the userland C headers needed for development profiles."
(file "/lib/libbsdxml.so.4" "lib/libbsdxml.so.4")
(file "/lib/libcrypt.so.5" "lib/libcrypt.so.5")
(file "/lib/libmd.so.7" "lib/libmd.so.7")
(file "/lib/libedit.so.8" "lib/libedit.so.8")
(file "/lib/libtinfow.so.9" "lib/libtinfow.so.9")
(file "/lib/libcasper.so.1" "lib/libcasper.so.1")
(file "/lib/libcap_syslog.so.1" "lib/libcap_syslog.so.1")
(file "/lib/libcap_fileargs.so.1" "lib/libcap_fileargs.so.1")
(file "/lib/libcap_net.so.1" "lib/libcap_net.so.1")
(file "/lib/libufs.so.8" "lib/libufs.so.8")
(file "/usr/lib/libdevinfo.so.7" "usr/lib/libdevinfo.so.7")
(file "/usr/lib/libdevctl.so.5" "usr/lib/libdevctl.so.5")
(file "/lib/libz.so.6" "lib/libz.so.6")
(file "/lib/libcrypto.so.35" "lib/libcrypto.so.35")
(file "/usr/lib/libssl.so.35" "usr/lib/libssl.so.35")
(file "/usr/lib/libdl.so.1" "usr/lib/libdl.so.1")
(file "/usr/lib/libpam.so.6" "usr/lib/libpam.so.6")
(file "/usr/lib/libbsm.so.3" "usr/lib/libbsm.so.3")
(file "/usr/lib/libblocklist.so.0" "usr/lib/libblocklist.so.0")
(file "/usr/lib/libregex.so.1" "usr/lib/libregex.so.1")
(file "/usr/lib/libprivatessh.so.5" "usr/lib/libprivatessh.so.5")
(file "/usr/lib/libprivateldns.so.5" "usr/lib/libprivateldns.so.5")
(file "/usr/lib/libwrap.so.6" "usr/lib/libwrap.so.6")
(file "/usr/lib/libgssapi_krb5.so.122" "usr/lib/libgssapi_krb5.so.122")
(file "/usr/lib/libkrb5.so.122" "usr/lib/libkrb5.so.122")
(file "/usr/lib/libk5crypto.so.122" "usr/lib/libk5crypto.so.122")
(file "/usr/lib/libcom_err.so.122" "usr/lib/libcom_err.so.122")
(file "/usr/lib/libkrb5support.so.122" "usr/lib/libkrb5support.so.122")
(file "/libexec/ld-elf.so.1" "libexec/ld-elf.so.1"))))
(define freebsd-bootloader
@@ -155,16 +189,45 @@ userland commands needed for development and build experiments."
#:license 'bsd-2
#:install-plan
'((file "/bin/cat" "bin/cat")
(file "/bin/chflags" "bin/chflags")
(file "/bin/chmod" "bin/chmod")
(file "/bin/cp" "bin/cp")
(file "/bin/date" "bin/date")
(file "/bin/dd" "bin/dd")
(file "/bin/echo" "bin/echo")
(file "/bin/expr" "bin/expr")
(file "/bin/ln" "bin/ln")
(file "/bin/ls" "bin/ls")
(file "/bin/mkdir" "bin/mkdir")
(file "/bin/mv" "bin/mv")
(file "/bin/ps" "bin/ps")
(file "/bin/pwd" "bin/pwd")
(file "/bin/rmdir" "bin/rmdir")
(file "/bin/rm" "bin/rm")
(file "/bin/sleep" "bin/sleep")
(file "/bin/stty" "bin/stty")
(file "/bin/sync" "bin/sync")
(file "/usr/bin/awk" "usr/bin/awk")
(file "/usr/bin/basename" "usr/bin/basename")
(file "/usr/bin/cap_mkdb" "usr/bin/cap_mkdb")
(file "/usr/bin/cut" "usr/bin/cut")
(file "/usr/bin/dirname" "usr/bin/dirname")
(file "/usr/bin/egrep" "usr/bin/egrep")
(file "/usr/bin/env" "usr/bin/env")
(file "/usr/bin/find" "bin/find")
(file "/usr/bin/fsync" "usr/bin/fsync")
(file "/usr/bin/grep" "usr/bin/grep")
(file "/usr/bin/mktemp" "usr/bin/mktemp")
(file "/usr/bin/head" "usr/bin/head")
(file "/usr/bin/install" "usr/bin/install")
(file "/usr/bin/limits" "usr/bin/limits")
(file "/usr/bin/logger" "usr/bin/logger")
(file "/usr/bin/readlink" "usr/bin/readlink")
(file "/usr/bin/sed" "usr/bin/sed")
(file "/usr/bin/sort" "usr/bin/sort")
(file "/usr/bin/tar" "bin/tar")
(file "/usr/bin/tr" "usr/bin/tr")
(file "/usr/bin/uname" "usr/bin/uname")
(file "/usr/bin/xargs" "bin/xargs"))))
(define freebsd-rc-scripts
@@ -183,6 +246,10 @@ files needed by the first Fruix system-closure experiments."
'((file "/etc/rc" "etc/rc")
(file "/etc/rc.subr" "etc/rc.subr")
(file "/etc/rc.shutdown" "etc/rc.shutdown")
(file "/etc/devd.conf" "etc/devd.conf")
(file "/etc/network.subr" "etc/network.subr")
(file "/etc/newsyslog.conf" "etc/newsyslog.conf")
(file "/etc/syslog.conf" "etc/syslog.conf")
(directory "/etc/rc.d" "etc/rc.d")
(directory "/etc/defaults" "etc/defaults"))))
@@ -200,17 +267,81 @@ commands needed by the first declarative Fruix system and activation payload
experiments."
#:license 'bsd-2
#:install-plan
'((file "/sbin/init" "sbin/init")
(file "/sbin/mount" "sbin/mount")
'((file "/sbin/adjkerntz" "sbin/adjkerntz")
(file "/sbin/devd" "sbin/devd")
(file "/sbin/devmatch" "sbin/devmatch")
(file "/sbin/dmesg" "sbin/dmesg")
(file "/sbin/fsck" "sbin/fsck")
(file "/sbin/fsck_ufs" "sbin/fsck_ufs")
(file "/sbin/gpart" "sbin/gpart")
(file "/sbin/init" "sbin/init")
(file "/sbin/ifconfig" "sbin/ifconfig")
(file "/sbin/md5" "sbin/md5")
(file "/sbin/mount" "sbin/mount")
(file "/sbin/rcorder" "sbin/rcorder")
(file "/sbin/reboot" "sbin/reboot")
(file "/sbin/sha256" "sbin/sha256")
(file "/sbin/shutdown" "sbin/shutdown")
(file "/sbin/swapon" "sbin/swapon")
(file "/sbin/sysctl" "sbin/sysctl")
(file "/usr/sbin/chown" "usr/sbin/chown")
(file "/usr/sbin/cron" "usr/sbin/cron")
(file "/usr/sbin/devctl" "usr/sbin/devctl")
(file "/usr/sbin/nologin" "usr/sbin/nologin")
(file "/usr/sbin/pwd_mkdb" "usr/sbin/pwd_mkdb")
(file "/usr/sbin/service" "usr/sbin/service")
(file "/usr/sbin/pw" "usr/sbin/pw")
(file "/usr/sbin/ip6addrctl" "usr/sbin/ip6addrctl")
(file "/usr/sbin/newsyslog" "usr/sbin/newsyslog")
(file "/usr/sbin/syslogd" "usr/sbin/syslogd")
(file "/usr/sbin/utx" "usr/sbin/utx")
(file "/usr/bin/id" "usr/bin/id")
(file "/sbin/kldload" "sbin/kldload")
(file "/sbin/kldstat" "sbin/kldstat")
(file "/sbin/devfs" "sbin/devfs")
(file "/bin/freebsd-version" "bin/freebsd-version")
(file "/bin/hostname" "bin/hostname")
(file "/bin/kenv" "bin/kenv"))))
(define freebsd-networking
(freebsd-package
#:name "freebsd-networking"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc freebsd-runtime freebsd-sh)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for FreeBSD network runtime tools"
#:description
"Prototype package definition that stages the minimal FreeBSD networking
runtime needed for DHCP-based boot validation in virtual machines."
#:license 'bsd-2
#:install-plan
'((file "/sbin/dhclient" "sbin/dhclient")
(file "/sbin/dhclient-script" "sbin/dhclient-script")
(file "/sbin/route" "sbin/route")
(file "/usr/bin/netstat" "usr/bin/netstat")
(file "/usr/sbin/arp" "usr/sbin/arp"))))
(define freebsd-openssh
(freebsd-package
#:name "freebsd-openssh"
#:version freebsd-release
#:build-system 'copy-build-system
#:inputs (list freebsd-libc freebsd-runtime freebsd-sh)
#:home-page "https://www.freebsd.org/"
#:synopsis "Prototype package for the FreeBSD OpenSSH runtime"
#:description
"Prototype package definition that stages the FreeBSD OpenSSH server and
client tools needed for first-boot operator access on the Fruix prototype
track."
#:license 'bsd-2
#:install-plan
'((file "/usr/sbin/sshd" "usr/sbin/sshd")
(file "/usr/bin/ssh" "usr/bin/ssh")
(file "/usr/bin/ssh-keygen" "usr/bin/ssh-keygen")
(file "/usr/libexec/sshd-auth" "usr/libexec/sshd-auth")
(file "/usr/libexec/sshd-session" "usr/libexec/sshd-session")
(file "/etc/ssh/moduli" "etc/ssh/moduli"))))
(define freebsd-clang-toolchain
(freebsd-package
#:name "freebsd-clang-toolchain"
@@ -354,6 +485,8 @@ library for profile experiments."
freebsd-libc
freebsd-rc-scripts
freebsd-runtime
freebsd-networking
freebsd-openssh
freebsd-userland
freebsd-sh
freebsd-bash))

View File

@@ -45,6 +45,7 @@
operating-system-loader-entries
operating-system-rc-conf-entries
operating-system-ready-marker
operating-system-root-authorized-keys
validate-operating-system
operating-system-closure-spec
operating-system-image-spec
@@ -96,7 +97,7 @@
(define-record-type <operating-system>
(make-operating-system host-name kernel bootloader base-packages users groups
file-systems services loader-entries rc-conf-entries
ready-marker)
ready-marker root-authorized-keys)
operating-system?
(host-name operating-system-host-name)
(kernel operating-system-kernel)
@@ -108,7 +109,8 @@
(services operating-system-services)
(loader-entries operating-system-loader-entries)
(rc-conf-entries operating-system-rc-conf-entries)
(ready-marker operating-system-ready-marker))
(ready-marker operating-system-ready-marker)
(root-authorized-keys operating-system-root-authorized-keys))
(define* (operating-system #:key
(host-name "fruix-freebsd")
@@ -153,10 +155,11 @@
(rc-conf-entries '(("clear_tmp_enable" . "YES")
("sendmail_enable" . "NONE")
("sshd_enable" . "NO")))
(ready-marker "/var/lib/fruix/ready"))
(ready-marker "/var/lib/fruix/ready")
(root-authorized-keys '()))
(make-operating-system host-name kernel bootloader base-packages users groups
file-systems services loader-entries rc-conf-entries
ready-marker))
ready-marker root-authorized-keys))
(define default-minimal-operating-system (operating-system))
@@ -292,11 +295,41 @@
(hash-set! cache (package-cache-key package) output-path)
output-path))))
(define (prefix-manifest-string source-path)
(string-append "prefix-source=" source-path "\n" (path-signature source-path)))
(define prefix-materializer-version "2")
(define (materialize-prefix source-path name version store-dir)
(let* ((manifest (prefix-manifest-string source-path))
(define (prefix-manifest-string source-path extra-files)
(string-append
"prefix-materializer-version=" prefix-materializer-version "\n"
"prefix-source=" source-path "\n"
(path-signature source-path)
(if (null? extra-files)
""
(string-append
"\nextra-files=\n"
(string-join
(map (lambda (entry)
(string-append (cdr entry) "\n" (path-signature (car entry))))
extra-files)
"\n")))))
(define (copy-extra-node source destination)
(let ((kind (stat:type (lstat source))))
(mkdir-p (dirname destination))
(case kind
((symlink)
(unless (or (file-exists? destination)
(false-if-exception (readlink destination)))
(let ((target (readlink source)))
(symlink target destination)
(unless (string-prefix? "/" target)
(copy-extra-node (string-append (dirname source) "/" target)
(string-append (dirname destination) "/" target))))))
(else
(unless (file-exists? destination)
(copy-node source destination))))))
(define* (materialize-prefix source-path name version store-dir #:key (extra-files '()))
(let* ((manifest (prefix-manifest-string source-path extra-files))
(hash (string-hash manifest))
(output-path (string-append store-dir "/" hash "-" name "-" version)))
(unless (file-exists? output-path)
@@ -305,6 +338,10 @@
(copy-node (string-append source-path "/" entry)
(string-append output-path "/" entry)))
(directory-entries source-path))
(for-each (lambda (entry)
(copy-extra-node (car entry)
(string-append output-path "/" (cdr entry))))
extra-files)
(write-file (string-append output-path "/.fruix-package") manifest))
output-path))
@@ -352,8 +389,18 @@
"\n")
"\n"))
(define (rc-conf-entry-value os key)
(let ((entry (assoc key (operating-system-rc-conf-entries os))))
(and entry (cdr entry))))
(define (sshd-enabled? os)
(let ((value (rc-conf-entry-value os "sshd_enable")))
(and value
(member (string-upcase value) '("YES" "TRUE" "1")))))
(define (render-rc.conf os)
(let* ((entries (append `(("hostname" . ,(operating-system-host-name os))
("fruix_activate_enable" . "YES")
("fruix_shepherd_enable" . "YES"))
(operating-system-rc-conf-entries os))))
(string-append
@@ -386,6 +433,23 @@
"\n")
"\n")))
(define (render-master-passwd os)
(let ((groups (operating-system-groups os)))
(string-append
(string-join
(map (lambda (account)
(format #f "~a:*:~a:~a::0:0:~a:~a:~a"
(user-account-name account)
(user-account-uid account)
(or (group-name->gid groups (user-account-group account))
(error "unknown primary group" (user-account-group account)))
(user-account-comment account)
(user-account-home account)
(user-account-shell account)))
(operating-system-users os))
"\n")
"\n")))
(define (render-group os)
(let ((users (operating-system-users os)))
(string-append
@@ -404,17 +468,25 @@
"\n")
"\n")))
(define (fstab-fsck-fields fs)
(if (string=? (file-system-type fs) "ufs")
(if (string=? (file-system-mount-point fs) "/")
'(1 1)
'(2 2))
'(0 0)))
(define (render-fstab os)
(string-append
(string-join
(map (lambda (fs)
(format #f "~a\t~a\t~a\t~a\t~a\t~a"
(file-system-device fs)
(file-system-mount-point fs)
(file-system-type fs)
(file-system-options fs)
(if (string=? (file-system-mount-point fs) "/") 1 0)
(if (file-system-needed-for-boot? fs) 1 2)))
(let ((checks (fstab-fsck-fields fs)))
(format #f "~a\t~a\t~a\t~a\t~a\t~a"
(file-system-device fs)
(file-system-mount-point fs)
(file-system-type fs)
(file-system-options fs)
(first checks)
(second checks))))
(operating-system-file-systems os))
"\n")
"\n"))
@@ -431,43 +503,85 @@
(define (render-motd os)
(string-append "Welcome to Fruix on FreeBSD (" (operating-system-host-name os) ")\n"))
(define (render-login-conf)
(string-append
"default:\\\n"
"\t:path=/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin:\\\n"
"\t:umask=022:\\\n"
"\t:charset=UTF-8:\\\n"
"\t:lang=C.UTF-8:\n"
"daemon:\\\n"
"\t:path=/sbin /bin /usr/sbin /usr/bin /usr/local/sbin /usr/local/bin:\\\n"
"\t:tc=default:\n"
"root:\\\n"
"\t:ignorenologin:\\\n"
"\t:tc=default:\n"))
(define (render-ttys)
(string-append
"console\tnone\tunknown\toff secure\n"
"ttyu0\tnone\tvt100\toff secure\n"
"xc0\tnone\txterm\toff secure\n"))
(define (render-root-authorized-keys os)
(if (null? (operating-system-root-authorized-keys os))
""
(string-append
(string-join (operating-system-root-authorized-keys os) "\n")
"\n")))
(define (render-sshd-config os)
(string-append
"Port 22\n"
"PermitRootLogin yes\n"
"PasswordAuthentication no\n"
"KbdInteractiveAuthentication no\n"
"ChallengeResponseAuthentication no\n"
"UsePAM no\n"
"PubkeyAuthentication yes\n"
"AuthorizedKeysFile .ssh/authorized_keys\n"
"PidFile /var/run/sshd.pid\n"
"UseDNS no\n"))
(define (render-activation-script os)
(let* ((users (operating-system-users os))
(groups (operating-system-groups os))
(non-root-groups (filter (lambda (group)
(> (user-group-gid group) 0))
groups))
(non-root-users (filter (lambda (account)
(> (user-account-uid account) 0))
users)))
(home-setup
(string-join
(map (lambda (account)
(let ((name (user-account-name account))
(uid (user-account-uid account))
(gid (or (group-name->gid groups (user-account-group account))
(error "unknown primary group" (user-account-group account))))
(home (user-account-home account))
(system? (user-account-system? account)))
(string-append
"mkdir -p " home "\n"
(if (or (string=? name "root") system?)
""
(format #f "if [ -x /usr/sbin/chown ]; then /usr/sbin/chown ~a:~a ~a 2>/dev/null || true; fi\n"
uid gid home)))))
users)
""))
(ssh-section
(string-append
"mkdir -p /var/empty /etc/ssh /root/.ssh\n"
"chmod 700 /root/.ssh\n"
(if (null? (operating-system-root-authorized-keys os))
""
"if [ -f /run/current-system/root/.ssh/authorized_keys ]; then cp /run/current-system/root/.ssh/authorized_keys /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys; fi\n")
(if (sshd-enabled? os)
"if [ -x /usr/bin/ssh-keygen ]; then /usr/bin/ssh-keygen -A; fi\n"
""))))
(string-append
"#!/bin/sh\n"
"set -eu\n"
"mkdir -p /var/lib/fruix /var/log /var/run /root /home /tmp\n"
"mkdir -p /var/cron /var/db /var/lib/fruix /var/log /var/run /root /home /tmp\n"
"chmod 1777 /tmp\n"
(string-join
(append
(map (lambda (group)
(format #f "pw groupadd ~a -g ~a 2>/dev/null || true"
(user-group-name group)
(user-group-gid group)))
non-root-groups)
(map (lambda (account)
(let ((group (user-account-group account))
(supplementary (user-account-supplementary-groups account)))
(format #f "pw useradd ~a -u ~a -g ~a~a -d ~a -m -s ~a -c '~a' 2>/dev/null || true"
(user-account-name account)
(user-account-uid account)
group
(if (null? supplementary)
""
(string-append " -G " (string-join supplementary ",")))
(user-account-home account)
(user-account-shell account)
(user-account-comment account))))
non-root-users))
"\n")
"\n")))
"if [ -x /usr/bin/cap_mkdb ] && [ -f /etc/login.conf ]; then /usr/bin/cap_mkdb /etc/login.conf; fi\n"
"if [ -x /usr/sbin/pwd_mkdb ] && [ -f /etc/master.passwd ]; then /usr/sbin/pwd_mkdb -p /etc/master.passwd; fi\n"
home-setup
ssh-section)))
(define (render-shepherd-config os)
(let ((ready-marker (operating-system-ready-marker os)))
@@ -502,6 +616,26 @@
" #:respawn? #f)))\n\n"
"(start-service (lookup-service 'fruix-ready))\n")))
(define (render-activation-rc-script)
(string-append
"#!/bin/sh\n"
"# PROVIDE: fruix_activate\n"
"# REQUIRE: FILESYSTEMS\n"
"# BEFORE: LOGIN sshd fruix_shepherd\n"
"# KEYWORD: shutdown\n\n"
". /etc/rc.subr\n\n"
"name=fruix_activate\n"
"rcvar=fruix_activate_enable\n"
": ${fruix_activate_enable:=YES}\n"
"start_cmd=fruix_activate_start\n"
"stop_cmd=:\n\n"
"fruix_activate_start()\n"
"{\n"
" /run/current-system/activate\n"
"}\n\n"
"load_rc_config $name\n"
"run_rc_command \"$1\"\n"))
(define (render-rc-script shepherd-store guile-store guile-extra-store)
(let ((ld-library-path (string-append guile-extra-store "/lib:"
guile-store "/lib:/usr/local/lib"))
@@ -514,7 +648,7 @@
(string-append
"#!/bin/sh\n"
"# PROVIDE: fruix_shepherd\n"
"# REQUIRE: FILESYSTEMS\n"
"# REQUIRE: FILESYSTEMS fruix_activate\n"
"# BEFORE: LOGIN\n"
"# KEYWORD: shutdown\n\n"
". /etc/rc.subr\n\n"
@@ -535,7 +669,7 @@
" GUILE_LOAD_PATH='" guile-load-path "' \\\n"
" GUILE_LOAD_COMPILED_PATH='" guile-load-compiled-path "' \\\n"
" GUILE_EXTENSIONS_PATH='" guile-extensions-path "' \\\n"
" " shepherd-store "/bin/shepherd -I -s \"$socket\" -c \"$config\" --pid=\"$pidfile\" -l \"$logfile\" >/var/log/shepherd-bootstrap.out 2>&1 &\n"
" " guile-store "/bin/guile --no-auto-compile " shepherd-store "/bin/shepherd -I -s \"$socket\" -c \"$config\" --pid=\"$pidfile\" -l \"$logfile\" >/var/log/shepherd-bootstrap.out 2>&1 &\n"
" for _try in 1 2 3 4 5 6 7 8 9 10; do\n"
" [ -f \"$pidfile\" ] && [ -S \"$socket\" ] && return 0\n"
" sleep 1\n"
@@ -548,7 +682,7 @@
" GUILE_LOAD_PATH='" guile-load-path "' \\\n"
" GUILE_LOAD_COMPILED_PATH='" guile-load-compiled-path "' \\\n"
" GUILE_EXTENSIONS_PATH='" guile-extensions-path "' \\\n"
" " shepherd-store "/bin/herd -s \"$socket\" stop root >/dev/null 2>&1 || true\n"
" " guile-store "/bin/guile --no-auto-compile " shepherd-store "/bin/herd -s \"$socket\" stop root >/dev/null 2>&1 || true\n"
" for _try in 1 2 3 4 5 6 7 8 9 10; do\n"
" [ ! -f \"$pidfile\" ] && return 0\n"
" sleep 1\n"
@@ -565,16 +699,26 @@
"run_rc_command \"$1\"\n")))
(define (operating-system-generated-files os)
`(("boot/loader.conf" . ,(render-loader-conf (operating-system-loader-entries os)))
("etc/rc.conf" . ,(render-rc.conf os))
("etc/fstab" . ,(render-fstab os))
("etc/hosts" . ,(render-hosts os))
("etc/passwd" . ,(render-passwd os))
("etc/group" . ,(render-group os))
("etc/shells" . ,(render-shells os))
("etc/motd" . ,(render-motd os))
("activate" . ,(render-activation-script os))
("shepherd/init.scm" . ,(render-shepherd-config os))))
(append
`(("boot/loader.conf" . ,(render-loader-conf (operating-system-loader-entries os)))
("etc/rc.conf" . ,(render-rc.conf os))
("etc/fstab" . ,(render-fstab os))
("etc/hosts" . ,(render-hosts os))
("etc/passwd" . ,(render-passwd os))
("etc/master.passwd" . ,(render-master-passwd os))
("etc/group" . ,(render-group os))
("etc/login.conf" . ,(render-login-conf))
("etc/shells" . ,(render-shells os))
("etc/motd" . ,(render-motd os))
("etc/ttys" . ,(render-ttys))
("activate" . ,(render-activation-script os))
("shepherd/init.scm" . ,(render-shepherd-config os)))
(if (sshd-enabled? os)
`(("etc/ssh/sshd_config" . ,(render-sshd-config os)))
'())
(if (null? (operating-system-root-authorized-keys os))
'()
`(("root/.ssh/authorized_keys" . ,(render-root-authorized-keys os))))))
(define (operating-system-closure-spec os)
(validate-operating-system os)
@@ -649,11 +793,31 @@
(base-package-stores (map (lambda (package)
(materialize-freebsd-package package store-dir cache))
(operating-system-base-packages os)))
(guile-store (materialize-prefix guile-prefix "fruix-guile-runtime" "3.0" store-dir))
(guile-extra-store (materialize-prefix guile-extra-prefix "fruix-guile-extra" "3.0" store-dir))
(guile-runtime-extra-files
'(("/usr/local/lib/libgc-threaded.so.1" . "lib/libgc-threaded.so.1")
("/usr/local/lib/libffi.so.8" . "lib/libffi.so.8")
("/usr/local/lib/libintl.so.8" . "lib/libintl.so.8")
("/usr/local/lib/libunistring.so.5" . "lib/libunistring.so.5")
("/usr/local/lib/libiconv.so.2" . "lib/libiconv.so.2")
("/usr/local/lib/libgmp.so.10" . "lib/libgmp.so.10")))
(guile-extra-runtime-files
'(("/usr/local/lib/libevent-2.1.so.7" . "lib/libevent-2.1.so.7")
("/usr/local/lib/libgnutls.so.30" . "lib/libgnutls.so.30")
("/usr/local/lib/libp11-kit.so.0" . "lib/libp11-kit.so.0")
("/usr/local/lib/libidn2.so.0" . "lib/libidn2.so.0")
("/usr/local/lib/libtasn1.so.6" . "lib/libtasn1.so.6")
("/usr/local/lib/libhogweed.so.6" . "lib/libhogweed.so.6")
("/usr/local/lib/libnettle.so.8" . "lib/libnettle.so.8")))
(guile-store (materialize-prefix guile-prefix "fruix-guile-runtime" "3.0" store-dir
#:extra-files guile-runtime-extra-files))
(guile-extra-store (materialize-prefix guile-extra-prefix "fruix-guile-extra" "3.0" store-dir
#:extra-files (append guile-runtime-extra-files
guile-extra-runtime-files)))
(shepherd-store (materialize-prefix shepherd-prefix "fruix-shepherd-runtime" "1.0.9" store-dir))
(generated-files (append (operating-system-generated-files os)
`(("usr/local/etc/rc.d/fruix-shepherd"
`(("usr/local/etc/rc.d/fruix-activate"
. ,(render-activation-rc-script))
("usr/local/etc/rc.d/fruix-shepherd"
. ,(render-rc-script shepherd-store guile-store guile-extra-store)))))
(references (append (list kernel-store bootloader-store guile-store guile-extra-store shepherd-store)
base-package-stores))
@@ -692,6 +856,7 @@
(write-file (string-append closure-path "/" (car entry)) (cdr entry)))
generated-files)
(chmod (string-append closure-path "/activate") #o555)
(chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-activate") #o555)
(chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd") #o555)
(write-file (string-append closure-path "/parameters.scm")
(object->string (operating-system-closure-spec os)))
@@ -731,8 +896,8 @@
(mkdir-p rootfs)
(for-each (lambda (dir)
(mkdir-p (string-append rootfs dir)))
'("/run" "/boot" "/etc" "/usr" "/usr/local" "/usr/local/etc"
"/usr/local/etc/rc.d" "/var" "/var/lib" "/var/lib/fruix"
'("/run" "/boot" "/etc" "/etc/ssh" "/usr" "/usr/local" "/usr/local/etc"
"/usr/local/etc/rc.d" "/var" "/var/cron" "/var/db" "/var/lib" "/var/lib/fruix"
"/var/log" "/var/run" "/tmp" "/dev" "/root" "/home"))
(chmod (string-append rootfs "/tmp") #o1777)
(symlink-force closure-path (string-append rootfs "/run/current-system"))
@@ -744,19 +909,26 @@
(for-each (lambda (dir)
(symlink-force (string-append "/run/current-system/profile/usr/" dir)
(string-append rootfs "/usr/" dir)))
'("bin" "sbin" "libexec"))
'("bin" "lib" "sbin" "libexec"))
(for-each (lambda (path)
(symlink-force (string-append "/run/current-system/profile/etc/" path)
(string-append rootfs "/etc/" path)))
'("rc" "rc.subr" "rc.shutdown" "rc.d" "defaults"))
'("rc" "rc.subr" "rc.shutdown" "rc.d" "defaults"
"devd.conf" "network.subr" "newsyslog.conf" "syslog.conf"))
(for-each (lambda (path)
(symlink-force (string-append "/run/current-system/etc/" path)
(string-append rootfs "/etc/" path)))
'("rc.conf" "fstab" "hosts" "passwd" "group" "shells" "motd"))
'("rc.conf" "fstab" "hosts" "passwd" "master.passwd" "group"
"login.conf" "shells" "motd" "ttys"))
(when (file-exists? (string-append closure-path "/etc/ssh/sshd_config"))
(symlink-force "/run/current-system/etc/ssh/sshd_config"
(string-append rootfs "/etc/ssh/sshd_config")))
(for-each (lambda (path)
(symlink-force (string-append "/run/current-system/boot/" path)
(string-append rootfs "/boot/" path)))
'("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf"))
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate"
(string-append rootfs "/usr/local/etc/rc.d/fruix-activate"))
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-shepherd"
(string-append rootfs "/usr/local/etc/rc.d/fruix-shepherd"))
`((rootfs . ,rootfs)
@@ -771,6 +943,7 @@
(partition-scheme 'gpt)
(efi-size "64m")
(root-size "256m")
(disk-capacity #f)
(efi-partition-label "efiboot")
(root-partition-label "fruix-root")
(serial-console "comconsole"))
@@ -780,6 +953,7 @@
(partition-scheme . ,partition-scheme)
(efi-size . ,efi-size)
(root-size . ,root-size)
(disk-capacity . ,disk-capacity)
(efi-partition-label . ,efi-partition-label)
(root-partition-label . ,root-partition-label)
(serial-console . ,serial-console)
@@ -830,6 +1004,19 @@
(define (mktemp-directory pattern)
(command-output "mktemp" "-d" pattern))
(define image-builder-version "2")
(define (resize-gpt-image image disk-capacity)
(when disk-capacity
(run-command "truncate" "-s" disk-capacity image)
(let ((md (command-output "mdconfig" "-a" "-t" "vnode" "-f" image)))
(dynamic-wind
(lambda () #t)
(lambda ()
(run-command "gpart" "recover" (string-append "/dev/" md)))
(lambda ()
(run-command "mdconfig" "-d" "-u" (string-drop md 2)))))))
(define* (materialize-bhyve-image os
#:key
(store-dir "/frx/store")
@@ -838,6 +1025,7 @@
(shepherd-prefix "/tmp/shepherd-freebsd-validate-install")
(efi-size "64m")
(root-size "256m")
(disk-capacity #f)
(efi-partition-label "efiboot")
(root-partition-label "fruix-root")
(serial-console "comconsole"))
@@ -850,12 +1038,15 @@
(image-spec (operating-system-image-spec os
#:efi-size efi-size
#:root-size root-size
#:disk-capacity disk-capacity
#:efi-partition-label efi-partition-label
#:root-partition-label root-partition-label
#:serial-console serial-console))
(store-items (store-reference-closure (list closure-path)))
(manifest (string-append
"image-spec=\n"
"image-builder-version=\n"
image-builder-version
"\nimage-spec=\n"
(object->string image-spec)
"closure-path=\n"
closure-path
@@ -905,6 +1096,7 @@
"-p" (string-append "efi/" efi-partition-label ":=" temp-esp)
"-p" (string-append "freebsd-ufs/" root-partition-label ":=" temp-root)
"-o" temp-disk)
(resize-gpt-image temp-disk disk-capacity)
(mkdir-p temp-output)
(copy-regular-file temp-disk (string-append temp-output "/disk.img"))
(copy-regular-file temp-esp (string-append temp-output "/esp.img"))

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,204 @@
#!/bin/sh
set -eu
repo_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd)
vm_id=90490f2e-e8fc-4b7a-388e-5c26f0157289
metadata_target=${METADATA_OUT:-}
root_authorized_key_file=${ROOT_AUTHORIZED_KEY_FILE:-$HOME/.ssh/id_ed25519.pub}
requested_disk_capacity=${DISK_CAPACITY:-}
cleanup=0
if [ -n "${WORKDIR:-}" ]; then
workdir=$WORKDIR
mkdir -p "$workdir"
else
workdir=$(mktemp -d /tmp/fruix-phase9-xcpng.XXXXXX)
cleanup=1
fi
if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then
cleanup=0
fi
phase9_os_template=$repo_root/tests/system/phase9-minimal-operating-system.scm.in
phase9_os_file=$workdir/phase9-minimal-operating-system.scm
phase8_log=$workdir/phase8-system-image.log
phase8_metadata=$workdir/phase8-system-image-metadata.txt
arp_scan_log=$workdir/arp-scan.log
ssh_stdout=$workdir/ssh.out
ssh_stderr=$workdir/ssh.err
metadata_file=$workdir/phase9-xcpng-boot-metadata.txt
vdi_info_json=$workdir/vdi-info.json
vm_info_json=$workdir/vm-info.json
upload_image=$workdir/disk.vhd
cleanup_workdir() {
if [ "$cleanup" -eq 1 ]; then
rm -rf "$workdir"
fi
}
trap cleanup_workdir EXIT INT TERM
[ -f "$root_authorized_key_file" ] || {
echo "missing root authorized key file: $root_authorized_key_file" >&2
exit 1
}
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
# Discover the existing target VDI attached as disk 0 for the operator-provided VM.
xo-cli list-objects id=$vm_id >"$vm_info_json"
vdi_id=$(xo-cli list-objects type=VBD | jq -r '.[] | select(.VM=="'$vm_id'" and .is_cd_drive==false and .position=="0") | .VDI' | head -n 1)
[ -n "$vdi_id" ] || { echo "failed to discover target VDI for VM $vm_id" >&2; exit 1; }
xo-cli list-objects type=VDI | jq '[.[] | select(.id=="'$vdi_id'")]' >"$vdi_info_json"
vdi_size=$(jq -r '.[0].size' "$vdi_info_json")
[ -n "$vdi_size" ] || { echo "failed to discover VDI size for $vdi_id" >&2; exit 1; }
if [ -n "$requested_disk_capacity" ] && [ "$requested_disk_capacity" != "$vdi_size" ]; then
echo "existing XCP-ng import path requires an image that matches the target VDI size; use DISK_CAPACITY=$vdi_size or leave it unset" >&2
exit 1
fi
disk_capacity=$vdi_size
requested_disk_bytes=$vdi_size
sed "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" "$phase9_os_template" > "$phase9_os_file"
KEEP_WORKDIR=1 WORKDIR=$workdir/phase8-build OS_FILE=$phase9_os_file DISK_CAPACITY=$disk_capacity \
METADATA_OUT=$phase8_metadata "$repo_root/tests/system/run-phase8-system-image.sh" \
>"$phase8_log" 2>&1
disk_image=$(sed -n 's/^disk_image=//p' "$phase8_metadata")
closure_path=$(sed -n 's/^closure_path=//p' "$phase8_metadata")
closure_base=$(basename "$closure_path")
raw_sha256=$(sed -n 's/^raw_sha256=//p' "$phase8_metadata")
image_store_path=$(sed -n 's/^image_store_path=//p' "$phase8_metadata")
command -v qemu-img >/dev/null 2>&1 || {
echo "qemu-img is required to convert the raw Fruix image to XCP-ng-compatible VHD" >&2
exit 1
}
qemu-img convert -f raw -O vpc -o subformat=dynamic,force_size=on "$disk_image" "$upload_image"
upload_sha256=$(sha256 -q "$upload_image")
upload_size_bytes=$(stat -f '%z' "$upload_image")
xo-cli vm.stop id=$vm_id force=true >/dev/null 2>&1 || true
xo-cli disk.importContent id=$vdi_id @=$upload_image >"$workdir/disk-import.out"
xo-cli vm.setBootOrder vm=$vm_id order=dcn >"$workdir/set-boot-order.out"
xo-cli vm.start id=$vm_id >"$workdir/vm-start.out"
# Wait for the VM to obtain an address and accept SSH using the injected key.
vm_mac=$(jq -r '.[0].VIFs[0]' "$vm_info_json")
if [ -n "$vm_mac" ] && [ "$vm_mac" != null ]; then
vm_mac=$(xo-cli list-objects type=VIF | jq -r '.[] | select(.id=="'$vm_mac'") | .MAC' | tr 'A-Z' 'a-z')
else
vm_mac=
fi
host_interface=$(route -n get default | awk '/interface:/{print $2; exit}')
host_ip=$(ifconfig "$host_interface" | awk '/inet /{print $2; exit}')
subnet_prefix=${host_ip%.*}
ssh_guest() {
ssh -i "$root_authorized_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=5 \
root@"$guest_ip" "$@"
}
guest_ip=
for attempt in $(jot 90 1 90); do
: >"$arp_scan_log"
for host in $(jot 254 1 254); do
ip=$subnet_prefix.$host
(
ping -c 1 -W 1000 "$ip" >/dev/null 2>&1 && echo "$ip" >>"$arp_scan_log"
) &
done
wait
if [ -n "$vm_mac" ]; then
guest_ip=$(arp -an | awk -v mac="$vm_mac" 'tolower($4)==mac {gsub(/[()]/,"",$2); print $2; exit}')
fi
if [ -n "$guest_ip" ]; then
if ssh -i "$root_authorized_key_file" \
-o BatchMode=yes \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=3 \
root@"$guest_ip" 'test -f /var/lib/fruix/ready' >"$ssh_stdout" 2>"$ssh_stderr"; then
break
fi
fi
sleep 5
done
[ -n "$guest_ip" ] || {
echo "guest IP was not discovered; manual console inspection is likely required" >&2
exit 1
}
ready_marker=$(ssh_guest 'cat /var/lib/fruix/ready')
run_current_system_target=$(ssh_guest 'readlink /run/current-system')
rc_conf_hostname=$(ssh_guest 'grep "^hostname=" /etc/rc.conf | cut -d"\"" -f2')
shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
logger_log=$(ssh_guest 'cat /var/log/fruix-shepherd.log' | tr '\n' ' ')
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
uname_output=$(ssh_guest 'uname -sr')
operator_home_listing=$(ssh_guest 'ls -d /home/operator')
activate_preview=$(ssh_guest 'head -n 5 /run/current-system/activate' | tr '\n' ' ')
[ "$ready_marker" = ready ] || { echo "unexpected ready marker contents: $ready_marker" >&2; exit 1; }
[ "$shepherd_status" = running ] || { echo "fruix_shepherd is not running" >&2; exit 1; }
[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; }
[ "$run_current_system_target" = "/frx/store/$closure_base" ] || {
echo "unexpected /run/current-system target in guest: $run_current_system_target" >&2
exit 1
}
[ "$rc_conf_hostname" = fruix-freebsd ] || { echo "unexpected guest hostname config: $rc_conf_hostname" >&2; exit 1; }
[ "$operator_home_listing" = /home/operator ] || { echo "operator home missing" >&2; exit 1; }
cat >"$metadata_file" <<EOF
workdir=$workdir
vm_id=$vm_id
vdi_id=$vdi_id
vdi_size=$vdi_size
disk_capacity=$disk_capacity
requested_disk_capacity=${requested_disk_capacity:-<auto>}
requested_disk_bytes=$requested_disk_bytes
phase9_os_file=$phase9_os_file
phase8_log=$phase8_log
phase8_metadata=$phase8_metadata
image_store_path=$image_store_path
disk_image=$disk_image
upload_image=$upload_image
upload_format=vhd-dynamic
upload_sha256=$upload_sha256
upload_size_bytes=$upload_size_bytes
closure_path=$closure_path
closure_base=$closure_base
raw_sha256=$raw_sha256
guest_ip=$guest_ip
vm_mac=$vm_mac
ready_marker=$ready_marker
run_current_system_target=$run_current_system_target
shepherd_status=$shepherd_status
sshd_status=$sshd_status
logger_log=$logger_log
uname_output=$uname_output
operator_home_listing=$operator_home_listing
activate_preview=$activate_preview
boot_backend=xcp-ng-xo-cli
operator_access=ssh-root-key
EOF
if [ -n "$metadata_target" ]; then
mkdir -p "$(dirname "$metadata_target")"
cp "$metadata_file" "$metadata_target"
fi
printf 'PASS phase9-xcpng-boot\n'
printf 'Work directory: %s\n' "$workdir"
printf 'Metadata file: %s\n' "$metadata_file"
if [ -n "$metadata_target" ]; then
printf 'Copied metadata to: %s\n' "$metadata_target"
fi
printf '%s\n' '--- metadata ---'
cat "$metadata_file"