system: validate installed rollback workflow
This commit is contained in:
@@ -48,13 +48,12 @@ That path remains the active runtime boundary used by activation and service wir
|
|||||||
|
|
||||||
Fruix avoids in-place mutation of an older deployed closure.
|
Fruix avoids in-place mutation of an older deployed closure.
|
||||||
|
|
||||||
The validated rollback story today is:
|
The validated rollback story now has two layers:
|
||||||
|
|
||||||
- keep the earlier declaration
|
- declaration-level rollback by rebuilding/redeploying an earlier declaration
|
||||||
- rebuild or rematerialize it
|
- installed-system rollback between already-recorded generations on the target itself
|
||||||
- boot or redeploy that earlier closure again
|
|
||||||
|
|
||||||
That is Guix-like in spirit even though Fruix does not yet expose the same installed-system rollback command surface.
|
That is Guix-like in spirit, although Fruix still exposes a smaller installed-system workflow than Guix's more mature `reconfigure` model.
|
||||||
|
|
||||||
### Generation-style metadata and roots
|
### Generation-style metadata and roots
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ Guix heavily reuses its profile-generation model and represents a lot of meaning
|
|||||||
|
|
||||||
Fruix keeps the **semantics** but uses a more explicit metadata-oriented layout for installed systems.
|
Fruix keeps the **semantics** but uses a more explicit metadata-oriented layout for installed systems.
|
||||||
|
|
||||||
Current Fruix layout:
|
Current Fruix layout starts as:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/var/lib/fruix/system/
|
/var/lib/fruix/system/
|
||||||
@@ -92,6 +91,20 @@ Current Fruix layout:
|
|||||||
install.scm
|
install.scm
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After a validated installed-system switch, Fruix also records:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/fruix/system/
|
||||||
|
rollback -> generations/1
|
||||||
|
rollback-generation
|
||||||
|
generations/
|
||||||
|
2/
|
||||||
|
closure -> /frx/store/...-fruix-system-...
|
||||||
|
metadata.scm
|
||||||
|
provenance.scm
|
||||||
|
install.scm
|
||||||
|
```
|
||||||
|
|
||||||
Why Fruix does this:
|
Why Fruix does this:
|
||||||
|
|
||||||
- it makes deployment state easier to inspect directly
|
- it makes deployment state easier to inspect directly
|
||||||
@@ -154,27 +167,35 @@ Validated examples:
|
|||||||
|
|
||||||
So compared with Guix-on-Linux intuition, Fruix operators should be more explicit about target-device selection during installation and installer-media validation.
|
So compared with Guix-on-Linux intuition, Fruix operators should be more explicit about target-device selection during installation and installer-media validation.
|
||||||
|
|
||||||
## 6. Fruix does not yet have Guix-equivalent installed-system generation commands
|
## 6. Fruix now has a minimal installed-system generation command surface, but it is still smaller than Guix's
|
||||||
|
|
||||||
This is the biggest current operational gap.
|
This remains the biggest operational gap, but it is no longer a complete gap.
|
||||||
|
|
||||||
Fruix does **not** yet provide a mature equivalent of the familiar Guix System operator flow around in-place generation switching and rollback commands.
|
Installed Fruix systems now provide a small in-guest helper:
|
||||||
|
|
||||||
Today, Fruix rollback is mostly:
|
- `fruix system status`
|
||||||
|
- `fruix system switch /frx/store/...-fruix-system-...`
|
||||||
|
- `fruix system rollback`
|
||||||
|
|
||||||
- declaration-driven
|
What this gives you today:
|
||||||
- rebuild/redeploy based
|
|
||||||
|
|
||||||
rather than:
|
- explicit current-generation tracking
|
||||||
|
- explicit rollback-generation tracking
|
||||||
|
- in-place switching between already-staged closures on the installed target
|
||||||
|
- rollback without reinstalling the whole system image again
|
||||||
|
|
||||||
- switch current system generation in place through a dedicated command
|
What it still does **not** give you yet compared with Guix:
|
||||||
|
|
||||||
So if you come from Guix, assume that Fruix currently has:
|
- a mature `reconfigure`-style workflow that builds and stages the new closure from inside the target system
|
||||||
|
- automatic closure transfer/fetch as part of `switch`
|
||||||
|
- the broader generation-management UX Guix operators expect
|
||||||
|
|
||||||
|
So if you come from Guix, assume that Fruix now has:
|
||||||
|
|
||||||
- strong closure/store semantics
|
- strong closure/store semantics
|
||||||
- explicit install artifacts
|
- explicit install artifacts
|
||||||
- explicit generation metadata roots
|
- explicit generation metadata roots
|
||||||
- but a less mature installed-system generation UX
|
- a real but still modest installed-system switch/rollback UX
|
||||||
|
|
||||||
## Where Fruix is intentionally trying to improve on Guix's representation
|
## Where Fruix is intentionally trying to improve on Guix's representation
|
||||||
|
|
||||||
@@ -202,10 +223,11 @@ If you are already comfortable with Guix, the safest Fruix mental model today is
|
|||||||
- closure path
|
- closure path
|
||||||
- source provenance metadata
|
- source provenance metadata
|
||||||
- install metadata
|
- install metadata
|
||||||
5. think of rollback today as:
|
5. think of rollback in two layers:
|
||||||
- “redeploy the earlier declaration again”
|
- if the target already has the desired closure staged locally:
|
||||||
rather than:
|
- use `fruix system rollback`
|
||||||
- “switch to an already-managed previous generation in place”
|
- otherwise:
|
||||||
|
- redeploy the earlier declaration again
|
||||||
|
|
||||||
## Status summary
|
## Status summary
|
||||||
|
|
||||||
@@ -222,4 +244,4 @@ It differs most from Guix in:
|
|||||||
- source-provenance emphasis
|
- source-provenance emphasis
|
||||||
- installer-medium-oriented workflows
|
- installer-medium-oriented workflows
|
||||||
- generation-layout representation
|
- generation-layout representation
|
||||||
- and the still-maturing installed-system generation command surface
|
- and an installed-system generation command surface that now exists, but is still much smaller than Guix's
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ Fruix currently has:
|
|||||||
- `/var/lib/fruix/system`
|
- `/var/lib/fruix/system`
|
||||||
- explicit installed-system retention roots under:
|
- explicit installed-system retention roots under:
|
||||||
- `/frx/var/fruix/gcroots`
|
- `/frx/var/fruix/gcroots`
|
||||||
|
- a validated installed-system generation switch/rollback workflow via:
|
||||||
|
- `fruix system status`
|
||||||
|
- `fruix system switch`
|
||||||
|
- `fruix system rollback`
|
||||||
|
|
||||||
Validated boot modes still are:
|
Validated boot modes still are:
|
||||||
|
|
||||||
@@ -42,30 +46,35 @@ The validated Phase 18 installation work currently uses:
|
|||||||
|
|
||||||
## Latest completed achievement
|
## Latest completed achievement
|
||||||
|
|
||||||
### 2026-04-04 — Phase 19.2 completed
|
### 2026-04-04 — Phase 19.3 completed
|
||||||
|
|
||||||
Fruix now records an explicit installed-system generation layout and retention-root model instead of relying mainly on harness knowledge.
|
Fruix now has a validated installed-system operator workflow for switching to a staged candidate generation and rolling back to the recorded previous generation.
|
||||||
|
|
||||||
Highlights:
|
Highlights:
|
||||||
|
|
||||||
- added explicit installed-system generation layout under:
|
- installed systems now ship an in-guest Fruix deployment helper at:
|
||||||
- `/var/lib/fruix/system`
|
- `/usr/local/bin/fruix`
|
||||||
- added explicit installed-system retention roots under:
|
- validated in-guest command surface:
|
||||||
- `/frx/var/fruix/gcroots`
|
- `fruix system status`
|
||||||
- installed targets now record a first-generation deployment directory containing:
|
- `fruix system switch /frx/store/...-fruix-system-...`
|
||||||
- `closure`
|
- `fruix system rollback`
|
||||||
- `metadata.scm`
|
- switching now records explicit rollback state under:
|
||||||
- `provenance.scm`
|
- `/var/lib/fruix/system/rollback`
|
||||||
- `install.scm`
|
- `/var/lib/fruix/system/rollback-generation`
|
||||||
- `/run/current-system` remains the runtime boundary and still points directly at the active closure path
|
- switching now records explicit rollback GC roots under:
|
||||||
- added Guix-oriented operator notes in:
|
- `/frx/var/fruix/gcroots/rollback-system`
|
||||||
- `docs/GUIX_DIFFERENCES.md`
|
- the validated installed-system workflow now supports:
|
||||||
- updated deployment workflow documentation to reflect the new explicit generation model
|
- stage candidate closure in `/frx/store`
|
||||||
|
- switch to generation 2
|
||||||
|
- reboot into the candidate
|
||||||
|
- rollback to generation 1
|
||||||
|
- reboot into the restored current system
|
||||||
|
|
||||||
Validation:
|
Validation:
|
||||||
|
|
||||||
- `PASS phase19-generation-layout-qemu`
|
- `PASS phase19-installed-system-rollback-qemu`
|
||||||
- regression re-check:
|
- regression re-checks:
|
||||||
|
- `PASS phase19-generation-layout-qemu`
|
||||||
- `PASS phase18-installer-iso`
|
- `PASS phase18-installer-iso`
|
||||||
|
|
||||||
Reports:
|
Reports:
|
||||||
@@ -74,6 +83,7 @@ Reports:
|
|||||||
- `docs/GUIX_DIFFERENCES.md`
|
- `docs/GUIX_DIFFERENCES.md`
|
||||||
- `docs/reports/phase19-deployment-workflow-freebsd.md`
|
- `docs/reports/phase19-deployment-workflow-freebsd.md`
|
||||||
- `docs/reports/phase19-generation-layout-freebsd.md`
|
- `docs/reports/phase19-generation-layout-freebsd.md`
|
||||||
|
- `docs/reports/phase19-installed-system-rollback-freebsd.md`
|
||||||
|
|
||||||
## Recent major milestones
|
## Recent major milestones
|
||||||
|
|
||||||
@@ -99,6 +109,6 @@ Reports:
|
|||||||
|
|
||||||
Per `docs/PLAN_4.md`, the next planned step is:
|
Per `docs/PLAN_4.md`, the next planned step is:
|
||||||
|
|
||||||
- **Phase 19.3** — validate installed-system rollback through the intended operator-facing workflow
|
- **Phase 20.1** — validate a Fruix-managed development environment for native FreeBSD base work
|
||||||
|
|
||||||
Phase 19.2 is now complete: Fruix has an explicit installed-system generation layout and retention-root model on FreeBSD.
|
Phase 19.3 is now complete: Fruix validates installed-system generation switching and rollback through the intended operator-facing workflow.
|
||||||
|
|||||||
226
docs/reports/phase19-installed-system-rollback-freebsd.md
Normal file
226
docs/reports/phase19-installed-system-rollback-freebsd.md
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
# Phase 19.3: installed-system rollback workflow on FreeBSD
|
||||||
|
|
||||||
|
Date: 2026-04-04
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Phase 19.3 is about validating installed-system rollback through the intended operator-facing workflow, not only through host-side build/image redeploy harnesses.
|
||||||
|
|
||||||
|
The key question was:
|
||||||
|
|
||||||
|
- can an already-installed Fruix system move between recorded generations coherently, using an operator-facing command surface on the target itself?
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
The current Fruix solution is intentionally modest.
|
||||||
|
|
||||||
|
Fruix now provides a small installed-system helper on the target itself:
|
||||||
|
|
||||||
|
- `/usr/local/bin/fruix`
|
||||||
|
|
||||||
|
Validated in-guest commands:
|
||||||
|
|
||||||
|
- `fruix system status`
|
||||||
|
- `fruix system switch /frx/store/...-fruix-system-...`
|
||||||
|
- `fruix system rollback`
|
||||||
|
|
||||||
|
Important scope choice:
|
||||||
|
|
||||||
|
- `switch` assumes the candidate closure is already present on the target's `/frx/store`
|
||||||
|
- Fruix does **not** yet fetch or transfer that closure onto the target automatically
|
||||||
|
|
||||||
|
That keeps Phase 19.3 focused on generation-state correctness rather than introducing a larger store-transfer story prematurely.
|
||||||
|
|
||||||
|
## Implemented model
|
||||||
|
|
||||||
|
Installed systems now support the following validated operator pattern:
|
||||||
|
|
||||||
|
1. build a candidate closure with the host-side Fruix frontend
|
||||||
|
2. stage that closure into the installed system's `/frx/store`
|
||||||
|
3. run:
|
||||||
|
- `fruix system switch /frx/store/...candidate...`
|
||||||
|
4. reboot into the candidate generation
|
||||||
|
5. if needed, run:
|
||||||
|
- `fruix system rollback`
|
||||||
|
6. reboot into the recorded rollback generation
|
||||||
|
|
||||||
|
The installed system now records explicit rollback state under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/fruix/system/
|
||||||
|
current -> generations/N
|
||||||
|
current-generation
|
||||||
|
rollback -> generations/M
|
||||||
|
rollback-generation
|
||||||
|
generations/
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
and explicit rollback reachability under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/frx/var/fruix/gcroots/
|
||||||
|
current-system -> /frx/store/...current...
|
||||||
|
rollback-system -> /frx/store/...rollback...
|
||||||
|
system-1 -> ...
|
||||||
|
system-2 -> ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code changes
|
||||||
|
|
||||||
|
### `modules/fruix/system/freebsd/render.scm`
|
||||||
|
|
||||||
|
Added a generated in-guest Fruix deployment helper script under:
|
||||||
|
|
||||||
|
- `usr/local/bin/fruix`
|
||||||
|
|
||||||
|
That helper now:
|
||||||
|
|
||||||
|
- reports installed-system state with `fruix system status`
|
||||||
|
- stages a new current generation with `fruix system switch`
|
||||||
|
- stages the recorded rollback generation with `fruix system rollback`
|
||||||
|
- updates:
|
||||||
|
- `/var/lib/fruix/system/current`
|
||||||
|
- `/var/lib/fruix/system/current-generation`
|
||||||
|
- `/var/lib/fruix/system/rollback`
|
||||||
|
- `/var/lib/fruix/system/rollback-generation`
|
||||||
|
- `/frx/var/fruix/gcroots/current-system`
|
||||||
|
- `/frx/var/fruix/gcroots/rollback-system`
|
||||||
|
- `/frx/var/fruix/gcroots/system-N`
|
||||||
|
- refreshes the ESP bootloader file from the selected closure's `boot/loader.efi`
|
||||||
|
|
||||||
|
A practical implementation detail mattered here:
|
||||||
|
|
||||||
|
- replacing `/run/current-system` with a remove-then-recreate strategy caused the live shell environment to break while the link was absent
|
||||||
|
- switching that update to an atomic symlink replacement path for `/run/current-system` avoided that gap and made the in-guest operator command reliable
|
||||||
|
|
||||||
|
### `modules/fruix/system/freebsd/media.scm`
|
||||||
|
|
||||||
|
Updated installed rootfs staging so that installed targets expose:
|
||||||
|
|
||||||
|
- `/usr/local/bin/fruix -> /run/current-system/usr/local/bin/fruix`
|
||||||
|
|
||||||
|
Also bumped the explicit generation-layout version from:
|
||||||
|
|
||||||
|
- `1` to `2`
|
||||||
|
|
||||||
|
because the installed-system model now includes operator-driven switch/rollback state as part of the validated layout story.
|
||||||
|
|
||||||
|
### `modules/fruix/system/freebsd/model.scm`
|
||||||
|
|
||||||
|
Updated generated-file metadata so the system closure records:
|
||||||
|
|
||||||
|
- `usr/local/bin/fruix`
|
||||||
|
|
||||||
|
as part of the generated operating-system file set.
|
||||||
|
|
||||||
|
## New validation harness
|
||||||
|
|
||||||
|
Added:
|
||||||
|
|
||||||
|
- `tests/system/run-phase19-installed-system-rollback-qemu.sh`
|
||||||
|
|
||||||
|
This harness validates the actual installed-system operator flow on local `QEMU/UEFI/TCG`.
|
||||||
|
|
||||||
|
## Validation flow
|
||||||
|
|
||||||
|
The harness now performs all of the following:
|
||||||
|
|
||||||
|
1. installs a current system image directly to a target disk image
|
||||||
|
2. builds a distinct candidate closure
|
||||||
|
- in the validated harness this differs by host name so the closure identity changes cleanly without needing a heavier base-version rebuild
|
||||||
|
3. stages the candidate closure and its referenced store items into the installed target's `/frx/store`
|
||||||
|
4. boots the installed current system
|
||||||
|
5. validates initial state:
|
||||||
|
- current generation = `1`
|
||||||
|
- current closure = current installed closure
|
||||||
|
- no rollback generation yet recorded
|
||||||
|
6. runs:
|
||||||
|
- `fruix system switch /frx/store/...candidate...`
|
||||||
|
7. validates staged switch state:
|
||||||
|
- current generation = `2`
|
||||||
|
- rollback generation = `1`
|
||||||
|
- current closure = candidate closure
|
||||||
|
- rollback closure = original current closure
|
||||||
|
- generation 2 metadata/install files were written
|
||||||
|
8. reboots and validates boot into the candidate closure
|
||||||
|
9. runs:
|
||||||
|
- `fruix system rollback`
|
||||||
|
10. validates staged rollback state:
|
||||||
|
- current generation = `1`
|
||||||
|
- rollback generation = `2`
|
||||||
|
- current closure = original closure
|
||||||
|
- rollback closure = candidate closure
|
||||||
|
11. reboots and validates boot back into the original current system
|
||||||
|
12. confirms post-rollback service state:
|
||||||
|
- `fruix-shepherd` running
|
||||||
|
- `sshd` running
|
||||||
|
- activation log still shows success
|
||||||
|
|
||||||
|
## Passing validation
|
||||||
|
|
||||||
|
Passing result:
|
||||||
|
|
||||||
|
- `PASS phase19-installed-system-rollback-qemu`
|
||||||
|
|
||||||
|
Validated metadata summary:
|
||||||
|
|
||||||
|
```text
|
||||||
|
current_closure_path=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
|
||||||
|
candidate_closure_path=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
|
||||||
|
current_host_name=fruix-freebsd-current
|
||||||
|
candidate_host_name=fruix-freebsd-canary
|
||||||
|
final_current_generation=1
|
||||||
|
final_current_closure=/frx/store/4debd106d62f14594ba1612e1e7105f1658bf5f4075d6e5db5436efeaf929d90-fruix-system-fruix-freebsd-current
|
||||||
|
final_rollback_generation=2
|
||||||
|
final_rollback_closure=/frx/store/54fb14e6071b8e5704a5dc75e2881c2f0533767771c26c4181f57afea88d1e8b-fruix-system-fruix-freebsd-canary
|
||||||
|
installed_system_switch=ok
|
||||||
|
installed_system_rollback=ok
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regression checks
|
||||||
|
|
||||||
|
After landing the installed-system switch/rollback workflow, the following regression checks still pass:
|
||||||
|
|
||||||
|
- `PASS phase19-generation-layout-qemu`
|
||||||
|
- `PASS phase18-installer-iso`
|
||||||
|
|
||||||
|
That means the new in-guest generation-management path did not regress:
|
||||||
|
|
||||||
|
- the previously validated explicit generation layout
|
||||||
|
- or the UEFI installer ISO boot/install path
|
||||||
|
|
||||||
|
## Relationship to Guix
|
||||||
|
|
||||||
|
This phase does **not** claim that Fruix now matches Guix's full installed-system UX.
|
||||||
|
|
||||||
|
What Fruix now has is:
|
||||||
|
|
||||||
|
- explicit generation state on disk
|
||||||
|
- explicit current/rollback pointers
|
||||||
|
- a minimal installed-system operator command surface
|
||||||
|
- validated switching and rollback between already-staged closures
|
||||||
|
|
||||||
|
What still remains compared with Guix:
|
||||||
|
|
||||||
|
- building/staging the candidate closure from inside the target system itself
|
||||||
|
- automatic closure transfer/fetch as part of `switch`
|
||||||
|
- a richer long-term generation lifecycle policy
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Phase 19.3 is complete.
|
||||||
|
|
||||||
|
Fruix now validates an actual installed-system rollback workflow on FreeBSD:
|
||||||
|
|
||||||
|
- the target system itself can report current/rollback state
|
||||||
|
- it can switch to a staged candidate generation
|
||||||
|
- it can reboot into that candidate generation
|
||||||
|
- it can roll back to the recorded prior generation
|
||||||
|
- and it can reboot into the restored current system
|
||||||
|
|
||||||
|
That closes the Phase 19 deployment story from:
|
||||||
|
|
||||||
|
- documented deployment workflow
|
||||||
|
- to explicit generation layout
|
||||||
|
- to validated installed-system operator rollback behavior
|
||||||
@@ -11,9 +11,10 @@ This document defines the current canonical Fruix workflow for:
|
|||||||
- installing a declarative system onto an image or disk
|
- installing a declarative system onto an image or disk
|
||||||
- booting through installer media
|
- booting through installer media
|
||||||
- rolling forward to a candidate system
|
- rolling forward to a candidate system
|
||||||
- rolling back to an earlier declared system
|
- switching an installed system to a staged candidate generation
|
||||||
|
- rolling an installed system back to an earlier recorded generation
|
||||||
|
|
||||||
This is the Phase 19.1 operator-facing view of the system model already implemented in earlier phases.
|
This is now the Phase 19 operator-facing view of the system model as validated through explicit installed-system generation switching and rollback.
|
||||||
|
|
||||||
## Core model
|
## Core model
|
||||||
|
|
||||||
@@ -168,6 +169,34 @@ Use this when you want to:
|
|||||||
- install from an ISO-attached Fruix environment
|
- install from an ISO-attached Fruix environment
|
||||||
- test the same install model on more realistic VM paths
|
- test the same install model on more realistic VM paths
|
||||||
|
|
||||||
|
### Installed-system generation commands
|
||||||
|
|
||||||
|
Installed Fruix systems now also ship a small in-guest deployment helper at:
|
||||||
|
|
||||||
|
- `/usr/local/bin/fruix`
|
||||||
|
|
||||||
|
Current validated in-guest commands are:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fruix system status
|
||||||
|
fruix system switch /frx/store/...-fruix-system-...
|
||||||
|
fruix system rollback
|
||||||
|
```
|
||||||
|
|
||||||
|
Current intended usage:
|
||||||
|
|
||||||
|
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`
|
||||||
|
3. run `fruix system switch /frx/store/...` on the installed system
|
||||||
|
4. reboot into the staged candidate generation
|
||||||
|
5. if needed, run `fruix system rollback`
|
||||||
|
6. reboot back into the recorded rollback generation
|
||||||
|
|
||||||
|
Important current limitation:
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
## Deployment patterns
|
## Deployment patterns
|
||||||
|
|
||||||
### 1. Build-first workflow
|
### 1. Build-first workflow
|
||||||
@@ -261,7 +290,7 @@ Installed Fruix systems now record an explicit first-generation deployment layou
|
|||||||
|
|
||||||
- `/var/lib/fruix/system`
|
- `/var/lib/fruix/system`
|
||||||
|
|
||||||
Current validated shape:
|
Initial installed shape:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/var/lib/fruix/system/
|
/var/lib/fruix/system/
|
||||||
@@ -275,6 +304,24 @@ Current validated shape:
|
|||||||
install.scm # present on installed targets
|
install.scm # present on installed targets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
After a validated in-place switch, the layout extends to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/var/lib/fruix/system/
|
||||||
|
current -> generations/2
|
||||||
|
current-generation
|
||||||
|
rollback -> generations/1
|
||||||
|
rollback-generation
|
||||||
|
generations/
|
||||||
|
1/
|
||||||
|
...
|
||||||
|
2/
|
||||||
|
closure -> /frx/store/...-fruix-system-...
|
||||||
|
metadata.scm
|
||||||
|
provenance.scm
|
||||||
|
install.scm # deployment metadata for the switch operation
|
||||||
|
```
|
||||||
|
|
||||||
Installed systems also now create explicit GC-root-style deployment links under:
|
Installed systems also now create explicit GC-root-style deployment links under:
|
||||||
|
|
||||||
- `/frx/var/fruix/gcroots`
|
- `/frx/var/fruix/gcroots`
|
||||||
@@ -284,7 +331,9 @@ Current validated shape:
|
|||||||
```text
|
```text
|
||||||
/frx/var/fruix/gcroots/
|
/frx/var/fruix/gcroots/
|
||||||
current-system -> /frx/store/...-fruix-system-...
|
current-system -> /frx/store/...-fruix-system-...
|
||||||
|
rollback-system -> /frx/store/...-fruix-system-...
|
||||||
system-1 -> /frx/store/...-fruix-system-...
|
system-1 -> /frx/store/...-fruix-system-...
|
||||||
|
system-2 -> /frx/store/...-fruix-system-...
|
||||||
```
|
```
|
||||||
|
|
||||||
Important detail:
|
Important detail:
|
||||||
@@ -294,7 +343,9 @@ Important detail:
|
|||||||
|
|
||||||
## Roll-forward workflow
|
## Roll-forward workflow
|
||||||
|
|
||||||
The current Fruix roll-forward model is declaration-driven.
|
The current Fruix roll-forward model now has two validated layers.
|
||||||
|
|
||||||
|
### Declaration/deployment roll-forward
|
||||||
|
|
||||||
Canonical process:
|
Canonical process:
|
||||||
|
|
||||||
@@ -314,47 +365,61 @@ Canonical process:
|
|||||||
5. boot or install the candidate
|
5. boot or install the candidate
|
||||||
6. validate the candidate closure in the booted system
|
6. validate the candidate closure in the booted system
|
||||||
|
|
||||||
The important property is that the candidate closure appears beside the earlier one in `/frx/store` rather than mutating it in place.
|
### Installed-system generation roll-forward
|
||||||
|
|
||||||
|
When the candidate closure is already present on an installed target:
|
||||||
|
|
||||||
|
1. run `fruix system switch /frx/store/...candidate...`
|
||||||
|
2. confirm the staged state with `fruix system status`
|
||||||
|
3. reboot into the candidate generation
|
||||||
|
4. validate the new active closure after reboot
|
||||||
|
|
||||||
|
The important property is still that the candidate closure appears beside the earlier one in `/frx/store` rather than mutating it in place.
|
||||||
|
|
||||||
## Rollback workflow
|
## Rollback workflow
|
||||||
|
|
||||||
The current canonical rollback workflow is also declaration-driven.
|
The current canonical rollback workflow also now has two validated layers.
|
||||||
|
|
||||||
Today, rollback means:
|
### Declaration/deployment rollback
|
||||||
|
|
||||||
|
You can still roll back by redeploying the earlier declaration:
|
||||||
|
|
||||||
1. retain the earlier declaration that produced the known-good closure
|
1. retain the earlier declaration that produced the known-good closure
|
||||||
2. rebuild or rematerialize that earlier declaration
|
2. rebuild or rematerialize that earlier declaration
|
||||||
3. redeploy or reboot that earlier artifact again
|
3. redeploy or reboot that earlier artifact again
|
||||||
|
|
||||||
Concretely, the usual rollback choices are:
|
Concretely, the usual declaration-level rollback choices are:
|
||||||
|
|
||||||
- rebuild the earlier declaration with `fruix system build` and confirm the old closure path reappears
|
- rebuild the earlier declaration with `fruix system build` and confirm the old closure path reappears
|
||||||
- boot the earlier declaration again through `fruix system image`
|
- boot the earlier declaration again through `fruix system image`
|
||||||
- reinstall the earlier declaration through `fruix system install`, `installer`, or `installer-iso` if the deployment medium itself must change
|
- reinstall the earlier declaration through `fruix system install`, `installer`, or `installer-iso` if the deployment medium itself must change
|
||||||
|
|
||||||
This rollback story has already been validated at the closure/image/deployment level:
|
### Installed-system generation rollback
|
||||||
|
|
||||||
- side-by-side base-version coexistence in `/frx/store`
|
When an installed target already has both the current and rollback generations recorded:
|
||||||
- roll-forward to a candidate closure
|
|
||||||
- rollback by rebuilding and booting the earlier declaration again
|
1. run `fruix system rollback`
|
||||||
- validation on both local QEMU and the approved XCP-ng VM path
|
2. confirm the staged state with `fruix system status`
|
||||||
|
3. reboot into the rollback generation
|
||||||
|
4. validate the restored active closure after reboot
|
||||||
|
|
||||||
|
This installed-system rollback path is now validated on local `QEMU/UEFI/TCG`.
|
||||||
|
|
||||||
### Important scope note
|
### Important scope note
|
||||||
|
|
||||||
This is not yet the same thing as a first-class installed-system generation switch command.
|
This is still not yet the same thing as Guix's full `reconfigure`/generation UX.
|
||||||
|
|
||||||
Current rollback is:
|
Current installed-system rollback is intentionally modest:
|
||||||
|
|
||||||
- **redeploy the earlier declaration again**
|
- it switches between already-recorded generations on the target
|
||||||
|
- it does not yet fetch candidate closures onto the machine for you
|
||||||
What still remains for later Phase 19 work is making rollback itself operator-driven at the installed-system layer, rather than only declaration/redeploy driven.
|
- it does not yet expose a richer history-management or generation-pruning policy
|
||||||
|
|
||||||
Still pending:
|
Still pending:
|
||||||
|
|
||||||
- previous-generation tracking beyond the initial explicit generation-1 layout
|
- operator-facing closure transfer or fetch onto installed systems
|
||||||
- an explicit rollback target link distinct from `current`
|
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
|
||||||
- an operator-facing installed-system rollback workflow
|
- a fuller `reconfigure`-style installed-system UX
|
||||||
- generation switching without full redeploy
|
|
||||||
|
|
||||||
## Provenance and deployment identity
|
## Provenance and deployment identity
|
||||||
|
|
||||||
@@ -375,16 +440,16 @@ Operators should retain metadata from successful candidate and current deploymen
|
|||||||
|
|
||||||
## Current limitations
|
## Current limitations
|
||||||
|
|
||||||
The deployment workflow is now coherent, but it is not yet the final generation-management story.
|
The deployment workflow is now coherent, and Fruix now has a validated installed-system switch/rollback path, but it is still not the final generation-management story.
|
||||||
|
|
||||||
Not yet first-class:
|
Not yet first-class:
|
||||||
|
|
||||||
- a dedicated `switch` or `reconfigure` command
|
- host-side closure transfer/fetch onto installed systems as part of `fruix system switch`
|
||||||
- an installed-system rollback command that moves between generations in place
|
- a fuller `reconfigure` workflow that builds and stages the new closure from inside the target environment
|
||||||
- multi-generation retention and previous-generation tracking beyond generation 1
|
- multi-generation lifecycle policy beyond the validated `current` and `rollback` pointers
|
||||||
- generation switching policy independent of full redeploy
|
- generation pruning and retention policy independent of full redeploy
|
||||||
|
|
||||||
Those are the next logical steps after the current explicit-generation layout.
|
Those are the next logical steps after the current explicit-generation switch/rollback model.
|
||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
@@ -395,6 +460,8 @@ The current canonical Fruix deployment model is:
|
|||||||
- **materialize** the artifact appropriate to the deployment target
|
- **materialize** the artifact appropriate to the deployment target
|
||||||
- **boot or install** that artifact
|
- **boot or install** that artifact
|
||||||
- **identify deployments by closure path and provenance metadata**
|
- **identify deployments by closure path and provenance metadata**
|
||||||
- **roll back by rebuilding/redeploying the earlier declaration**, not by mutating the current closure in place
|
- on installed systems, **switch** to a staged candidate with `fruix system switch`
|
||||||
|
- on installed systems, **roll back** to the recorded rollback generation with `fruix system rollback`
|
||||||
|
- still use declaration/redeploy rollback when the target does not already have the desired closure staged locally
|
||||||
|
|
||||||
That is the operator-facing workflow Fruix should document and use while installed-system generation switching remains more limited than Guix's mature in-place system-generation workflow.
|
That is the operator-facing workflow Fruix should document and use while its installed-system generation UX remains simpler than Guix's mature in-place system-generation workflow.
|
||||||
|
|||||||
@@ -201,6 +201,8 @@
|
|||||||
(chmod (string-append closure-path "/etc/master.passwd") #o600))
|
(chmod (string-append closure-path "/etc/master.passwd") #o600))
|
||||||
(chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-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)
|
(chmod (string-append closure-path "/usr/local/etc/rc.d/fruix-shepherd") #o555)
|
||||||
|
(when (file-exists? (string-append closure-path "/usr/local/bin/fruix"))
|
||||||
|
(chmod (string-append closure-path "/usr/local/bin/fruix") #o555))
|
||||||
(when (file-exists? (string-append closure-path "/boot/fruix-pid1"))
|
(when (file-exists? (string-append closure-path "/boot/fruix-pid1"))
|
||||||
(chmod (string-append closure-path "/boot/fruix-pid1") #o555))
|
(chmod (string-append closure-path "/boot/fruix-pid1") #o555))
|
||||||
(write-file (string-append closure-path "/parameters.scm")
|
(write-file (string-append closure-path "/parameters.scm")
|
||||||
@@ -233,7 +235,7 @@
|
|||||||
(mkdir-p (dirname link-name))
|
(mkdir-p (dirname link-name))
|
||||||
(symlink target link-name))
|
(symlink target link-name))
|
||||||
|
|
||||||
(define system-generation-layout-version "1")
|
(define system-generation-layout-version "2")
|
||||||
|
|
||||||
(define* (system-generation-metadata-object os closure-path
|
(define* (system-generation-metadata-object os closure-path
|
||||||
#:key
|
#:key
|
||||||
@@ -308,8 +310,9 @@
|
|||||||
(mkdir-p rootfs)
|
(mkdir-p rootfs)
|
||||||
(for-each (lambda (dir)
|
(for-each (lambda (dir)
|
||||||
(mkdir-p (string-append rootfs dir)))
|
(mkdir-p (string-append rootfs dir)))
|
||||||
'("/run" "/boot" "/etc" "/etc/ssh" "/usr" "/usr/share" "/usr/local" "/usr/local/etc"
|
'("/run" "/boot" "/etc" "/etc/ssh" "/usr" "/usr/share" "/usr/local"
|
||||||
"/usr/local/etc/rc.d" "/var" "/var/cron" "/var/db" "/var/lib" "/var/lib/fruix"
|
"/usr/local/bin" "/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"))
|
"/var/log" "/var/run" "/tmp" "/dev" "/root" "/home"))
|
||||||
(chmod (string-append rootfs "/tmp") #o1777)
|
(chmod (string-append rootfs "/tmp") #o1777)
|
||||||
(symlink-force closure-path (string-append rootfs "/run/current-system"))
|
(symlink-force closure-path (string-append rootfs "/run/current-system"))
|
||||||
@@ -345,6 +348,8 @@
|
|||||||
(symlink-force (string-append "/run/current-system/boot/" path)
|
(symlink-force (string-append "/run/current-system/boot/" path)
|
||||||
(string-append rootfs "/boot/" path)))
|
(string-append rootfs "/boot/" path)))
|
||||||
'("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf"))
|
'("kernel" "loader" "loader.efi" "device.hints" "defaults" "lua" "loader.conf"))
|
||||||
|
(symlink-force "/run/current-system/usr/local/bin/fruix"
|
||||||
|
(string-append rootfs "/usr/local/bin/fruix"))
|
||||||
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate"
|
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-activate"
|
||||||
(string-append rootfs "/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"
|
(symlink-force "/run/current-system/usr/local/etc/rc.d/fruix-shepherd"
|
||||||
|
|||||||
@@ -295,7 +295,8 @@
|
|||||||
"metadata/host-base-provenance.scm"
|
"metadata/host-base-provenance.scm"
|
||||||
"metadata/store-layout.scm"
|
"metadata/store-layout.scm"
|
||||||
"activate"
|
"activate"
|
||||||
"shepherd/init.scm")
|
"shepherd/init.scm"
|
||||||
|
"usr/local/bin/fruix")
|
||||||
(if (pid1-init-mode? os)
|
(if (pid1-init-mode? os)
|
||||||
'("boot/fruix-pid1")
|
'("boot/fruix-pid1")
|
||||||
'())
|
'())
|
||||||
|
|||||||
@@ -467,6 +467,290 @@
|
|||||||
"}\n\n"
|
"}\n\n"
|
||||||
"load_rc_config $name\n"
|
"load_rc_config $name\n"
|
||||||
"run_rc_command \"$1\"\n")))
|
"run_rc_command \"$1\"\n")))
|
||||||
|
(define (render-installed-system-fruix os)
|
||||||
|
(string-append
|
||||||
|
"#!/bin/sh\n"
|
||||||
|
"set -eu\n"
|
||||||
|
"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin\n"
|
||||||
|
"tool_closure=$(readlink /run/current-system 2>/dev/null || true)\n"
|
||||||
|
"if [ -n \"$tool_closure\" ]; then\n"
|
||||||
|
" PATH=\"$tool_closure/profile/bin:$tool_closure/profile/sbin:$tool_closure/profile/usr/bin:$tool_closure/profile/usr/sbin:$PATH\"\n"
|
||||||
|
"fi\n"
|
||||||
|
"export PATH\n\n"
|
||||||
|
"system_root=/var/lib/fruix/system\n"
|
||||||
|
"generations_root=\"$system_root/generations\"\n"
|
||||||
|
"current_link=\"$system_root/current\"\n"
|
||||||
|
"current_generation_file=\"$system_root/current-generation\"\n"
|
||||||
|
"rollback_link=\"$system_root/rollback\"\n"
|
||||||
|
"rollback_generation_file=\"$system_root/rollback-generation\"\n"
|
||||||
|
"gcroots_root=/frx/var/fruix/gcroots\n"
|
||||||
|
"run_current_link=/run/current-system\n"
|
||||||
|
"layout_version=2\n"
|
||||||
|
"host_name='" (operating-system-host-name os) "'\n"
|
||||||
|
"ready_marker='" (operating-system-ready-marker os) "'\n"
|
||||||
|
"init_mode='" (symbol->string (operating-system-init-mode os)) "'\n\n"
|
||||||
|
"usage()\n"
|
||||||
|
"{\n"
|
||||||
|
" cat <<'EOF'\n"
|
||||||
|
"Usage: fruix system status\n"
|
||||||
|
" fruix system switch /frx/store/...-fruix-system-...\n"
|
||||||
|
" fruix system rollback\n"
|
||||||
|
"EOF\n"
|
||||||
|
"}\n\n"
|
||||||
|
"die()\n"
|
||||||
|
"{\n"
|
||||||
|
" echo \"fruix: $*\" >&2\n"
|
||||||
|
" exit 1\n"
|
||||||
|
"}\n\n"
|
||||||
|
"read_link_maybe()\n"
|
||||||
|
"{\n"
|
||||||
|
" if [ -L \"$1\" ]; then\n"
|
||||||
|
" readlink \"$1\"\n"
|
||||||
|
" fi\n"
|
||||||
|
"}\n\n"
|
||||||
|
"read_file_maybe()\n"
|
||||||
|
"{\n"
|
||||||
|
" if [ -f \"$1\" ]; then\n"
|
||||||
|
" tr -d '\\n' < \"$1\"\n"
|
||||||
|
" fi\n"
|
||||||
|
"}\n\n"
|
||||||
|
"symlink_force()\n"
|
||||||
|
"{\n"
|
||||||
|
" target=$1\n"
|
||||||
|
" link_name=$2\n"
|
||||||
|
" tmp_link=\"${link_name}.new.$$\"\n"
|
||||||
|
" mkdir -p \"$(dirname \"$link_name\")\"\n"
|
||||||
|
" if [ \"$link_name\" = \"$run_current_link\" ]; then\n"
|
||||||
|
" rm -f \"$tmp_link\"\n"
|
||||||
|
" ln -s \"$target\" \"$tmp_link\"\n"
|
||||||
|
" mv -h -f \"$tmp_link\" \"$link_name\"\n"
|
||||||
|
" else\n"
|
||||||
|
" rm -f \"$link_name\"\n"
|
||||||
|
" ln -s \"$target\" \"$link_name\"\n"
|
||||||
|
" fi\n"
|
||||||
|
"}\n\n"
|
||||||
|
"validate_closure()\n"
|
||||||
|
"{\n"
|
||||||
|
" closure=$1\n"
|
||||||
|
" [ -d \"$closure\" ] || die \"missing closure directory: $closure\"\n"
|
||||||
|
" [ -f \"$closure/activate\" ] || die \"closure is missing activate script: $closure\"\n"
|
||||||
|
" [ -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"
|
||||||
|
"max_generation_number()\n"
|
||||||
|
"{\n"
|
||||||
|
" max=0\n"
|
||||||
|
" if [ -d \"$generations_root\" ]; then\n"
|
||||||
|
" for path in \"$generations_root\"/*; do\n"
|
||||||
|
" [ -d \"$path\" ] || continue\n"
|
||||||
|
" base=$(basename \"$path\")\n"
|
||||||
|
" case \"$base\" in\n"
|
||||||
|
" ''|*[!0-9]*)\n"
|
||||||
|
" continue\n"
|
||||||
|
" ;;\n"
|
||||||
|
" esac\n"
|
||||||
|
" if [ \"$base\" -gt \"$max\" ]; then\n"
|
||||||
|
" max=$base\n"
|
||||||
|
" fi\n"
|
||||||
|
" done\n"
|
||||||
|
" fi\n"
|
||||||
|
" printf '%s\\n' \"$max\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"next_generation_number()\n"
|
||||||
|
"{\n"
|
||||||
|
" max=$(max_generation_number)\n"
|
||||||
|
" printf '%s\\n' $((max + 1))\n"
|
||||||
|
"}\n\n"
|
||||||
|
"write_generation_metadata()\n"
|
||||||
|
"{\n"
|
||||||
|
" generation=$1\n"
|
||||||
|
" closure=$2\n"
|
||||||
|
" action=$3\n"
|
||||||
|
" previous_generation=$4\n"
|
||||||
|
" previous_closure=$5\n"
|
||||||
|
" generation_dir=\"$generations_root/$generation\"\n"
|
||||||
|
" install_metadata_path=\"/var/lib/fruix/system/generations/$generation/install.scm\"\n"
|
||||||
|
" cat > \"$generation_dir/metadata.scm\" <<EOF\n"
|
||||||
|
"((system-generation-version . \"$layout_version\")\n"
|
||||||
|
" (generation-number . $generation)\n"
|
||||||
|
" (host-name . \"$host_name\")\n"
|
||||||
|
" (ready-marker . \"$ready_marker\")\n"
|
||||||
|
" (init-mode . $init_mode)\n"
|
||||||
|
" (closure-path . \"$closure\")\n"
|
||||||
|
" (parameters-file . \"$closure/parameters.scm\")\n"
|
||||||
|
" (freebsd-base-file . \"$closure/metadata/freebsd-base.scm\")\n"
|
||||||
|
" (freebsd-source-file . \"$closure/metadata/freebsd-source.scm\")\n"
|
||||||
|
" (freebsd-source-materializations-file . \"$closure/metadata/freebsd-source-materializations.scm\")\n"
|
||||||
|
" (host-base-provenance-file . \"$closure/metadata/host-base-provenance.scm\")\n"
|
||||||
|
" (store-layout-file . \"$closure/metadata/store-layout.scm\")\n"
|
||||||
|
" (install-metadata-path . \"$install_metadata_path\")\n"
|
||||||
|
" (deployment-action . \"$action\")\n"
|
||||||
|
" (previous-generation-number . \"$previous_generation\")\n"
|
||||||
|
" (previous-closure-path . \"$previous_closure\"))\n"
|
||||||
|
"EOF\n"
|
||||||
|
" chmod 644 \"$generation_dir/metadata.scm\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"write_generation_provenance()\n"
|
||||||
|
"{\n"
|
||||||
|
" generation=$1\n"
|
||||||
|
" closure=$2\n"
|
||||||
|
" generation_dir=\"$generations_root/$generation\"\n"
|
||||||
|
" cat > \"$generation_dir/provenance.scm\" <<EOF\n"
|
||||||
|
"((closure-path . \"$closure\")\n"
|
||||||
|
" (parameters-file . \"$closure/parameters.scm\")\n"
|
||||||
|
" (freebsd-base-file . \"$closure/metadata/freebsd-base.scm\")\n"
|
||||||
|
" (freebsd-source-file . \"$closure/metadata/freebsd-source.scm\")\n"
|
||||||
|
" (freebsd-source-materializations-file . \"$closure/metadata/freebsd-source-materializations.scm\")\n"
|
||||||
|
" (host-base-provenance-file . \"$closure/metadata/host-base-provenance.scm\")\n"
|
||||||
|
" (store-layout-file . \"$closure/metadata/store-layout.scm\"))\n"
|
||||||
|
"EOF\n"
|
||||||
|
" chmod 644 \"$generation_dir/provenance.scm\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"write_generation_install_metadata()\n"
|
||||||
|
"{\n"
|
||||||
|
" generation=$1\n"
|
||||||
|
" closure=$2\n"
|
||||||
|
" action=$3\n"
|
||||||
|
" previous_generation=$4\n"
|
||||||
|
" previous_closure=$5\n"
|
||||||
|
" generation_dir=\"$generations_root/$generation\"\n"
|
||||||
|
" cat > \"$generation_dir/install.scm\" <<EOF\n"
|
||||||
|
"((deployment-kind . \"$action\")\n"
|
||||||
|
" (generation-number . $generation)\n"
|
||||||
|
" (closure-path . \"$closure\")\n"
|
||||||
|
" (previous-generation-number . \"$previous_generation\")\n"
|
||||||
|
" (previous-closure-path . \"$previous_closure\")\n"
|
||||||
|
" (freebsd-base-file . \"$closure/metadata/freebsd-base.scm\")\n"
|
||||||
|
" (freebsd-source-file . \"$closure/metadata/freebsd-source.scm\")\n"
|
||||||
|
" (freebsd-source-materializations-file . \"$closure/metadata/freebsd-source-materializations.scm\")\n"
|
||||||
|
" (store-layout-file . \"$closure/metadata/store-layout.scm\"))\n"
|
||||||
|
"EOF\n"
|
||||||
|
" chmod 644 \"$generation_dir/install.scm\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"prepare_generation()\n"
|
||||||
|
"{\n"
|
||||||
|
" generation=$1\n"
|
||||||
|
" closure=$2\n"
|
||||||
|
" action=$3\n"
|
||||||
|
" previous_generation=$4\n"
|
||||||
|
" previous_closure=$5\n"
|
||||||
|
" generation_dir=\"$generations_root/$generation\"\n"
|
||||||
|
" mkdir -p \"$generation_dir\"\n"
|
||||||
|
" symlink_force \"$closure\" \"$generation_dir/closure\"\n"
|
||||||
|
" write_generation_metadata \"$generation\" \"$closure\" \"$action\" \"$previous_generation\" \"$previous_closure\"\n"
|
||||||
|
" write_generation_provenance \"$generation\" \"$closure\"\n"
|
||||||
|
" write_generation_install_metadata \"$generation\" \"$closure\" \"$action\" \"$previous_generation\" \"$previous_closure\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"update_efi_loader()\n"
|
||||||
|
"{\n"
|
||||||
|
" closure=$1\n"
|
||||||
|
" [ -e /dev/gpt/efiboot ] || return 0\n"
|
||||||
|
" esp_mount=$(mktemp -d /tmp/fruix-efiboot.XXXXXX)\n"
|
||||||
|
" if /sbin/mount -t msdosfs /dev/gpt/efiboot \"$esp_mount\" >/dev/null 2>&1; then\n"
|
||||||
|
" mkdir -p \"$esp_mount/EFI/BOOT\"\n"
|
||||||
|
" cp \"$closure/boot/loader.efi\" \"$esp_mount/EFI/BOOT/BOOTX64.EFI\"\n"
|
||||||
|
" sync\n"
|
||||||
|
" /sbin/umount \"$esp_mount\" >/dev/null 2>&1 || true\n"
|
||||||
|
" fi\n"
|
||||||
|
" rmdir \"$esp_mount\" >/dev/null 2>&1 || true\n"
|
||||||
|
"}\n\n"
|
||||||
|
"status()\n"
|
||||||
|
"{\n"
|
||||||
|
" current_generation=$(read_file_maybe \"$current_generation_file\")\n"
|
||||||
|
" current_generation_link=$(read_link_maybe \"$current_link\")\n"
|
||||||
|
" current_closure=$(read_link_maybe \"$run_current_link\")\n"
|
||||||
|
" rollback_generation=$(read_file_maybe \"$rollback_generation_file\")\n"
|
||||||
|
" rollback_generation_link=$(read_link_maybe \"$rollback_link\")\n"
|
||||||
|
" rollback_closure=\"\"\n"
|
||||||
|
" if [ -n \"$rollback_generation_link\" ] && [ -L \"$system_root/$rollback_generation_link/closure\" ]; then\n"
|
||||||
|
" rollback_closure=$(readlink \"$system_root/$rollback_generation_link/closure\")\n"
|
||||||
|
" fi\n"
|
||||||
|
" printf 'current_generation=%s\\n' \"$current_generation\"\n"
|
||||||
|
" printf 'current_link=%s\\n' \"$current_generation_link\"\n"
|
||||||
|
" printf 'current_closure=%s\\n' \"$current_closure\"\n"
|
||||||
|
" printf 'rollback_generation=%s\\n' \"$rollback_generation\"\n"
|
||||||
|
" printf 'rollback_link=%s\\n' \"$rollback_generation_link\"\n"
|
||||||
|
" printf 'rollback_closure=%s\\n' \"$rollback_closure\"\n"
|
||||||
|
"}\n\n"
|
||||||
|
"switch_to_closure()\n"
|
||||||
|
"{\n"
|
||||||
|
" target_closure=$1\n"
|
||||||
|
" validate_closure \"$target_closure\"\n"
|
||||||
|
" current_generation=$(read_file_maybe \"$current_generation_file\")\n"
|
||||||
|
" current_closure=$(read_link_maybe \"$run_current_link\")\n"
|
||||||
|
" [ -n \"$current_generation\" ] || die \"missing current generation metadata\"\n"
|
||||||
|
" [ -n \"$current_closure\" ] || die \"missing /run/current-system target\"\n"
|
||||||
|
" if [ \"$target_closure\" = \"$current_closure\" ]; then\n"
|
||||||
|
" status\n"
|
||||||
|
" return 0\n"
|
||||||
|
" fi\n"
|
||||||
|
" new_generation=$(next_generation_number)\n"
|
||||||
|
" prepare_generation \"$new_generation\" \"$target_closure\" switch \"$current_generation\" \"$current_closure\"\n"
|
||||||
|
" symlink_force \"generations/$current_generation\" \"$rollback_link\"\n"
|
||||||
|
" printf '%s\\n' \"$current_generation\" > \"$rollback_generation_file\"\n"
|
||||||
|
" symlink_force \"$current_closure\" \"$gcroots_root/rollback-system\"\n"
|
||||||
|
" symlink_force \"generations/$new_generation\" \"$current_link\"\n"
|
||||||
|
" printf '%s\\n' \"$new_generation\" > \"$current_generation_file\"\n"
|
||||||
|
" symlink_force \"$target_closure\" \"$gcroots_root/system-$new_generation\"\n"
|
||||||
|
" symlink_force \"$target_closure\" \"$gcroots_root/current-system\"\n"
|
||||||
|
" symlink_force \"$target_closure\" \"$run_current_link\"\n"
|
||||||
|
" update_efi_loader \"$target_closure\"\n"
|
||||||
|
" status\n"
|
||||||
|
"}\n\n"
|
||||||
|
"rollback_current_generation()\n"
|
||||||
|
"{\n"
|
||||||
|
" rollback_generation=$(read_file_maybe \"$rollback_generation_file\")\n"
|
||||||
|
" rollback_generation_link=$(read_link_maybe \"$rollback_link\")\n"
|
||||||
|
" [ -n \"$rollback_generation\" ] || die \"no rollback generation is recorded\"\n"
|
||||||
|
" [ -n \"$rollback_generation_link\" ] || die \"no rollback link is recorded\"\n"
|
||||||
|
" rollback_closure=$(read_link_maybe \"$system_root/$rollback_generation_link/closure\")\n"
|
||||||
|
" [ -n \"$rollback_closure\" ] || die \"rollback generation has no closure link\"\n"
|
||||||
|
" current_generation=$(read_file_maybe \"$current_generation_file\")\n"
|
||||||
|
" current_closure=$(read_link_maybe \"$run_current_link\")\n"
|
||||||
|
" [ -n \"$current_generation\" ] || die \"missing current generation metadata\"\n"
|
||||||
|
" [ -n \"$current_closure\" ] || die \"missing current closure link\"\n"
|
||||||
|
" symlink_force \"generations/$current_generation\" \"$rollback_link\"\n"
|
||||||
|
" printf '%s\\n' \"$current_generation\" > \"$rollback_generation_file\"\n"
|
||||||
|
" symlink_force \"$current_closure\" \"$gcroots_root/rollback-system\"\n"
|
||||||
|
" symlink_force \"$rollback_generation_link\" \"$current_link\"\n"
|
||||||
|
" printf '%s\\n' \"$rollback_generation\" > \"$current_generation_file\"\n"
|
||||||
|
" symlink_force \"$rollback_closure\" \"$gcroots_root/current-system\"\n"
|
||||||
|
" symlink_force \"$rollback_closure\" \"$run_current_link\"\n"
|
||||||
|
" update_efi_loader \"$rollback_closure\"\n"
|
||||||
|
" status\n"
|
||||||
|
"}\n\n"
|
||||||
|
"case \"${1:-}\" in\n"
|
||||||
|
" system)\n"
|
||||||
|
" case \"${2:-}\" in\n"
|
||||||
|
" status)\n"
|
||||||
|
" [ $# -eq 2 ] || { usage >&2; exit 1; }\n"
|
||||||
|
" status\n"
|
||||||
|
" ;;\n"
|
||||||
|
" switch)\n"
|
||||||
|
" [ $# -eq 3 ] || { usage >&2; exit 1; }\n"
|
||||||
|
" switch_to_closure \"$3\"\n"
|
||||||
|
" ;;\n"
|
||||||
|
" rollback)\n"
|
||||||
|
" [ $# -eq 2 ] || { usage >&2; exit 1; }\n"
|
||||||
|
" rollback_current_generation\n"
|
||||||
|
" ;;\n"
|
||||||
|
" --help|-h|'')\n"
|
||||||
|
" usage\n"
|
||||||
|
" ;;\n"
|
||||||
|
" *)\n"
|
||||||
|
" usage >&2\n"
|
||||||
|
" exit 1\n"
|
||||||
|
" ;;\n"
|
||||||
|
" esac\n"
|
||||||
|
" ;;\n"
|
||||||
|
" --help|-h|'')\n"
|
||||||
|
" usage\n"
|
||||||
|
" ;;\n"
|
||||||
|
" *)\n"
|
||||||
|
" usage >&2\n"
|
||||||
|
" exit 1\n"
|
||||||
|
" ;;\n"
|
||||||
|
"esac\n"))
|
||||||
|
|
||||||
|
|
||||||
(define* (operating-system-generated-files os #:key guile-store guile-extra-store shepherd-store)
|
(define* (operating-system-generated-files os #:key guile-store guile-extra-store shepherd-store)
|
||||||
@@ -486,7 +770,8 @@
|
|||||||
#:guile-store guile-store
|
#:guile-store guile-store
|
||||||
#:guile-extra-store guile-extra-store
|
#:guile-extra-store guile-extra-store
|
||||||
#:shepherd-store shepherd-store))
|
#:shepherd-store shepherd-store))
|
||||||
("shepherd/init.scm" . ,(render-shepherd-config os)))
|
("shepherd/init.scm" . ,(render-shepherd-config os))
|
||||||
|
("usr/local/bin/fruix" . ,(render-installed-system-fruix os)))
|
||||||
(if (pid1-init-mode? os)
|
(if (pid1-init-mode? os)
|
||||||
`(("boot/fruix-pid1" . ,(render-pid1-script os shepherd-store guile-store guile-extra-store)))
|
`(("boot/fruix-pid1" . ,(render-pid1-script os shepherd-store guile-store guile-extra-store)))
|
||||||
'())
|
'())
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
(use-modules (fruix system freebsd)
|
||||||
|
(fruix packages freebsd))
|
||||||
|
|
||||||
|
(define phase19-source
|
||||||
|
(freebsd-source
|
||||||
|
#:name "__SOURCE_NAME__"
|
||||||
|
#:kind 'git
|
||||||
|
#:ref "__SOURCE_REF__"
|
||||||
|
#:commit "__SOURCE_COMMIT__"))
|
||||||
|
|
||||||
|
(define phase19-base
|
||||||
|
(freebsd-base
|
||||||
|
#:name "__BASE_NAME__"
|
||||||
|
#:version-label "__BASE_VERSION_LABEL__"
|
||||||
|
#:release "__BASE_RELEASE__"
|
||||||
|
#:branch "__BASE_BRANCH__"
|
||||||
|
#:source phase19-source
|
||||||
|
#:source-root "__DECLARED_SOURCE_ROOT__"
|
||||||
|
#:target "amd64"
|
||||||
|
#:target-arch "amd64"
|
||||||
|
#:kernconf "GENERIC"))
|
||||||
|
|
||||||
|
(define phase19-operating-system
|
||||||
|
(operating-system
|
||||||
|
#:host-name "__HOST_NAME__"
|
||||||
|
#:freebsd-base phase19-base
|
||||||
|
#:kernel (freebsd-native-kernel-for phase19-base)
|
||||||
|
#:bootloader (freebsd-native-bootloader-for phase19-base)
|
||||||
|
#:base-packages (freebsd-native-system-packages-for phase19-base)
|
||||||
|
#: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 'freebsd-init+rc.d-shepherd
|
||||||
|
#:ready-marker "/var/lib/fruix/ready"
|
||||||
|
#:root-authorized-keys '("__ROOT_AUTHORIZED_KEY__")))
|
||||||
388
tests/system/run-phase19-installed-system-rollback-qemu.sh
Executable file
388
tests/system/run-phase19-installed-system-rollback-qemu.sh
Executable file
@@ -0,0 +1,388 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
project_root=${PROJECT_ROOT:-$(pwd)}
|
||||||
|
script_dir=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
|
||||||
|
fruix_cmd=$project_root/bin/fruix
|
||||||
|
os_template=${OS_TEMPLATE:-$script_dir/phase19-generation-rollback-operating-system.scm.in}
|
||||||
|
system_name=${SYSTEM_NAME:-phase19-operating-system}
|
||||||
|
store_dir=${STORE_DIR:-/frx/store}
|
||||||
|
disk_capacity=${DISK_CAPACITY:-12g}
|
||||||
|
root_size=${ROOT_SIZE:-10g}
|
||||||
|
qemu_smp=${QEMU_SMP:-2}
|
||||||
|
ssh_port=${QEMU_SSH_PORT:-10024}
|
||||||
|
base_name=${BASE_NAME:-phase19-generation-layout}
|
||||||
|
base_version_label=${BASE_VERSION_LABEL:-15.0-STABLE-generation-layout}
|
||||||
|
base_release=${BASE_RELEASE:-15.0-STABLE}
|
||||||
|
base_branch=${BASE_BRANCH:-stable/15}
|
||||||
|
source_name=${SOURCE_NAME:-stable15-generation-layout-source}
|
||||||
|
source_ref=${SOURCE_REF:-stable/15}
|
||||||
|
source_commit=${SOURCE_COMMIT:-332708a606f6bf0841c1d4a74c0d067f5640fe89}
|
||||||
|
declared_source_root=${DECLARED_SOURCE_ROOT:-/var/empty/fruix-unused-source-root-generation-layout}
|
||||||
|
current_host_name=${CURRENT_HOST_NAME:-fruix-freebsd-current}
|
||||||
|
candidate_host_name=${CANDIDATE_HOST_NAME:-fruix-freebsd-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
|
||||||
|
|
||||||
|
[ -x "$fruix_cmd" ] || {
|
||||||
|
echo "fruix command is not executable: $fruix_cmd" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[ -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
|
||||||
|
}
|
||||||
|
command -v qemu-system-x86_64 >/dev/null 2>&1 || {
|
||||||
|
echo "qemu-system-x86_64 is required" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[ -f /usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd ] || {
|
||||||
|
echo "missing QEMU UEFI firmware" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "${WORKDIR:-}" ]; then
|
||||||
|
workdir=$WORKDIR
|
||||||
|
mkdir -p "$workdir"
|
||||||
|
else
|
||||||
|
workdir=$(mktemp -d /tmp/fruix-phase19-installed-system-rollback-qemu.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
|
||||||
|
current_install_out=$workdir/current-install.txt
|
||||||
|
candidate_build_out=$workdir/candidate-build.txt
|
||||||
|
target_image=$workdir/installed.img
|
||||||
|
candidate_store_items=$workdir/candidate-store-items.txt
|
||||||
|
stage_log=$workdir/stage-candidate-store.txt
|
||||||
|
serial_log=$workdir/serial.log
|
||||||
|
qemu_pidfile=$workdir/qemu.pid
|
||||||
|
uefi_vars=$workdir/QEMU_UEFI_VARS.fd
|
||||||
|
metadata_file=$workdir/phase19-installed-system-rollback-qemu-metadata.txt
|
||||||
|
switch_status_file=$workdir/switch-status.txt
|
||||||
|
rollback_status_file=$workdir/rollback-status.txt
|
||||||
|
boot_current_status_file=$workdir/boot-current-status.txt
|
||||||
|
boot_candidate_status_file=$workdir/boot-candidate-status.txt
|
||||||
|
boot_rollback_status_file=$workdir/boot-rollback-status.txt
|
||||||
|
generation2_metadata_file=$workdir/generation2-metadata.scm
|
||||||
|
generation2_install_file=$workdir/generation2-install.scm
|
||||||
|
mnt_root=$workdir/mnt-root
|
||||||
|
md_unit=
|
||||||
|
|
||||||
|
cleanup_workdir() {
|
||||||
|
if [ -f "$qemu_pidfile" ]; then
|
||||||
|
sudo kill "$(sudo cat "$qemu_pidfile")" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [ -n "$md_unit" ]; then
|
||||||
|
sudo umount "$mnt_root" >/dev/null 2>&1 || true
|
||||||
|
sudo mdconfig -d -u "$md_unit" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if [ "$cleanup" -eq 1 ]; then
|
||||||
|
rm -rf "$workdir" 2>/dev/null || sudo rm -rf "$workdir"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup_workdir EXIT INT TERM
|
||||||
|
|
||||||
|
render_os() {
|
||||||
|
output=$1
|
||||||
|
host_name=$2
|
||||||
|
root_authorized_key=$(tr -d '\n' < "$root_authorized_key_file")
|
||||||
|
sed \
|
||||||
|
-e "s|__BASE_NAME__|$base_name|g" \
|
||||||
|
-e "s|__BASE_VERSION_LABEL__|$base_version_label|g" \
|
||||||
|
-e "s|__BASE_RELEASE__|$base_release|g" \
|
||||||
|
-e "s|__BASE_BRANCH__|$base_branch|g" \
|
||||||
|
-e "s|__SOURCE_NAME__|$source_name|g" \
|
||||||
|
-e "s|__SOURCE_REF__|$source_ref|g" \
|
||||||
|
-e "s|__SOURCE_COMMIT__|$source_commit|g" \
|
||||||
|
-e "s|__DECLARED_SOURCE_ROOT__|$declared_source_root|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"
|
||||||
|
cp /usr/local/share/edk2-qemu/QEMU_UEFI_VARS-x86_64.fd "$uefi_vars"
|
||||||
|
mkdir -p "$mnt_root"
|
||||||
|
|
||||||
|
action_env() {
|
||||||
|
sudo env \
|
||||||
|
HOME="$HOME" \
|
||||||
|
GUILE_AUTO_COMPILE=0 \
|
||||||
|
FRUIX_FREEBSD_BUILD_JOBS="${FRUIX_FREEBSD_BUILD_JOBS:-8}" \
|
||||||
|
GUIX_SOURCE_DIR="${GUIX_SOURCE_DIR:-$HOME/repos/guix}" \
|
||||||
|
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}" \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
field() {
|
||||||
|
name=$1
|
||||||
|
file=$2
|
||||||
|
sed -n "s/^$name=//p" "$file" | tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
status_field() {
|
||||||
|
name=$1
|
||||||
|
file=$2
|
||||||
|
sed -n "s/^$name=//p" "$file" | tail -n 1
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
ssh_guest() {
|
||||||
|
ssh -p "$ssh_port" -i "$root_ssh_private_key_file" \
|
||||||
|
-o BatchMode=yes \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ConnectTimeout=5 \
|
||||||
|
root@127.0.0.1 "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_status() {
|
||||||
|
output_file=$1
|
||||||
|
ssh_guest '/usr/local/bin/fruix system status' > "$output_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
action_env "$fruix_cmd" system install "$current_os_file" \
|
||||||
|
--system "$system_name" \
|
||||||
|
--store "$store_dir" \
|
||||||
|
--target "$target_image" \
|
||||||
|
--disk-capacity "$disk_capacity" \
|
||||||
|
--root-size "$root_size" > "$current_install_out"
|
||||||
|
|
||||||
|
current_closure_path=$(field closure_path "$current_install_out")
|
||||||
|
install_metadata_path=$(field install_metadata_path "$current_install_out")
|
||||||
|
materialized_source_store=$(field materialized_source_stores "$current_install_out")
|
||||||
|
[ -n "$current_closure_path" ] || { echo "missing current closure path" >&2; exit 1; }
|
||||||
|
[ "$install_metadata_path" = /var/lib/fruix/install.scm ] || { echo "unexpected install metadata path: $install_metadata_path" >&2; exit 1; }
|
||||||
|
|
||||||
|
action_env "$fruix_cmd" system build "$candidate_os_file" \
|
||||||
|
--system "$system_name" \
|
||||||
|
--store "$store_dir" > "$candidate_build_out"
|
||||||
|
|
||||||
|
candidate_closure_path=$(field closure_path "$candidate_build_out")
|
||||||
|
[ -n "$candidate_closure_path" ] || { echo "missing candidate closure path" >&2; exit 1; }
|
||||||
|
[ "$candidate_closure_path" != "$current_closure_path" ] || { echo "candidate closure unexpectedly matches current closure" >&2; exit 1; }
|
||||||
|
|
||||||
|
{
|
||||||
|
printf '%s\n' "$candidate_closure_path"
|
||||||
|
cat "$candidate_closure_path/.references"
|
||||||
|
} | awk 'NF { print }' | sort -u > "$candidate_store_items"
|
||||||
|
|
||||||
|
candidate_store_item_count=$(wc -l < "$candidate_store_items" | tr -d ' ')
|
||||||
|
|
||||||
|
md=$(sudo mdconfig -a -t vnode -f "$target_image")
|
||||||
|
md_unit=${md#md}
|
||||||
|
sudo mount -t ufs "/dev/${md}p2" "$mnt_root"
|
||||||
|
sudo mkdir -p "$mnt_root/frx/store"
|
||||||
|
: > "$stage_log"
|
||||||
|
while IFS= read -r item; do
|
||||||
|
[ -n "$item" ] || continue
|
||||||
|
item_base=$(basename "$item")
|
||||||
|
if [ -e "$mnt_root/frx/store/$item_base" ]; then
|
||||||
|
printf 'already-present=%s\n' "$item_base" >> "$stage_log"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
printf 'copy=%s\n' "$item_base" >> "$stage_log"
|
||||||
|
sudo sh -c "cd '$store_dir' && pax -rw -pe '$item_base' '$mnt_root/frx/store'"
|
||||||
|
done < "$candidate_store_items"
|
||||||
|
sudo sync
|
||||||
|
sudo umount "$mnt_root"
|
||||||
|
sudo mdconfig -d -u "$md_unit"
|
||||||
|
md_unit=
|
||||||
|
|
||||||
|
sudo qemu-system-x86_64 \
|
||||||
|
-machine q35,accel=tcg \
|
||||||
|
-cpu max \
|
||||||
|
-m 2048 \
|
||||||
|
-smp "$qemu_smp" \
|
||||||
|
-display none \
|
||||||
|
-serial "file:$serial_log" \
|
||||||
|
-monitor none \
|
||||||
|
-pidfile "$qemu_pidfile" \
|
||||||
|
-daemonize \
|
||||||
|
-drive if=pflash,format=raw,readonly=on,file=/usr/local/share/edk2-qemu/QEMU_UEFI_CODE-x86_64.fd \
|
||||||
|
-drive if=pflash,format=raw,file="$uefi_vars" \
|
||||||
|
-drive if=virtio,format=raw,file="$target_image" \
|
||||||
|
-netdev user,id=net0,hostfwd=tcp::${ssh_port}-:22 \
|
||||||
|
-device virtio-net-pci,netdev=net0
|
||||||
|
|
||||||
|
wait_for_ssh || {
|
||||||
|
echo "guest never became reachable over SSH" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_status "$boot_current_status_file"
|
||||||
|
boot_current_closure=$(ssh_guest 'readlink /run/current-system')
|
||||||
|
boot_current_hostname=$(ssh_guest 'hostname')
|
||||||
|
boot_current_generation=$(status_field current_generation "$boot_current_status_file")
|
||||||
|
boot_current_link=$(status_field current_link "$boot_current_status_file")
|
||||||
|
boot_current_rollback_generation=$(status_field rollback_generation "$boot_current_status_file")
|
||||||
|
[ "$boot_current_closure" = "$current_closure_path" ] || { echo "unexpected current closure after initial boot: $boot_current_closure" >&2; exit 1; }
|
||||||
|
[ "$boot_current_hostname" = "$current_host_name" ] || { echo "unexpected hostname after initial boot: $boot_current_hostname" >&2; exit 1; }
|
||||||
|
[ "$boot_current_generation" = 1 ] || { echo "unexpected initial generation: $boot_current_generation" >&2; exit 1; }
|
||||||
|
[ "$boot_current_link" = generations/1 ] || { echo "unexpected initial current link: $boot_current_link" >&2; exit 1; }
|
||||||
|
[ -z "$boot_current_rollback_generation" ] || { echo "rollback generation should be empty before switch" >&2; exit 1; }
|
||||||
|
ssh_guest 'test -x /usr/local/bin/fruix'
|
||||||
|
|
||||||
|
ssh_guest "/usr/local/bin/fruix system switch $candidate_closure_path" > "$switch_status_file"
|
||||||
|
switch_current_generation=$(status_field current_generation "$switch_status_file")
|
||||||
|
switch_current_link=$(status_field current_link "$switch_status_file")
|
||||||
|
switch_current_closure=$(status_field current_closure "$switch_status_file")
|
||||||
|
switch_rollback_generation=$(status_field rollback_generation "$switch_status_file")
|
||||||
|
switch_rollback_link=$(status_field rollback_link "$switch_status_file")
|
||||||
|
switch_rollback_closure=$(status_field rollback_closure "$switch_status_file")
|
||||||
|
[ "$switch_current_generation" = 2 ] || { echo "unexpected generation after switch: $switch_current_generation" >&2; exit 1; }
|
||||||
|
[ "$switch_current_link" = generations/2 ] || { echo "unexpected current link after switch: $switch_current_link" >&2; exit 1; }
|
||||||
|
[ "$switch_current_closure" = "$candidate_closure_path" ] || { echo "unexpected current closure after switch: $switch_current_closure" >&2; exit 1; }
|
||||||
|
[ "$switch_rollback_generation" = 1 ] || { echo "unexpected rollback generation after switch: $switch_rollback_generation" >&2; exit 1; }
|
||||||
|
[ "$switch_rollback_link" = generations/1 ] || { echo "unexpected rollback link after switch: $switch_rollback_link" >&2; exit 1; }
|
||||||
|
[ "$switch_rollback_closure" = "$current_closure_path" ] || { echo "unexpected rollback closure after switch: $switch_rollback_closure" >&2; exit 1; }
|
||||||
|
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/current-system')" = "$candidate_closure_path" ] || { echo "unexpected current-system gc root after switch" >&2; exit 1; }
|
||||||
|
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/rollback-system')" = "$current_closure_path" ] || { echo "unexpected rollback-system gc root after switch" >&2; exit 1; }
|
||||||
|
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/system-2')" = "$candidate_closure_path" ] || { echo "unexpected system-2 gc root after switch" >&2; exit 1; }
|
||||||
|
ssh_guest 'test -f /var/lib/fruix/system/generations/2/metadata.scm'
|
||||||
|
ssh_guest 'test -f /var/lib/fruix/system/generations/2/provenance.scm'
|
||||||
|
ssh_guest 'test -f /var/lib/fruix/system/generations/2/install.scm'
|
||||||
|
ssh_guest "cat /var/lib/fruix/system/generations/2/metadata.scm" > "$generation2_metadata_file"
|
||||||
|
ssh_guest "cat /var/lib/fruix/system/generations/2/install.scm" > "$generation2_install_file"
|
||||||
|
case "$(cat "$generation2_metadata_file")" in
|
||||||
|
*"$candidate_closure_path"*"$current_closure_path"*) : ;;
|
||||||
|
*) echo "generation 2 metadata does not record both candidate and previous closure paths" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$(cat "$generation2_install_file")" in
|
||||||
|
*"(deployment-kind . \"switch\")"*"$candidate_closure_path"*) : ;;
|
||||||
|
*) echo "generation 2 install metadata does not record switch provenance" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
reboot_guest
|
||||||
|
capture_status "$boot_candidate_status_file"
|
||||||
|
boot_candidate_closure=$(ssh_guest 'readlink /run/current-system')
|
||||||
|
boot_candidate_hostname=$(ssh_guest 'hostname')
|
||||||
|
boot_candidate_generation=$(status_field current_generation "$boot_candidate_status_file")
|
||||||
|
boot_candidate_rollback_generation=$(status_field rollback_generation "$boot_candidate_status_file")
|
||||||
|
boot_candidate_rollback_closure=$(status_field rollback_closure "$boot_candidate_status_file")
|
||||||
|
[ "$boot_candidate_closure" = "$candidate_closure_path" ] || { echo "unexpected closure after switch reboot: $boot_candidate_closure" >&2; exit 1; }
|
||||||
|
[ "$boot_candidate_hostname" = "$candidate_host_name" ] || { echo "unexpected hostname after switch reboot: $boot_candidate_hostname" >&2; exit 1; }
|
||||||
|
[ "$boot_candidate_generation" = 2 ] || { echo "unexpected generation after switch reboot: $boot_candidate_generation" >&2; exit 1; }
|
||||||
|
[ "$boot_candidate_rollback_generation" = 1 ] || { echo "unexpected rollback generation after switch reboot: $boot_candidate_rollback_generation" >&2; exit 1; }
|
||||||
|
[ "$boot_candidate_rollback_closure" = "$current_closure_path" ] || { echo "unexpected rollback closure after switch reboot: $boot_candidate_rollback_closure" >&2; exit 1; }
|
||||||
|
|
||||||
|
ssh_guest '/usr/local/bin/fruix system rollback' > "$rollback_status_file"
|
||||||
|
rollback_current_generation=$(status_field current_generation "$rollback_status_file")
|
||||||
|
rollback_current_link=$(status_field current_link "$rollback_status_file")
|
||||||
|
rollback_current_closure=$(status_field current_closure "$rollback_status_file")
|
||||||
|
rollback_rollback_generation=$(status_field rollback_generation "$rollback_status_file")
|
||||||
|
rollback_rollback_link=$(status_field rollback_link "$rollback_status_file")
|
||||||
|
rollback_rollback_closure=$(status_field rollback_closure "$rollback_status_file")
|
||||||
|
[ "$rollback_current_generation" = 1 ] || { echo "unexpected generation after rollback: $rollback_current_generation" >&2; exit 1; }
|
||||||
|
[ "$rollback_current_link" = generations/1 ] || { echo "unexpected current link after rollback: $rollback_current_link" >&2; exit 1; }
|
||||||
|
[ "$rollback_current_closure" = "$current_closure_path" ] || { echo "unexpected current closure after rollback: $rollback_current_closure" >&2; exit 1; }
|
||||||
|
[ "$rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback: $rollback_rollback_generation" >&2; exit 1; }
|
||||||
|
[ "$rollback_rollback_link" = generations/2 ] || { echo "unexpected rollback link after rollback: $rollback_rollback_link" >&2; exit 1; }
|
||||||
|
[ "$rollback_rollback_closure" = "$candidate_closure_path" ] || { echo "unexpected rollback closure after rollback: $rollback_rollback_closure" >&2; exit 1; }
|
||||||
|
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/current-system')" = "$current_closure_path" ] || { echo "unexpected current-system gc root after rollback" >&2; exit 1; }
|
||||||
|
[ "$(ssh_guest 'readlink /frx/var/fruix/gcroots/rollback-system')" = "$candidate_closure_path" ] || { echo "unexpected rollback-system gc root after rollback" >&2; exit 1; }
|
||||||
|
|
||||||
|
reboot_guest
|
||||||
|
capture_status "$boot_rollback_status_file"
|
||||||
|
boot_rollback_closure=$(ssh_guest 'readlink /run/current-system')
|
||||||
|
boot_rollback_hostname=$(ssh_guest 'hostname')
|
||||||
|
boot_rollback_generation=$(status_field current_generation "$boot_rollback_status_file")
|
||||||
|
boot_rollback_rollback_generation=$(status_field rollback_generation "$boot_rollback_status_file")
|
||||||
|
boot_rollback_rollback_closure=$(status_field rollback_closure "$boot_rollback_status_file")
|
||||||
|
shepherd_status=$(ssh_guest '/usr/local/etc/rc.d/fruix-shepherd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||||
|
sshd_status=$(ssh_guest 'service sshd onestatus >/dev/null 2>&1 && echo running || echo stopped')
|
||||||
|
activate_log=$(ssh_guest 'cat /var/log/fruix-activate.log 2>/dev/null || true' | tr '\n' ' ')
|
||||||
|
[ "$boot_rollback_closure" = "$current_closure_path" ] || { echo "unexpected closure after rollback reboot: $boot_rollback_closure" >&2; exit 1; }
|
||||||
|
[ "$boot_rollback_hostname" = "$current_host_name" ] || { echo "unexpected hostname after rollback reboot: $boot_rollback_hostname" >&2; exit 1; }
|
||||||
|
[ "$boot_rollback_generation" = 1 ] || { echo "unexpected generation after rollback reboot: $boot_rollback_generation" >&2; exit 1; }
|
||||||
|
[ "$boot_rollback_rollback_generation" = 2 ] || { echo "unexpected rollback generation after rollback reboot: $boot_rollback_rollback_generation" >&2; exit 1; }
|
||||||
|
[ "$boot_rollback_rollback_closure" = "$candidate_closure_path" ] || { echo "unexpected rollback closure after rollback reboot: $boot_rollback_rollback_closure" >&2; exit 1; }
|
||||||
|
[ "$shepherd_status" = running ] || { echo "fruix-shepherd is not running after rollback reboot" >&2; exit 1; }
|
||||||
|
[ "$sshd_status" = running ] || { echo "sshd is not running after rollback reboot" >&2; exit 1; }
|
||||||
|
case "$activate_log" in
|
||||||
|
*fruix-activate:done*) : ;;
|
||||||
|
*) echo "activation log does not show success after rollback workflow" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cat >"$metadata_file" <<EOF
|
||||||
|
workdir=$workdir
|
||||||
|
current_os_file=$current_os_file
|
||||||
|
candidate_os_file=$candidate_os_file
|
||||||
|
target_image=$target_image
|
||||||
|
current_closure_path=$current_closure_path
|
||||||
|
candidate_closure_path=$candidate_closure_path
|
||||||
|
current_host_name=$current_host_name
|
||||||
|
candidate_host_name=$candidate_host_name
|
||||||
|
candidate_store_item_count=$candidate_store_item_count
|
||||||
|
candidate_store_items=$candidate_store_items
|
||||||
|
stage_log=$stage_log
|
||||||
|
serial_log=$serial_log
|
||||||
|
install_metadata_path=$install_metadata_path
|
||||||
|
materialized_source_store=$materialized_source_store
|
||||||
|
switch_status_file=$switch_status_file
|
||||||
|
rollback_status_file=$rollback_status_file
|
||||||
|
boot_current_status_file=$boot_current_status_file
|
||||||
|
boot_candidate_status_file=$boot_candidate_status_file
|
||||||
|
boot_rollback_status_file=$boot_rollback_status_file
|
||||||
|
generation2_metadata_file=$generation2_metadata_file
|
||||||
|
generation2_install_file=$generation2_install_file
|
||||||
|
final_current_generation=$boot_rollback_generation
|
||||||
|
final_current_closure=$boot_rollback_closure
|
||||||
|
final_rollback_generation=$boot_rollback_rollback_generation
|
||||||
|
final_rollback_closure=$boot_rollback_rollback_closure
|
||||||
|
shepherd_status=$shepherd_status
|
||||||
|
sshd_status=$sshd_status
|
||||||
|
boot_backend=qemu-uefi-tcg
|
||||||
|
installed_system_switch=ok
|
||||||
|
installed_system_rollback=ok
|
||||||
|
EOF
|
||||||
|
|
||||||
|
if [ -n "$metadata_target" ]; then
|
||||||
|
mkdir -p "$(dirname "$metadata_target")"
|
||||||
|
cp "$metadata_file" "$metadata_target"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'PASS phase19-installed-system-rollback-qemu\n'
|
||||||
|
printf 'Work directory: %s\n' "$workdir"
|
||||||
|
printf 'Metadata file: %s\n' "$metadata_file"
|
||||||
|
if [ -n "$metadata_target" ]; then
|
||||||
|
printf 'Copied metadata to: %s\n' "$metadata_target"
|
||||||
|
fi
|
||||||
|
printf '%s\n' '--- metadata ---'
|
||||||
|
cat "$metadata_file"
|
||||||
Reference in New Issue
Block a user