Files
fruix/docs/STORAGE_MODEL.md

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 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:

(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:

(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:

(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:

(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.

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