From db4d5bdf4c8062166df5b6ef2b6ef4e96b782d62 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Mon, 6 Apr 2026 08:55:35 +0200 Subject: [PATCH] system: add in-node build and reconfigure --- docs/GUIX_DIFFERENCES.md | 12 +- docs/PROGRESS.md | 63 ++-- ...ase20-installed-node-management-freebsd.md | 178 ++++++++++++ docs/system-deployment-workflow.md | 57 +++- modules/fruix/system/freebsd/media.scm | 110 +++++-- modules/fruix/system/freebsd/model.scm | 6 + modules/fruix/system/freebsd/render.scm | 155 +++++++++- scripts/fruix.scm | 37 ++- ...e20-installed-node-operating-system.scm.in | 73 +++++ tests/system/run-phase8-system-image.sh | 6 +- ...-installed-node-build-reconfigure-xcpng.sh | 271 ++++++++++++++++++ 11 files changed, 916 insertions(+), 52 deletions(-) create mode 100644 docs/reports/postphase20-installed-node-management-freebsd.md create mode 100644 tests/system/postphase20-installed-node-operating-system.scm.in create mode 100755 tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh diff --git a/docs/GUIX_DIFFERENCES.md b/docs/GUIX_DIFFERENCES.md index 020e93d..032bee1 100644 --- a/docs/GUIX_DIFFERENCES.md +++ b/docs/GUIX_DIFFERENCES.md @@ -171,8 +171,10 @@ So compared with Guix-on-Linux intuition, Fruix operators should be more explici This remains the biggest operational gap, but it is no longer a complete gap. -Installed Fruix systems now provide a small in-guest helper: +Installed Fruix systems now provide a larger in-guest helper surface: +- `fruix system build` +- `fruix system reconfigure` - `fruix system status` - `fruix system switch /frx/store/...-fruix-system-...` - `fruix system rollback` @@ -183,11 +185,14 @@ What this gives you today: - explicit rollback-generation tracking - in-place switching between already-staged closures on the installed target - rollback without reinstalling the whole system image again +- a validated in-node build path that can read the embedded current declaration inputs +- a validated in-node `reconfigure` path that can build a candidate closure locally and stage it as the next generation What it still does **not** give you yet compared with Guix: -- a mature `reconfigure`-style workflow that builds and stages the new closure from inside the target system +- a mature end-to-end `upgrade` story for advancing declared inputs automatically - automatic closure transfer/fetch as part of `switch` +- a higher-level `deploy` workflow across multiple machines or targets - the broader generation-management UX Guix operators expect So if you come from Guix, assume that Fruix now has: @@ -195,7 +200,8 @@ So if you come from Guix, assume that Fruix now has: - strong closure/store semantics - explicit install artifacts - explicit generation metadata roots -- a real but still modest installed-system switch/rollback UX +- a real installed-system build/reconfigure/switch/rollback surface +- but not yet the fuller long-term node/deployment UX that Guix users may expect ## 7. Fruix keeps Guix-like store semantics, but not Guix/Nix hash-prefix machinery exactly diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 0a81a6e..b57fd05 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -64,6 +64,11 @@ Fruix currently has: - `promoted-native-build-result` - `operating-system-from-promoted-native-build-result` - a real XCP-ng boot validation of a system materialized from a promoted native-build result identity +- installed systems that now carry their own canonical declaration inputs and bundled Fruix node CLI sources +- a real XCP-ng validation of in-node: + - `fruix system build` + - `fruix system reconfigure` + - `fruix system rollback` Validated boot modes still are: @@ -76,34 +81,45 @@ The validated Phase 18 installation work currently uses: ## Latest completed achievement -### 2026-04-06 β€” Promoted native-base result sets are now first-class declaration inputs +### 2026-04-06 β€” Installed systems can now build and reconfigure themselves from local declaration state -Fruix can now materialize and boot a normal system declaration directly from a promoted native-build result bundle in `/frx/store`. +Fruix-installed systems are now meaningfully closer to real Fruix nodes. Highlights: -- promoted native-build result bundles are no longer only post-build artifacts -- system declarations can now refer directly to those promoted identities via: - - `promoted-native-build-result` - - `operating-system-from-promoted-native-build-result` -- promoted result bundles now drive: - - kernel selection - - bootloader selection - - base world selection - - optional promoted headers selection -- the resulting system closure now records promoted-result provenance explicitly in: - - `metadata/promoted-native-build-result.scm` - - `metadata/store-layout.scm` - - closure `.references` -- the validated real-VM boot used the promoted `ssh-guest` result bundle as the declaration input, proving that the result/promotion model is now consumable by ordinary Fruix system materialization +- system closures now carry canonical declaration metadata in: + - `metadata/system-declaration.scm` + - `metadata/system-declaration-info.scm` + - `metadata/system-declaration-system` +- system closures now also carry bundled Fruix node CLI sources in: + - `share/fruix/node/scripts/fruix.scm` + - `share/fruix/node/modules/...` + - `share/fruix/node/guix/guix/build/utils.scm` +- the installed helper at `/usr/local/bin/fruix` now supports: + - `fruix system build` + - `fruix system reconfigure` + - `fruix system status` + - `fruix system switch` + - `fruix system rollback` +- no-argument in-node `build` and `reconfigure` now use the node's own embedded declaration inputs +- in-node Fruix builds now reuse the installed Guile/Shepherd runtime stores already referenced by the system instead of assuming host-only `/tmp/...` build prefixes +- the real XCP-ng validation proved the full installed-node flow: + - boot current system + - build from local declaration state + - build a candidate declaration on-node + - `reconfigure` into that candidate generation + - reboot into the candidate generation + - `rollback` + - reboot back into the original generation Validation: -- `PASS phase20-promoted-native-base-declaration-xcpng` +- `PASS postphase20-installed-node-build-reconfigure-xcpng` -Report: +Reports: - `docs/reports/postphase20-promoted-native-base-declarations-freebsd.md` +- `docs/reports/postphase20-installed-node-management-freebsd.md` - `docs/system-deployment-workflow.md` - `docs/GUIX_DIFFERENCES.md` @@ -133,12 +149,15 @@ Report: The next practical follow-up is now clearer: -- decide how much of result creation, validation, promotion, and declaration consumption should be wrapped into one higher-level Fruix workflow -- determine whether native-build result selection should become a more explicit operator-facing deployment/declaration surface rather than a lower-level helper step -- continue tightening how executor policy and promoted-result identity are presented to operators without losing the new first-class declaration-input model +- grow the installed-node command surface from validated `build`/`reconfigure`/`rollback` toward: + - `upgrade` + - `build-base` + - `deploy` +- decide how executor policy, native-base promotion, and installed-node reconfiguration should compose in one operator-facing workflow +- determine how much of native-build request/promotion should remain explicit versus being absorbed into higher-level Fruix node actions The immediate architectural direction is no longer just β€œcan guest self-hosting work?” It is now: -- how should Fruix expose build executor choice, promoted native-base identities, and deployment materialization as one coherent product workflow? +- how should Fruix expose real managed-node behavior across declaration inputs, native-base results, generation switching, and deployment actions? diff --git a/docs/reports/postphase20-installed-node-management-freebsd.md b/docs/reports/postphase20-installed-node-management-freebsd.md new file mode 100644 index 0000000..0741cf0 --- /dev/null +++ b/docs/reports/postphase20-installed-node-management-freebsd.md @@ -0,0 +1,178 @@ +# Post-Phase 20: installed systems as real Fruix nodes + +Date: 2026-04-06 + +## Goal + +Make a Fruix-installed machine feel like a managed Fruix node instead of only a deployed image with switch/rollback helpers. + +The immediate target was not yet the full long-term command surface of: + +- `fruix system upgrade` +- `fruix system build-base` +- `fruix system deploy` + +but it was enough to cross an important product threshold: + +- installed nodes can now remember their own declaration inputs +- installed nodes can build from those inputs locally +- installed nodes can reconfigure themselves into a newly built generation +- installed nodes can still roll back cleanly + +## What changed + +### Canonical declaration state now ships inside the system closure + +Fruix system closures now carry explicit declaration metadata: + +- `metadata/system-declaration.scm` +- `metadata/system-declaration-info.scm` +- `metadata/system-declaration-system` + +That gives an installed system a canonical local answer to: + +- what declaration source produced me? +- what top-level system variable should be used? + +This declaration metadata is also recorded through the installed generation layout metadata. + +### Bundled Fruix node CLI sources now ship inside the closure + +Installed system closures now also carry a self-contained Fruix node CLI source bundle under: + +- `share/fruix/node/scripts/fruix.scm` +- `share/fruix/node/modules/...` +- `share/fruix/node/guix/guix/build/utils.scm` + +This gives the installed node enough local Fruix/Guix Scheme source to run Fruix system actions from the node itself. + +### Installed `fruix` helper gained local build/reconfigure support + +The installed helper at: + +- `/usr/local/bin/fruix` + +now supports: + +- `fruix system build` +- `fruix system reconfigure` +- `fruix system status` +- `fruix system switch` +- `fruix system rollback` + +Current behavior: + +- `fruix system build` with no extra arguments uses: + - `/run/current-system/metadata/system-declaration.scm` + - `/run/current-system/metadata/system-declaration-system` +- `fruix system reconfigure` with no extra arguments builds from that same embedded declaration and then stages a switch to the resulting closure +- both commands can also take an explicit declaration file plus `--system NAME`, using the same general CLI shape as the host-side Fruix frontend + +### In-node builds now reuse the installed Fruix runtime stores + +A crucial implementation fix was needed here. + +An installed node should not try to reconstruct the Guile/Shepherd runtime prefixes from host-side `/tmp/...` build roots or host `/usr/local/lib/...` assumptions. + +Instead, in-node Fruix builds now explicitly reuse the installed runtime stores already referenced by the current system closure. + +That allows the in-node build path to work on the real installed system rather than depending on build-host-only paths. + +## Validation harness + +Added: + +- `tests/system/postphase20-installed-node-operating-system.scm.in` +- `tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh` + +This harness validates the new installed-node workflow on the approved real XCP-ng path. + +## Validation performed + +Approved real XCP-ng path: + +- VM `90490f2e-e8fc-4b7a-388e-5c26f0157289` +- VDI `0f1f90d3-48ca-4fa2-91d8-fc6339b95743` + +Promoted native-base result consumed by the installed node declaration: + +- `/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest` + +Validated flow: + +1. boot a Fruix-installed node built from the promoted native-base result +2. confirm the installed node exposes: + - embedded declaration metadata + - bundled node CLI sources +3. run in-guest: + - `fruix system build` + using the node's own embedded declaration inputs +4. copy a candidate declaration to the guest +5. run in-guest: + - `fruix system build /root/candidate.scm --system postphase20-installed-node-operating-system` +6. run in-guest: + - `fruix system reconfigure /root/candidate.scm --system postphase20-installed-node-operating-system` +7. reboot and verify the candidate generation boots +8. run in-guest: + - `fruix system rollback` +9. reboot and verify the original generation boots again + +Passing run: + +- `PASS postphase20-installed-node-build-reconfigure-xcpng` + +Representative metadata: + +```text +closure_path=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current +current_built_closure=/frx/store/18ee10925a15b48c676463a3359c45ff766e16a0-fruix-system-fruix-node-current +candidate_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary +reconfigure_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary +reconfigure_current_generation=2 +reconfigure_current_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary +reconfigure_rollback_generation=1 +reconfigure_rollback_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current +candidate_hostname=fruix-node-canary +rollback_current_generation=1 +rollback_current_closure=/frx/store/cd4e52d8bff348953939401c8623d4189d7c9432-fruix-system-fruix-node-current +rollback_rollback_generation=2 +rollback_rollback_closure=/frx/store/46fc9631faf556c30a1a5f39f718d5d38a3f6ba8-fruix-system-fruix-node-canary +rollback_hostname=fruix-node-current +installed_node_build_reconfigure=ok +``` + +## Important observation + +The no-argument in-node build of the current declaration did not necessarily reproduce the exact original current closure path. + +That is expected with the current model because closure metadata still records builder-local provenance such as the current build host context. + +So the significant validated fact is not strict bit-for-bit closure reuse. + +It is that the installed node can now: + +- read its own declaration inputs locally +- build a valid local candidate closure from them +- build a different candidate declaration locally +- reconfigure itself into that candidate generation +- boot the candidate generation +- roll back and boot the prior generation again + +## Result + +Fruix-installed machines are now meaningfully closer to real Fruix nodes. + +They no longer only support: + +- `status` +- `switch` +- `rollback` + +They now also support a validated local node workflow for: + +- reading embedded declaration inputs +- building a local candidate closure +- reconfiguring into that locally built candidate +- rolling back through the same installed generation model + +This is the first concrete step toward a fuller operator-facing Fruix node command surface. \ No newline at end of file diff --git a/docs/system-deployment-workflow.md b/docs/system-deployment-workflow.md index 40b7b0d..a3257d9 100644 --- a/docs/system-deployment-workflow.md +++ b/docs/system-deployment-workflow.md @@ -178,12 +178,32 @@ Installed Fruix systems now also ship a small in-guest deployment helper at: Current validated in-guest commands are: ```sh +fruix system build +fruix system reconfigure fruix system status fruix system switch /frx/store/...-fruix-system-... fruix system rollback ``` -Current intended usage: +Installed systems now carry canonical declaration state in: + +- `/run/current-system/metadata/system-declaration.scm` +- `/run/current-system/metadata/system-declaration-info.scm` +- `/run/current-system/metadata/system-declaration-system` + +So the in-guest helper can now build from the node's own embedded declaration inputs. + +Current validated build/reconfigure behavior is: + +- `fruix system build` + - with no extra arguments, builds from the embedded current declaration +- `fruix system reconfigure` + - with no extra arguments, builds from the embedded current declaration and stages a switch to the resulting closure +- both commands can also take an explicit declaration file plus `--system NAME` + +Current intended usage now has two validated patterns. + +### Pattern A: build elsewhere, then switch/rollback locally 1. build a candidate closure on the operator side with `./bin/fruix system build` 2. ensure that candidate closure is present on the installed target's `/frx/store` @@ -192,11 +212,44 @@ Current intended usage: 5. if needed, run `fruix system rollback` 6. reboot back into the recorded rollback generation -Important current limitation: +Important current limitation of this lower-level pattern: - `fruix system switch` does **not** yet fetch or copy the candidate closure onto the target for you - it assumes the selected closure is already present in the installed system's `/frx/store` +### Pattern B: build and reconfigure from the node itself + +1. inspect or edit the node declaration inputs + - embedded current declaration, or + - an explicit replacement declaration file +2. run: + +```sh +fruix system build +``` + +or: + +```sh +fruix system build /path/to/candidate.scm --system my-operating-system +``` + +3. stage a local generation update with: + +```sh +fruix system reconfigure +``` + +or: + +```sh +fruix system reconfigure /path/to/candidate.scm --system my-operating-system +``` + +4. reboot into the staged generation +5. if needed, run `fruix system rollback` +6. reboot back into the recorded prior generation + ### In-guest development environment Opt-in systems can also expose a separate development overlay under: diff --git a/modules/fruix/system/freebsd/media.scm b/modules/fruix/system/freebsd/media.scm index 0ff0e19..f121cb5 100644 --- a/modules/fruix/system/freebsd/media.scm +++ b/modules/fruix/system/freebsd/media.scm @@ -72,7 +72,13 @@ (store-dir "/frx/store") (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") - (shepherd-prefix "/tmp/shepherd-freebsd-validate-install")) + (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (guile-store-path #f) + (guile-extra-store-path #f) + (shepherd-store-path #f) + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f)) (validate-operating-system os) (let* ((cache (make-hash-table)) (source-cache (make-hash-table)) @@ -110,12 +116,15 @@ ("/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)) + (guile-store (or guile-store-path + (materialize-prefix guile-prefix "fruix-guile-runtime" "3.0" store-dir + #:extra-files guile-runtime-extra-files))) + (guile-extra-store (or guile-extra-store-path + (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 (or shepherd-store-path + (materialize-prefix shepherd-prefix "fruix-shepherd-runtime" "1.0.9" store-dir))) (host-base-stores (delete-duplicates (map cdr @@ -149,6 +158,19 @@ (promoted-native-build-result-artifact-store native-build-result artifact-kind)) '(world kernel headers bootloader)) '())))) + (declaration-source-text + (or declaration-source + ";; Fruix declaration source is unavailable for this closure.\n")) + (declaration-origin-text (or declaration-origin "")) + (declaration-system-text + (cond ((symbol? declaration-system-symbol) + (symbol->string declaration-system-symbol)) + ((string? declaration-system-symbol) + declaration-system-symbol) + (else ""))) + (declaration-info-object + `((available? . ,(not (not declaration-source))) + (system-variable . ,declaration-system-text))) (metadata-files (append (list (cons "metadata/freebsd-base.scm" @@ -159,10 +181,18 @@ (object->string (map freebsd-source-materialization-spec source-materializations))) (cons "metadata/host-base-provenance.scm" (object->string (host-freebsd-provenance))) + (cons "metadata/system-declaration.scm" + declaration-source-text) + (cons "metadata/system-declaration-info.scm" + (object->string declaration-info-object)) + (cons "metadata/system-declaration-system" + (string-append declaration-system-text "\n")) (cons "metadata/store-layout.scm" (object->string `((freebsd-base . ,(freebsd-base-spec (operating-system-freebsd-base os))) (freebsd-source . ,(freebsd-source-spec (freebsd-base-source (operating-system-freebsd-base os)))) + (system-declaration-available? . ,(not (not declaration-source))) + (system-declaration-system-variable . ,declaration-system-text) (promoted-native-build-result . ,promoted-native-build-result-summary) (promoted-native-build-artifact-store-count . ,(length promoted-native-build-artifact-stores)) (promoted-native-build-artifact-stores . ,promoted-native-build-artifact-stores) @@ -277,6 +307,9 @@ (freebsd-source-materializations-file . ,(string-append closure-path "/metadata/freebsd-source-materializations.scm")) (materialized-source-stores . ,materialized-source-stores) (host-base-provenance-file . ,(string-append closure-path "/metadata/host-base-provenance.scm")) + (system-declaration-file . ,(string-append closure-path "/metadata/system-declaration.scm")) + (system-declaration-info-file . ,(string-append closure-path "/metadata/system-declaration-info.scm")) + (system-declaration-system-file . ,(string-append closure-path "/metadata/system-declaration-system")) (store-layout-file . ,(string-append closure-path "/metadata/store-layout.scm")) (promoted-native-build-result-file . ,(and promoted-native-build-result-summary @@ -309,6 +342,9 @@ (freebsd-source-materializations-file . ,(string-append closure-path "/metadata/freebsd-source-materializations.scm")) (host-base-provenance-file . ,(string-append closure-path "/metadata/host-base-provenance.scm")) + (system-declaration-file . ,(string-append closure-path "/metadata/system-declaration.scm")) + (system-declaration-info-file . ,(string-append closure-path "/metadata/system-declaration-info.scm")) + (system-declaration-system-file . ,(string-append closure-path "/metadata/system-declaration-system")) (store-layout-file . ,(string-append closure-path "/metadata/store-layout.scm")) (install-metadata-path . ,install-metadata-path) (install-spec . ,install-spec))) @@ -321,6 +357,9 @@ (freebsd-source-materializations-file . ,(string-append closure-path "/metadata/freebsd-source-materializations.scm")) (host-base-provenance-file . ,(string-append closure-path "/metadata/host-base-provenance.scm")) + (system-declaration-file . ,(string-append closure-path "/metadata/system-declaration.scm")) + (system-declaration-info-file . ,(string-append closure-path "/metadata/system-declaration-info.scm")) + (system-declaration-system-file . ,(string-append closure-path "/metadata/system-declaration-system")) (store-layout-file . ,(string-append closure-path "/metadata/store-layout.scm")))) (define* (populate-system-generation-layout os rootfs closure-path @@ -437,12 +476,18 @@ (store-dir "/frx/store") (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") - (shepherd-prefix "/tmp/shepherd-freebsd-validate-install")) + (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f)) (let* ((closure (materialize-operating-system os #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (closure-path (assoc-ref closure 'closure-path))) (populate-rootfs-from-closure os rootfs closure-path))) @@ -783,6 +828,9 @@ (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f) (efi-size "64m") (root-size #f) (disk-capacity #f) @@ -795,7 +843,10 @@ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (closure-path (assoc-ref closure 'closure-path)) (store-items (store-reference-closure (list closure-path))) (target-kind (if (string-prefix? "/dev/" target) @@ -910,6 +961,9 @@ (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f) (efi-size "64m") (root-size "256m") (disk-capacity #f) @@ -920,7 +974,10 @@ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (closure-path (assoc-ref closure 'closure-path)) (image-spec (operating-system-image-spec os #:efi-size efi-size @@ -963,7 +1020,10 @@ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol) (copy-rootfs-for-image rootfs image-rootfs) (copy-store-items-into-rootfs image-rootfs store-dir store-items) (mkdir-p (string-append esp-stage "/EFI/BOOT")) @@ -1031,6 +1091,9 @@ (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f) (install-target-device "/dev/vtbd1") (efi-size "64m") (root-size "10g") @@ -1049,12 +1112,18 @@ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (installer-closure (materialize-operating-system installer-os #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (target-closure-path (assoc-ref target-closure 'closure-path)) (installer-closure-path (assoc-ref installer-closure 'closure-path)) (target-store-items (store-reference-closure (list target-closure-path))) @@ -1339,6 +1408,9 @@ (guile-prefix "/tmp/guile-freebsd-validate-install") (guile-extra-prefix "/tmp/guile-gnutls-freebsd-validate-install") (shepherd-prefix "/tmp/shepherd-freebsd-validate-install") + (declaration-source #f) + (declaration-origin #f) + (declaration-system-symbol #f) (install-target-device "/dev/vtbd0") (root-size #f) (installer-host-name (string-append (operating-system-host-name os) @@ -1355,12 +1427,18 @@ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (installer-closure (materialize-operating-system installer-os #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix)) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin declaration-origin + #:declaration-system-symbol declaration-system-symbol)) (target-closure-path (assoc-ref target-closure 'closure-path)) (installer-closure-path (assoc-ref installer-closure 'closure-path)) (target-closure-store-items (store-reference-closure (list target-closure-path))) diff --git a/modules/fruix/system/freebsd/model.scm b/modules/fruix/system/freebsd/model.scm index 151fed3..59375d6 100644 --- a/modules/fruix/system/freebsd/model.scm +++ b/modules/fruix/system/freebsd/model.scm @@ -369,8 +369,12 @@ "metadata/freebsd-base.scm" "metadata/host-base-provenance.scm" "metadata/store-layout.scm" + "metadata/system-declaration.scm" + "metadata/system-declaration-info.scm" + "metadata/system-declaration-system" "activate" "shepherd/init.scm" + "share/fruix/node/scripts/fruix.scm" "usr/local/bin/fruix") (if (operating-system-native-build-result os) '("metadata/promoted-native-build-result.scm") @@ -404,6 +408,8 @@ (base-packages . ,(package-names (operating-system-base-packages os))) (development-package-count . ,(length (operating-system-development-packages os))) (development-packages . ,(package-names (operating-system-development-packages os))) + (installed-system-command-surface-version . "2") + (bundled-fruix-node-cli-version . "1") (development-environment-helper-version . ,(if (null? (operating-system-development-packages os)) #f "1")) (self-hosted-native-build-helper-version diff --git a/modules/fruix/system/freebsd/render.scm b/modules/fruix/system/freebsd/render.scm index 3da46e3..38aa05f 100644 --- a/modules/fruix/system/freebsd/render.scm +++ b/modules/fruix/system/freebsd/render.scm @@ -4,6 +4,7 @@ #:use-module (ice-9 match) #:use-module (srfi srfi-1) #:use-module (srfi srfi-13) + #:use-module (rnrs io ports) #:export (operating-system-generated-files render-activation-rc-script render-rc-script)) @@ -467,7 +468,57 @@ "}\n\n" "load_rc_config $name\n" "run_rc_command \"$1\"\n"))) -(define (render-installed-system-fruix os) + +(define (path-parent path) + (let ((index (string-rindex path #\/))) + (cond + ((not index) ".") + ((zero? index) "/") + (else (substring path 0 index))))) + +(define (read-source-file-string path) + (call-with-input-file path get-string-all)) + +(define (bundled-fruix-node-files) + (let* ((repo-root (or (getenv "FRUIX_PROJECT_ROOT") + (let ((render-file (current-filename))) + (and render-file + (path-parent + (path-parent + (path-parent + (path-parent + (path-parent render-file))))))) + (getcwd))) + (guix-root (or (getenv "GUIX_SOURCE_DIR") + (string-append (getenv "HOME") "/repos/guix"))) + (specs `((,(string-append repo-root "/scripts/fruix.scm") + . "share/fruix/node/scripts/fruix.scm") + (,(string-append repo-root "/modules/fruix/packages/freebsd.scm") + . "share/fruix/node/modules/fruix/packages/freebsd.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd.scm") + . "share/fruix/node/modules/fruix/system/freebsd.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/build.scm") + . "share/fruix/node/modules/fruix/system/freebsd/build.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/executor.scm") + . "share/fruix/node/modules/fruix/system/freebsd/executor.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/media.scm") + . "share/fruix/node/modules/fruix/system/freebsd/media.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/model.scm") + . "share/fruix/node/modules/fruix/system/freebsd/model.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/render.scm") + . "share/fruix/node/modules/fruix/system/freebsd/render.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/source.scm") + . "share/fruix/node/modules/fruix/system/freebsd/source.scm") + (,(string-append repo-root "/modules/fruix/system/freebsd/utils.scm") + . "share/fruix/node/modules/fruix/system/freebsd/utils.scm") + (,(string-append guix-root "/guix/build/utils.scm") + . "share/fruix/node/guix/guix/build/utils.scm")))) + (map (lambda (entry) + (cons (cdr entry) + (read-source-file-string (car entry)))) + specs))) + +(define (render-installed-system-fruix os guile-store guile-extra-store shepherd-store) (string-append "#!/bin/sh\n" "set -eu\n" @@ -485,6 +536,17 @@ "rollback_generation_file=\"$system_root/rollback-generation\"\n" "gcroots_root=/frx/var/fruix/gcroots\n" "run_current_link=/run/current-system\n" + "node_root=/run/current-system/share/fruix/node\n" + "node_script=\"$node_root/scripts/fruix.scm\"\n" + "node_module_root=\"$node_root/modules\"\n" + "node_guix_root=\"$node_root/guix\"\n" + "declaration_file=/run/current-system/metadata/system-declaration.scm\n" + "declaration_info_file=/run/current-system/metadata/system-declaration-info.scm\n" + "declaration_system_file=/run/current-system/metadata/system-declaration-system\n" + "default_store_dir=/frx/store\n" + "guile_store='" guile-store "'\n" + "guile_extra_store='" guile-extra-store "'\n" + "shepherd_store='" shepherd-store "'\n" "layout_version=2\n" "host_name='" (operating-system-host-name os) "'\n" "ready_marker='" (operating-system-ready-marker os) "'\n" @@ -493,6 +555,8 @@ "{\n" " cat <<'EOF'\n" "Usage: fruix system status\n" + " fruix system build [DECLARATION [--system NAME] ...]\n" + " fruix system reconfigure [DECLARATION [--system NAME] ...]\n" " fruix system switch /frx/store/...-fruix-system-...\n" " fruix system rollback\n" "EOF\n" @@ -514,6 +578,10 @@ " tr -d '\\n' < \"$1\"\n" " fi\n" "}\n\n" + "default_system_name()\n" + "{\n" + " read_file_maybe \"$declaration_system_file\"\n" + "}\n\n" "symlink_force()\n" "{\n" " target=$1\n" @@ -537,6 +605,79 @@ " [ -f \"$closure/shepherd/init.scm\" ] || die \"closure is missing shepherd config: $closure\"\n" " [ -f \"$closure/boot/loader.efi\" ] || die \"closure is missing loader.efi: $closure\"\n" "}\n\n" + "ensure_default_declaration()\n" + "{\n" + " [ -f \"$declaration_file\" ] || die \"current declaration file is missing: $declaration_file\"\n" + " [ -f \"$declaration_info_file\" ] || die \"current declaration info file is missing: $declaration_info_file\"\n" + " current_system_name=$(default_system_name)\n" + " [ -n \"$current_system_name\" ] || die \"current declaration is missing a system variable name\"\n" + "}\n\n" + "run_node_cli()\n" + "{\n" + " [ -x \"$guile_store/bin/guile\" ] || die \"missing Guile runtime: $guile_store/bin/guile\"\n" + " [ -f \"$node_script\" ] || die \"missing bundled Fruix node CLI: $node_script\"\n" + " [ -d \"$node_module_root\" ] || die \"missing bundled Fruix modules: $node_module_root\"\n" + " [ -d \"$node_guix_root\" ] || die \"missing bundled Guix modules: $node_guix_root\"\n" + " guile_load_path=\"$node_module_root:$node_guix_root:$shepherd_store/share/guile/site/3.0:$guile_extra_store/share/guile/site/3.0\"\n" + " guile_system_path=\"$guile_store/share/guile/3.0:$guile_store/share/guile/site/3.0:$guile_store/share/guile/site:$guile_store/share/guile\"\n" + " guile_system_compiled_path=\"$guile_store/lib/guile/3.0/ccache:$guile_store/lib/guile/3.0/site-ccache\"\n" + " guile_load_compiled_path=\"$shepherd_store/lib/guile/3.0/site-ccache:$guile_extra_store/lib/guile/3.0/site-ccache\"\n" + " guile_system_extensions_path=\"$guile_store/lib/guile/3.0/extensions\"\n" + " guile_extensions_path=\"$guile_extra_store/lib/guile/3.0/extensions\"\n" + " ld_library_path=\"$guile_extra_store/lib:$guile_store/lib:/usr/local/lib\"\n" + " env \\\n" + " GUILE_AUTO_COMPILE=0 \\\n" + " GUILE_SYSTEM_PATH=\"$guile_system_path\" \\\n" + " GUILE_LOAD_PATH=\"$guile_load_path\" \\\n" + " GUILE_SYSTEM_COMPILED_PATH=\"$guile_system_compiled_path\" \\\n" + " GUILE_LOAD_COMPILED_PATH=\"$guile_load_compiled_path\" \\\n" + " GUILE_SYSTEM_EXTENSIONS_PATH=\"$guile_system_extensions_path\" \\\n" + " GUILE_EXTENSIONS_PATH=\"$guile_extensions_path\" \\\n" + " LD_LIBRARY_PATH=\"$ld_library_path\" \\\n" + " GUILE_PREFIX=\"$guile_store\" \\\n" + " GUILE_EXTRA_PREFIX=\"$guile_extra_store\" \\\n" + " SHEPHERD_PREFIX=\"$shepherd_store\" \\\n" + " FRUIX_GUILE_STORE=\"$guile_store\" \\\n" + " FRUIX_GUILE_EXTRA_STORE=\"$guile_extra_store\" \\\n" + " FRUIX_SHEPHERD_STORE=\"$shepherd_store\" \\\n" + " GUIX_SOURCE_DIR=\"$node_guix_root\" \\\n" + " FRUIX_PROJECT_ROOT=\"$node_root\" \\\n" + " \"$guile_store/bin/guile\" --no-auto-compile -s \"$node_script\" \"$@\"\n" + "}\n\n" + "system_build()\n" + "{\n" + " if [ $# -eq 0 ]; then\n" + " ensure_default_declaration\n" + " run_node_cli system build \"$declaration_file\" --system \"$current_system_name\" --store \"$default_store_dir\"\n" + " else\n" + " run_node_cli system build \"$@\"\n" + " fi\n" + "}\n\n" + "reconfigure_system()\n" + "{\n" + " build_output=$(mktemp /tmp/fruix-system-reconfigure.XXXXXX)\n" + " if [ $# -eq 0 ]; then\n" + " ensure_default_declaration\n" + " if ! run_node_cli system build \"$declaration_file\" --system \"$current_system_name\" --store \"$default_store_dir\" > \"$build_output\"; then\n" + " cat \"$build_output\" >&2 || true\n" + " rm -f \"$build_output\"\n" + " exit 1\n" + " fi\n" + " else\n" + " if ! run_node_cli system build \"$@\" > \"$build_output\"; then\n" + " cat \"$build_output\" >&2 || true\n" + " rm -f \"$build_output\"\n" + " exit 1\n" + " fi\n" + " fi\n" + " closure=$(sed -n 's/^closure_path=//p' \"$build_output\" | tail -n 1)\n" + " [ -n \"$closure\" ] || die \"failed to recover closure_path from in-system build output\"\n" + " cat \"$build_output\"\n" + " rm -f \"$build_output\"\n" + " switch_to_closure \"$closure\"\n" + " printf 'reconfigure_closure=%s\\n' \"$closure\"\n" + " printf 'reboot_required=true\\n'\n" + "}\n\n" "max_generation_number()\n" "{\n" " max=0\n" @@ -726,6 +867,14 @@ " [ $# -eq 2 ] || { usage >&2; exit 1; }\n" " status\n" " ;;\n" + " build)\n" + " shift 2\n" + " system_build \"$@\"\n" + " ;;\n" + " reconfigure)\n" + " shift 2\n" + " reconfigure_system \"$@\"\n" + " ;;\n" " switch)\n" " [ $# -eq 3 ] || { usage >&2; exit 1; }\n" " switch_to_closure \"$3\"\n" @@ -1030,7 +1179,9 @@ #:guile-extra-store guile-extra-store #:shepherd-store shepherd-store)) ("shepherd/init.scm" . ,(render-shepherd-config os)) - ("usr/local/bin/fruix" . ,(render-installed-system-fruix os))) + ("usr/local/bin/fruix" + . ,(render-installed-system-fruix os guile-store guile-extra-store shepherd-store))) + (bundled-fruix-node-files) (if (null? (operating-system-development-packages os)) '() `(("usr/local/bin/fruix-development-environment" diff --git a/scripts/fruix.scm b/scripts/fruix.scm index 79bd607..8618b30 100644 --- a/scripts/fruix.scm +++ b/scripts/fruix.scm @@ -6,7 +6,8 @@ (ice-9 format) (ice-9 match) (srfi srfi-1) - (srfi srfi-13)) + (srfi srfi-13) + (rnrs io ports)) (define (usage code) (format (if (= code 0) #t (current-error-port)) @@ -69,6 +70,9 @@ Common options:\n\ (format #t "~a=~a~%" (car field) (stringify (cdr field)))) fields)) +(define (read-file-string file) + (call-with-input-file file get-string-all)) + (define (lookup-bound-value module symbol) (let ((var (module-variable module symbol))) (and var (variable-ref var)))) @@ -629,7 +633,11 @@ Common options:\n\ (lambda (os resolved-symbol) (let* ((guile-prefix (or (getenv "GUILE_PREFIX") "/tmp/guile-freebsd-validate-install")) (guile-extra-prefix (or (getenv "GUILE_EXTRA_PREFIX") "/tmp/guile-gnutls-freebsd-validate-install")) - (shepherd-prefix (or (getenv "SHEPHERD_PREFIX") "/tmp/shepherd-freebsd-validate-install"))) + (shepherd-prefix (or (getenv "SHEPHERD_PREFIX") "/tmp/shepherd-freebsd-validate-install")) + (guile-store-path (getenv "FRUIX_GUILE_STORE")) + (guile-extra-store-path (getenv "FRUIX_GUILE_EXTRA_STORE")) + (shepherd-store-path (getenv "FRUIX_SHEPHERD_STORE")) + (declaration-source (read-file-string os-file))) (cond ((string=? action "build") (emit-system-build-metadata @@ -638,7 +646,13 @@ Common options:\n\ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix))) + #:shepherd-prefix shepherd-prefix + #:guile-store-path guile-store-path + #:guile-extra-store-path guile-extra-store-path + #:shepherd-store-path shepherd-store-path + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol))) ((string=? action "rootfs") (unless rootfs (error "rootfs action requires ROOTFS-DIR or --rootfs DIR")) @@ -646,7 +660,10 @@ Common options:\n\ #:store-dir store-dir #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix - #:shepherd-prefix shepherd-prefix))) + #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol))) (emit-metadata `((action . "rootfs") (os_file . ,os-file) @@ -664,6 +681,9 @@ Common options:\n\ #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol #:root-size (or root-size "256m") #:disk-capacity disk-capacity))) ((string=? action "installer") @@ -674,6 +694,9 @@ Common options:\n\ #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol #:install-target-device (or install-target-device "/dev/vtbd1") #:root-size (or root-size "10g") #:disk-capacity disk-capacity))) @@ -685,6 +708,9 @@ Common options:\n\ #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol #:install-target-device (or install-target-device "/dev/vtbd0") #:root-size root-size))) ((string=? action "install") @@ -698,6 +724,9 @@ Common options:\n\ #:guile-prefix guile-prefix #:guile-extra-prefix guile-extra-prefix #:shepherd-prefix shepherd-prefix + #:declaration-source declaration-source + #:declaration-origin os-file + #:declaration-system-symbol resolved-symbol #:root-size root-size #:disk-capacity disk-capacity)))))))))) ((string=? command "source") diff --git a/tests/system/postphase20-installed-node-operating-system.scm.in b/tests/system/postphase20-installed-node-operating-system.scm.in new file mode 100644 index 0000000..3c6b13d --- /dev/null +++ b/tests/system/postphase20-installed-node-operating-system.scm.in @@ -0,0 +1,73 @@ +(use-modules (fruix system freebsd) + (fruix packages freebsd)) + +(define postphase20-promoted-native-build-result + (promoted-native-build-result + #:store-path "__PROMOTED_RESULT_STORE__")) + +(define postphase20-installed-node-operating-system + (operating-system-from-promoted-native-build-result + postphase20-promoted-native-build-result + #:host-name "__HOST_NAME__" + #: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")) + #:init-mode 'shepherd-pid1 + #:ready-marker "/var/lib/fruix/ready" + #:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__"))) diff --git a/tests/system/run-phase8-system-image.sh b/tests/system/run-phase8-system-image.sh index 3cecf3b..b6df0b0 100755 --- a/tests/system/run-phase8-system-image.sh +++ b/tests/system/run-phase8-system-image.sh @@ -104,11 +104,11 @@ image_size_bytes=$(stat -f '%z' "$disk_image") closure_base=$(basename "$closure_path") case "$image_store_path" in - /frx/store/*-fruix-bhyve-image-fruix-freebsd) : ;; + /frx/store/*-fruix-bhyve-image-*) : ;; *) echo "unexpected image store path: $image_store_path" >&2; exit 1 ;; esac case "$disk_image" in - /frx/store/*-fruix-bhyve-image-fruix-freebsd/disk.img) : ;; + /frx/store/*-fruix-bhyve-image-*/disk.img) : ;; *) echo "unexpected disk image path: $disk_image" >&2; exit 1 ;; esac @@ -142,7 +142,7 @@ if [ -L "$mnt_root/etc/master.passwd" ]; then master_passwd_kind=symlink; elif [ 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 '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; } +grep -E '^hostname=".+"$' "$rc_conf_image" >/dev/null || { echo "rc.conf is missing hostname" >&2; exit 1; } [ -f "$host_base_provenance_file" ] || { echo "missing host base provenance file: $host_base_provenance_file" >&2; exit 1; } [ -f "$store_layout_file" ] || { echo "missing store layout file: $store_layout_file" >&2; exit 1; } [ -n "$host_freebsd_version" ] || { echo "missing host freebsd version provenance" >&2; exit 1; } diff --git a/tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh b/tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh new file mode 100755 index 0000000..2277ce5 --- /dev/null +++ b/tests/system/run-postphase20-installed-node-build-reconfigure-xcpng.sh @@ -0,0 +1,271 @@ +#!/bin/sh +set -eu + +repo_root=${PROJECT_ROOT:-$(pwd)} +os_template=${OS_TEMPLATE:-$repo_root/tests/system/postphase20-installed-node-operating-system.scm.in} +system_name=${SYSTEM_NAME:-postphase20-installed-node-operating-system} +result_store=${RESULT_STORE:-/frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest} +root_size=${ROOT_SIZE:-12g} +current_host_name=${CURRENT_HOST_NAME:-fruix-node-current} +candidate_host_name=${CANDIDATE_HOST_NAME:-fruix-node-canary} +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} +cleanup=0 + +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-postphase20-installed-node-xcpng.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +current_os_file=$workdir/current-operating-system.scm +candidate_os_file=$workdir/candidate-operating-system.scm +inner_metadata=$workdir/postphase20-installed-node-inner-metadata.txt +current_build_out=$workdir/current-build.txt +candidate_build_out=$workdir/candidate-build.txt +reconfigure_out=$workdir/reconfigure.txt +rollback_out=$workdir/rollback.txt +post_reconfigure_status=$workdir/post-reconfigure-status.txt +post_boot_candidate_status=$workdir/post-boot-candidate-status.txt +post_boot_rollback_status=$workdir/post-boot-rollback-status.txt +metadata_file=$workdir/postphase20-installed-node-build-reconfigure-xcpng-metadata.txt + +cleanup_workdir() { + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +[ -f "$os_template" ] || { echo "missing operating-system template: $os_template" >&2; exit 1; } +[ -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; } +[ -d "$result_store" ] || { echo "promoted result store does not exist: $result_store" >&2; exit 1; } + +root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file") + +render_os() { + output=$1 + host_name=$2 + sed \ + -e "s|__PROMOTED_RESULT_STORE__|$result_store|g" \ + -e "s|__HOST_NAME__|$host_name|g" \ + -e "s|__ROOT_AUTHORIZED_KEY__|$root_authorized_key|g" \ + "$os_template" > "$output" +} + +render_os "$current_os_file" "$current_host_name" +render_os "$candidate_os_file" "$candidate_host_name" + +KEEP_WORKDIR=1 WORKDIR="$workdir/boot" METADATA_OUT="$inner_metadata" \ + ROOT_AUTHORIZED_KEY_FILE="$root_authorized_key_file" \ + ROOT_SSH_PRIVATE_KEY_FILE="$root_ssh_private_key_file" \ + OS_TEMPLATE="$current_os_file" SYSTEM_NAME="$system_name" ROOT_SIZE="$root_size" \ + "$repo_root/tests/system/run-phase11-shepherd-pid1-xcpng.sh" + +phase8_metadata=$(sed -n 's/^phase8_metadata=//p' "$inner_metadata") +closure_path=$(sed -n 's/^closure_path=//p' "$inner_metadata") +closure_base=$(sed -n 's/^closure_base=//p' "$inner_metadata") +guest_ip=$(sed -n 's/^guest_ip=//p' "$inner_metadata") +vm_id=$(sed -n 's/^vm_id=//p' "$inner_metadata") +vdi_id=$(sed -n 's/^vdi_id=//p' "$inner_metadata") +shepherd_pid=$(sed -n 's/^shepherd_pid=//p' "$inner_metadata") +sshd_status=$(sed -n 's/^sshd_status=//p' "$inner_metadata") +compat_prefix_shims=$(sed -n 's/^compat_prefix_shims=//p' "$inner_metadata") +guile_module_smoke=$(sed -n 's/^guile_module_smoke=//p' "$inner_metadata") +activate_log=$(sed -n 's/^activate_log=//p' "$inner_metadata") + +[ "$shepherd_pid" = 1 ] || { echo "shepherd was not PID 1" >&2; exit 1; } +[ "$sshd_status" = running ] || { echo "sshd is not running" >&2; exit 1; } +[ "$compat_prefix_shims" = absent ] || { echo "compatibility prefix shims reappeared" >&2; exit 1; } +[ "$guile_module_smoke" = ok ] || { echo "guest Guile module smoke failed" >&2; exit 1; } +case "$activate_log" in + *fruix-activate:done*) : ;; + *) echo "activation log does not show success" >&2; exit 1 ;; +esac + +for path in \ + "$closure_path/metadata/system-declaration.scm" \ + "$closure_path/metadata/system-declaration-info.scm" \ + "$closure_path/metadata/system-declaration-system" \ + "$closure_path/share/fruix/node/scripts/fruix.scm" \ + "$closure_path/share/fruix/node/modules/fruix/system/freebsd/render.scm" \ + "$closure_path/share/fruix/node/guix/guix/build/utils.scm" +do + [ -f "$path" ] || { + echo "required installed-node path missing: $path" >&2 + exit 1 + } +done + +grep -F "$current_host_name" "$closure_path/metadata/system-declaration.scm" >/dev/null || { + echo "embedded declaration does not mention current host name" >&2 + exit 1 +} +grep -F "$system_name" "$closure_path/metadata/system-declaration-system" >/dev/null || { + echo "embedded declaration system name is missing" >&2 + exit 1 +} + +ssh_guest() { + ssh -i "$root_ssh_private_key_file" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=5 \ + root@"$guest_ip" "$@" +} + +scp_guest() { + scp -O -i "$root_ssh_private_key_file" \ + -o BatchMode=yes \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=5 \ + "$@" +} + +wait_for_ssh() { + for attempt in $(jot 120 1 120); do + if ssh_guest 'service sshd onestatus >/dev/null 2>&1' >/dev/null 2>&1; then + return 0 + fi + sleep 2 + done + return 1 +} + +reboot_guest() { + ssh_guest 'shutdown -r now >/dev/null 2>&1 || reboot >/dev/null 2>&1 || true' >/dev/null 2>&1 || true + sleep 5 + wait_for_ssh || { + echo "guest did not return over SSH after reboot" >&2 + exit 1 + } +} + +ssh_guest 'sh -s' </dev/null +EOF + +ssh_guest '/usr/local/bin/fruix system build' > "$current_build_out" +current_built_closure=$(sed -n 's/^closure_path=//p' "$current_build_out" | tail -n 1) +[ -n "$current_built_closure" ] || { echo "missing closure_path from in-system build output" >&2; exit 1; } +case "$current_built_closure" in + /frx/store/*-fruix-system-$current_host_name) : ;; + *) + echo "in-system build of current declaration produced an unexpected closure path: $current_built_closure" >&2 + exit 1 + ;; +esac + +scp_guest "$candidate_os_file" root@"$guest_ip":/root/candidate.scm >/dev/null +ssh_guest "/usr/local/bin/fruix system build /root/candidate.scm --system $system_name" > "$candidate_build_out" +candidate_closure=$(sed -n 's/^closure_path=//p' "$candidate_build_out" | tail -n 1) +[ -n "$candidate_closure" ] || { echo "missing candidate closure_path from in-system build output" >&2; exit 1; } +[ "$candidate_closure" != "$closure_path" ] || { + echo "candidate closure unexpectedly matches current closure" >&2 + exit 1 +} + +ssh_guest "/usr/local/bin/fruix system reconfigure /root/candidate.scm --system $system_name" > "$reconfigure_out" +reconfigure_closure=$(sed -n 's/^reconfigure_closure=//p' "$reconfigure_out" | tail -n 1) +reconfigure_current_generation=$(sed -n 's/^current_generation=//p' "$reconfigure_out" | tail -n 1) +reconfigure_current_closure=$(sed -n 's/^current_closure=//p' "$reconfigure_out" | tail -n 1) +reconfigure_rollback_generation=$(sed -n 's/^rollback_generation=//p' "$reconfigure_out" | tail -n 1) +reconfigure_rollback_closure=$(sed -n 's/^rollback_closure=//p' "$reconfigure_out" | tail -n 1) +[ "$reconfigure_closure" = "$candidate_closure" ] || { echo "reconfigure closure mismatch" >&2; exit 1; } +[ "$reconfigure_current_generation" = 2 ] || { echo "unexpected current generation after reconfigure: $reconfigure_current_generation" >&2; exit 1; } +[ "$reconfigure_current_closure" = "$candidate_closure" ] || { echo "unexpected current closure after reconfigure" >&2; exit 1; } +[ "$reconfigure_rollback_generation" = 1 ] || { echo "unexpected rollback generation after reconfigure: $reconfigure_rollback_generation" >&2; exit 1; } +[ "$reconfigure_rollback_closure" = "$closure_path" ] || { echo "unexpected rollback closure after reconfigure" >&2; exit 1; } +ssh_guest '/usr/local/bin/fruix system status' > "$post_reconfigure_status" + +reboot_guest +candidate_hostname=$(ssh_guest 'hostname') +candidate_run_current=$(ssh_guest 'readlink /run/current-system') +ssh_guest '/usr/local/bin/fruix system status' > "$post_boot_candidate_status" +[ "$candidate_hostname" = "$candidate_host_name" ] || { echo "unexpected host name after candidate boot: $candidate_hostname" >&2; exit 1; } +[ "$candidate_run_current" = "$candidate_closure" ] || { echo "unexpected current closure after candidate boot: $candidate_run_current" >&2; exit 1; } + +ssh_guest '/usr/local/bin/fruix system rollback' > "$rollback_out" +rollback_current_generation=$(sed -n 's/^current_generation=//p' "$rollback_out" | tail -n 1) +rollback_current_closure=$(sed -n 's/^current_closure=//p' "$rollback_out" | tail -n 1) +rollback_rollback_generation=$(sed -n 's/^rollback_generation=//p' "$rollback_out" | tail -n 1) +rollback_rollback_closure=$(sed -n 's/^rollback_closure=//p' "$rollback_out" | tail -n 1) +[ "$rollback_current_generation" = 1 ] || { echo "unexpected current generation after rollback: $rollback_current_generation" >&2; exit 1; } +[ "$rollback_current_closure" = "$closure_path" ] || { echo "unexpected current closure after rollback" >&2; exit 1; } +[ "$rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback: $rollback_rollback_generation" >&2; exit 1; } +[ "$rollback_rollback_closure" = "$candidate_closure" ] || { echo "unexpected rollback closure after rollback" >&2; exit 1; } + +reboot_guest +rollback_hostname=$(ssh_guest 'hostname') +rollback_run_current=$(ssh_guest 'readlink /run/current-system') +ssh_guest '/usr/local/bin/fruix system status' > "$post_boot_rollback_status" +[ "$rollback_hostname" = "$current_host_name" ] || { echo "unexpected host name after rollback boot: $rollback_hostname" >&2; exit 1; } +[ "$rollback_run_current" = "$closure_path" ] || { echo "unexpected current closure after rollback boot: $rollback_run_current" >&2; exit 1; } + +cat > "$metadata_file" <