Files
fruix/docs/STORAGE_MODEL.md

622 lines
16 KiB
Markdown

# STORAGE_MODEL
## Summary
This document defines the first Fruix storage-model direction for the installer work described in `docs/PLAN_INSTALLER.md`.
Recommendation:
- use a Fruix-native name such as **`storage-layout`**
- do **not** adopt `disko` as a Fruix subsystem name
- do **not** make storage mutation part of routine `reconfigure` / `switch` / `rollback`
- make storage declarations a base Fruix concept reused by installer, non-interactive install, and image generation
In short:
- **Disko-like idea**
- **Fruix-native naming and semantics**
- **FreeBSD-native realization**
## Naming
## Chosen public term
Use:
- **storage-layout**
Related terms:
- `storage-disk`
- `storage-partition`
- `partition-file-system`
- `partition-swap`
- `realized-storage-layout`
## Terms to avoid as Fruix subsystem names
Avoid using these as the Fruix public subsystem name:
- `disko`
- `frisko`
Reasoning:
- `disko` already refers to a real Nix project with its own semantics and docs
- `frisko` is fun, but too cute for a core long-lived product surface
- `storage-layout` is clearer in code, docs, and CLI design
We can still say **“Disko-like”** in architecture notes when comparing ideas.
## Product goal
Fruix needs a first-class way to describe how storage should be laid out for installation.
That declaration should be reusable by:
- the interactive installer
- `fruix system install`
- disk image generation
- installer media generation
- later install/deploy workflows
It should also preserve the distinction between:
1. **declared storage intent**
2. **realized storage result**
3. **runtime mount/boot configuration**
Those three things are related, but they are not the same object.
## Core semantics
## 1. Declared storage intent
A `storage-layout` describes what Fruix intends to create.
Examples:
- one GPT disk
- an EFI partition
- a UFS root filesystem
- an optional swap partition
- an optional separate `/var`
This object is:
- declarative
- serializable
- safe to build/evaluate without mutating disks
- reusable across installer and non-interactive install flows
This is the Fruix equivalent of the good part of the Disko model.
## 2. Realized storage result
A realized storage result records what was actually created on a specific machine or image.
Examples:
- target device `/dev/ada0`
- partition labels actually written
- UFS labels actually created
- GPT partition UUIDs if available
- final runtime device paths used in generated config
This object is:
- machine-specific
- produced only after a successful apply/install step
- stored as install metadata / provenance
## 3. Runtime mount and boot configuration
The running Fruix system still needs concrete runtime configuration.
Examples:
- root mounted from `/dev/ufs/fruix-root`
- EFI mounted from `/dev/gpt/fruix-efi`
- swap activated from `/dev/gpt/fruix-swap`
- generated `fstab` / boot metadata / loader-related config
This object is:
- what the installed system actually uses at boot/runtime
- derived from the layout and/or realized result
- not a command to repartition the machine later
## Safety boundary
This is the most important semantic rule.
### Allowed to mutate storage
These workflows may partition/format/mount:
- installer TUI final apply step
- `fruix system install`
- image-building paths that explicitly materialize a target disk/image layout
- future explicitly named storage-apply/install commands
### Not allowed to mutate storage by default
These workflows must **not** repartition/reformat disks by default:
- `fruix system build`
- `fruix system reconfigure`
- `fruix system switch`
- `fruix system rollback`
- `fruix system deploy`
If Fruix later grows explicit storage migration commands, they should remain separate from ordinary generation switching.
## Relationship to the existing `operating-system` model
Current Fruix `operating-system` objects already carry runtime file-system information via `file-systems`.
That should remain true.
For the installer work, Fruix should add an **optional** storage declaration alongside existing runtime configuration.
Recommended direction:
- `file-systems` remains the runtime/booted-system view
- `storage-layout` becomes the install-time declarative view
- Fruix validates that the two are compatible when both are present
- Fruix may later derive more of `file-systems` automatically from `storage-layout`, but v1 does not need to force that change everywhere
This keeps the existing API stable while adding the new storage layer incrementally.
## Proposed `operating-system` integration
Add an optional field conceptually like:
- `storage-layout`
So the model becomes:
- host/users/services/etc. = running system intent
- file-systems = runtime mount intent
- storage-layout = install-time disk realization intent
That means a Fruix system declaration can eventually answer both:
- “how should this installed system boot and mount?”
- “how should its disk have been created?”
## Proposed v1 scope
V1 should intentionally be small.
### Supported layout
- single target disk
- GPT partition table
- UEFI boot
- EFI system partition
- UFS root filesystem
- optional swap partition
- optional separate `/var`
### Deferred
- ZFS root
- encryption
- multi-disk layouts
- mirrored boot
- RAID
- LVM-like layering
- post-install storage migration
- Wi-Fi-oriented installer behaviors
## Proposed model shape
This section proposes the shape of the first Fruix storage records. It is a design target, not yet a frozen implementation.
## Top-level object: `storage-layout`
Suggested responsibilities:
- identify the layout version
- record platform/backend intent
- hold one or more disks
- define boot mode
- define install-time defaults / policy
Suggested fields:
- `version`
- `platform` (initially `'freebsd`)
- `boot-mode` (initially `'uefi`)
- `disks`
- `name` or `profile-name`
- optional description/metadata
Example conceptual form:
```scheme
(storage-layout
#:name "default-efi-ufs"
#:version "1"
#:platform 'freebsd
#:boot-mode 'uefi
#:disks (list ...))
```
## Disk object: `storage-disk`
Suggested responsibilities:
- identify one install target disk
- define partition table type
- carry ordered partitions
- optionally carry selection/binding information
Suggested fields:
- `name` or `role` (example: `'system`)
- `device` or binding selector
- `partition-table` (v1: `'gpt`)
- `partitions`
Example conceptual form:
```scheme
(storage-disk
#:name 'system
#:device "/dev/ada0"
#:partition-table 'gpt
#:partitions (list ...))
```
## Partition object: `storage-partition`
Suggested responsibilities:
- define a partition's role and size
- define labels
- attach a content object
Suggested fields:
- `name`
- `role` (example: `'efi`, `'root`, `'swap`, `'var`)
- `size`
- `gpt-type` or type shortcut
- `label`
- `content`
Example conceptual form:
```scheme
(storage-partition
#:name 'root
#:role 'root
#:size "100%"
#:label "fruix-root"
#:content (partition-file-system ...))
```
## Content variants for v1
V1 needs only a small set of partition content kinds.
### `partition-file-system`
Suggested fields:
- `format` (initially `'ufs` or `'msdosfs`/`'vfat` equivalent for EFI)
- `mount-point`
- `mount-options`
- `label`
- `needed-for-boot?`
### `partition-swap`
Suggested fields:
- `label`
- optional priority/options
### `partition-empty`
For reserved/unformatted cases if needed later.
## Concrete v1 example
A concrete v1 declaration might conceptually look like this:
```scheme
(operating-system
#:host-name "fruix-node"
#:file-systems
(list (file-system #:device "/dev/ufs/fruix-root"
#:mount-point "/"
#:type "ufs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "/dev/gpt/fruix-efi"
#:mount-point "/boot/efi"
#:type "msdosfs"
#:options "rw"
#:needed-for-boot? #t)
(file-system #:device "devfs"
#:mount-point "/dev"
#:type "devfs"
#:options "rw"
#:needed-for-boot? #t))
#:storage-layout
(storage-layout
#:name "default-efi-ufs"
#:version "1"
#:platform 'freebsd
#:boot-mode 'uefi
#:disks
(list
(storage-disk
#:name 'system
#:device "/dev/ada0"
#:partition-table 'gpt
#:partitions
(list
(storage-partition
#:name 'efi
#:role 'efi
#:size "512m"
#:label "fruix-efi"
#:content (partition-file-system
#:format 'msdosfs
#:mount-point "/boot/efi"
#:mount-options '("rw")
#:needed-for-boot? #t))
(storage-partition
#:name 'swap
#:role 'swap
#:size "2g"
#:label "fruix-swap"
#:content (partition-swap #:label "fruix-swap"))
(storage-partition
#:name 'root
#:role 'root
#:size "100%"
#:label "fruix-root"
#:content (partition-file-system
#:format 'ufs
#:label "fruix-root"
#:mount-point "/"
#:mount-options '("rw")
#:needed-for-boot? #t)))))))
```
The exact constructor names may change, but the semantic split should remain.
## Device binding semantics
This is the main question where Fruix should stay simple at first.
## V1 rule
In v1, it is acceptable for the applied layout to use a concrete device path during install, such as:
- `/dev/ada0`
- `/dev/vtbd0`
- image file target translated by the install backend
That keeps the first implementation tractable.
## Later extension
Later Fruix may add more abstract binding forms, for example:
- symbolic disk roles
- serial-based selection
- by-path selection
- interactive installer selection bound at apply time
But that should be an extension to the same model, not a separate subsystem.
## FreeBSD-specific runtime naming policy
For installed runtime configuration, Fruix should prefer stable FreeBSD-native names where possible.
Recommended initial policy:
- UFS root mounts via `/dev/ufs/<label>`
- EFI partition mounts via `/dev/gpt/<label>`
- swap uses `/dev/gpt/<label>`
Why this helps:
- avoids fragile raw device-number coupling
- makes realized runtime config clearer
- aligns with native FreeBSD device naming conventions
The realized metadata should still record the raw underlying install target device and any discovered GPT UUID/provider details for provenance.
## Realized metadata shape
After a successful apply/install, Fruix should record a `realized-storage-layout` object or equivalent metadata file.
Suggested recorded data:
- layout schema version
- original declared storage layout snapshot
- install target disk path
- created partition labels
- created filesystem labels
- discovered runtime device paths
- mount points
- swap devices
- boot mode
- install timestamp
- Fruix declaration identity
- Fruix channel/origin/revision when available
Suggested install metadata paths on the installed node:
- `/var/lib/fruix/system/install/storage-layout.scm`
- `/var/lib/fruix/system/install/realized-storage-layout.scm`
- `/var/lib/fruix/system/install/install-metadata.scm`
Exact paths can still change, but the distinction between declared and realized metadata should remain explicit.
## Apply/backend semantics
The storage backend should be generated from the declaration model, not hand-coded independently in each installer path.
For v1, the backend should be able to produce:
- a **plan** view
- a **dry-run script or ordered action list**
- a **real apply** execution path
The apply path should shell out to native FreeBSD tools such as:
- `gpart`
- `newfs`
- `newfs_msdos` or equivalent EFI filesystem formatter
- `mount`
- `umount`
- `swapon`
- boot/install helpers already used by Fruix install flows
That backend should be shared by:
- installer TUI final apply step
- `fruix system install`
- image-building paths where possible
## Validation rules for the declaration model
The first validation layer should reject obviously unsafe or inconsistent layouts.
Examples:
- no disks defined
- more than one disk in a v1 single-disk layout
- unsupported partition table type
- duplicate partition names or labels
- missing root filesystem
- multiple root filesystems in a single-disk v1 layout
- EFI required for UEFI boot mode but not present
- mount-point conflicts
- unsupported filesystem types for the current backend
- missing concrete device binding when performing a real apply
## Relationship to installer UX
The installer UI should not invent its own storage logic.
Instead, it should:
1. gather user choices
2. produce or modify a `storage-layout`
3. validate it
4. show a final review/summary
5. hand it to the shared backend
That keeps the installer TUI as a frontend, not as the source of truth.
## Relationship to non-interactive install
`fruix system install` should use the same model and backend.
In practice that means:
- the CLI may consume a declaration already containing a `storage-layout`
- or accept install-time device overrides/bindings
- then validate and apply through the same backend used by the installer UI
This is the main reason to make `storage-layout` part of base Fruix rather than a UI-only concept.
## Relationship to system rebuild/reconfigure
A booted Fruix node may continue to carry the original `storage-layout` as metadata or declaration state.
But:
- `reconfigure` should rebuild/switch generations
- `switch` should change the active generation
- `rollback` should change the active generation backward
- none of those should repartition the disk simply because a `storage-layout` exists in the declaration
That rule should be baked into the semantics from the start.
## Recommended module direction
Suggested new modules:
- `modules/fruix/system/storage/model.scm`
- `modules/fruix/system/storage/validate.scm`
- `modules/fruix/system/storage/render.scm`
- `modules/fruix/system/freebsd/storage.scm`
Suggested responsibilities:
### `model.scm`
- record types
- constructors
- normalization helpers
- metadata/spec rendering
### `validate.scm`
- structural validation
- v1 policy checks
- compatibility checks with `operating-system`
### `render.scm`
- plan rendering
- dry-run text output
- metadata serialization helpers
### `freebsd/storage.scm`
- FreeBSD disk probing
- apply ordering
- execution of native tools
- realized metadata collection
## Immediate v1 implementation recommendation
Implement in this order:
1. add the base `storage-layout` record types
2. add structural validation
3. add spec/metadata serialization
4. extend `operating-system` with optional `storage-layout`
5. add a minimal FreeBSD backend for:
- GPT
- EFI
- UFS root
- optional swap
6. route `fruix system install` through that backend
7. only then build the Newt installer UI on top
That order prevents the UI from hard-coding installer-only storage behavior.
## Open questions intentionally deferred
These are real design questions, but they do not need to block v1.
- whether `file-systems` can later be partially or fully derived from `storage-layout`
- how best to model ZFS datasets in a Fruix-native way
- how to model encryption layers cleanly on FreeBSD
- whether device binding should later support serial/path matching in the declaration itself
- what the final public CLI names for standalone storage planning/apply commands should be
## Final recommendation
Fruix should adopt the **Disko-like architecture** but not the Disko name.
The right Fruix model is:
- **`storage-layout`** as a first-class declaration
- **FreeBSD-native** realization backend
- **explicit realized metadata** after installation
- **shared install engine** for both TUI and CLI install
- **no implicit repartitioning** during routine system lifecycle operations