16 KiB
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
diskoas 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-diskstorage-partitionpartition-file-systempartition-swaprealized-storage-layout
Terms to avoid as Fruix subsystem names
Avoid using these as the Fruix public subsystem name:
diskofrisko
Reasoning:
diskoalready refers to a real Nix project with its own semantics and docsfriskois fun, but too cute for a core long-lived product surfacestorage-layoutis 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:
- declared storage intent
- realized storage result
- 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 buildfruix system reconfigurefruix system switchfruix system rollbackfruix 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-systemsremains the runtime/booted-system viewstorage-layoutbecomes the install-time declarative view- Fruix validates that the two are compatible when both are present
- Fruix may later derive more of
file-systemsautomatically fromstorage-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:
versionplatform(initially'freebsd)boot-mode(initially'uefi)disksnameorprofile-name- optional description/metadata
Example conceptual form:
(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:
nameorrole(example:'system)deviceor binding selectorpartition-table(v1:'gpt)partitions
Example conceptual form:
(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:
namerole(example:'efi,'root,'swap,'var)sizegpt-typeor type shortcutlabelcontent
Example conceptual form:
(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'ufsor'msdosfs/'vfatequivalent for EFI)mount-pointmount-optionslabelneeded-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:
(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:
gpartnewfsnewfs_msdosor equivalent EFI filesystem formattermountumountswapon- 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:
- gather user choices
- produce or modify a
storage-layout - validate it
- show a final review/summary
- 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:
reconfigureshould rebuild/switch generationsswitchshould change the active generationrollbackshould change the active generation backward- none of those should repartition the disk simply because a
storage-layoutexists 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.scmmodules/fruix/system/storage/validate.scmmodules/fruix/system/storage/render.scmmodules/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:
- add the base
storage-layoutrecord types - add structural validation
- add spec/metadata serialization
- extend
operating-systemwith optionalstorage-layout - add a minimal FreeBSD backend for:
- GPT
- EFI
- UFS root
- optional swap
- route
fruix system installthrough that backend - 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-systemscan later be partially or fully derived fromstorage-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-layoutas 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