Files
fruix-bootstrap/docs/reports/postphase20-system-deploy-freebsd.md
2026-04-06 23:10:31 +02:00

5.7 KiB

Post-Phase 20: host-driven fruix system deploy

Date: 2026-04-06

Goal

Add the first real host-driven deployment command that can move a built Fruix system closure onto an already installed Fruix node and stage a generation switch there.

This closes a major remaining product gap between:

  • building a closure locally
  • manually copying or pre-seeding it onto a target
  • manually invoking fruix system switch on the target

The new operator-facing command is:

  • fruix system deploy

What changed

Host CLI gained fruix system deploy

The main Fruix CLI now supports:

  • fruix system deploy TARGET DECLARATION [--system NAME] ...
  • fruix system deploy TARGET /frx/store/...-fruix-system-...

Current deploy options are intentionally narrow:

  • --host HOST
  • --user USER
  • --port PORT
  • --identity FILE
  • --reboot

The first positional TARGET remains the usual path, while --host is available for explicitness.

Deploy now performs closure transfer and remote switch

Current host-driven flow is:

  1. build or resolve the selected system closure locally
  2. compute the recursive store closure from .references
  3. copy only missing store items onto the remote node under /frx/store
  4. run on the target:
    • /usr/local/bin/fruix system switch /frx/store/...-fruix-system-...
  5. optionally reboot the target and wait for SSH to return
  6. emit deployment metadata describing:
    • local closure path
    • remote closure path
    • transfer counts
    • generation switch state
    • reboot state

This makes deploy a real Fruix workflow rather than just a shell-harness convention.

Installed helper surface was bumped again

The installed helper generation marker was bumped so node closures do not silently reuse the older helper command surface.

The installed helper also now accepts:

  • fruix system deploy ...

by forwarding to the bundled node CLI.

Store hash helper was hardened

While validating deploy, a real bug was exposed in the shared store-hash helper:

  • sha256-string used a fixed temporary path under /tmp

That could fail when an earlier root-run left the temp file behind with incompatible ownership.

The helper now uses a fresh mktemp path and removes it afterward.

Validation assets

Added:

  • tests/system/run-postphase20-system-deploy-xcpng.sh

This harness boots an installed Fruix node on the approved XCP-ng path, deploys a candidate declaration from the host with the new Fruix command, verifies the candidate boot, then rolls back and verifies the original generation again.

Validation performed

Approved real XCP-ng path:

  • VM 90490f2e-e8fc-4b7a-388e-5c26f0157289
  • VDI 0f1f90d3-48ca-4fa2-91d8-fc6339b95743

Source promoted native-base result used by both current and candidate declarations:

  • /frx/store/ffe44f5d1ba576e1f811ad3fe3a526a242b5c4a5-fruix-native-build-result-15.0-STABLE-ssh-guest

Validated flow:

  1. boot an installed Fruix node with host name:
    • fruix-node-deploy-current
  2. confirm the installed node exposes the expected Fruix node helper/runtime state
  3. run on the host:
sudo env HOME="$HOME" PATH="$PATH" \
  ./bin/fruix system deploy 192.168.213.62 candidate-operating-system.scm \
  --system postphase20-installed-node-operating-system \
  --user root \
  --identity /home/self/.ssh/id_ed25519 \
  --reboot
  1. verify that Fruix:
    • built the candidate closure locally
    • transferred only missing store items
    • switched the remote node to generation 2
    • rebooted into the candidate host name:
      • fruix-node-deploy-canary
  2. run on the target:
    • fruix system rollback
  3. reboot and verify the original generation returns

Passing run:

  • PASS postphase20-system-deploy-xcpng

Representative validated metadata:

action=deploy
source_kind=declaration
deployed_closure=/frx/store/1ae11838a0c8cd6a158c28e779c32c21cb50bf10-fruix-system-fruix-node-deploy-canary
remote_closure_path=/frx/store/1ae11838a0c8cd6a158c28e779c32c21cb50bf10-fruix-system-fruix-node-deploy-canary
target_host=192.168.213.62
target_user=root
target_port=22
reference_count=20
transfer_item_count=2
skipped_item_count=18
deploy_current_generation=2
deploy_current_closure=/frx/store/1ae11838a0c8cd6a158c28e779c32c21cb50bf10-fruix-system-fruix-node-deploy-canary
deploy_rollback_generation=1
deploy_rollback_closure=/frx/store/a7169a2db9bc3492fb8b11f81d55655dbbc0e9c2-fruix-system-fruix-node-deploy-current
reboot_requested=true
reboot_completed=true
remote_hostname=fruix-node-deploy-canary
remote_run_current=/frx/store/1ae11838a0c8cd6a158c28e779c32c21cb50bf10-fruix-system-fruix-node-deploy-canary
rollback_hostname=fruix-node-deploy-current
rollback_run_current=/frx/store/a7169a2db9bc3492fb8b11f81d55655dbbc0e9c2-fruix-system-fruix-node-deploy-current
system_deploy=ok

Result

Fruix now has its first validated host-driven deployment command for installed nodes.

That means Fruix can now:

  • build a candidate closure on the host
  • transfer the missing store closure onto a target Fruix node
  • stage the target generation switch automatically
  • reboot into the candidate generation
  • keep the target's rollback path intact

This is a meaningful product step toward “real Fruix nodes” because deployment is no longer just:

  • build here
  • manually copy somehow
  • log in and switch by hand

It is now a real Fruix command with validated end-to-end behavior on the approved XCP-ng path.

Next likely follow-ups

The next refinements are now clearer:

  • richer deploy targeting and transport policy
  • better transfer/import ergonomics than the current minimal SSH/tar path
  • integration between build-base, promoted native-base identities, and deploy policy
  • a first-class jail executor for local native-base builds
  • a clearer fruix system upgrade policy