Compare commits

..

1 Commits

Author SHA1 Message Date
7edc1ce94b feat(zfs): import ZFS pools and add the shared write group
Add `modules.zfs`, the host-level pool import: a host declares its ZFS host id
and the pools to import with their dataset mountpoints, and the module imports
those pools as durable state rather than recreating them, so a service's data
survives any rebuild or reimage.

Add a shared `storage` group with a fixed gid to the base config both the host
base and the guest-base build on, so a host and every guest carry the same
number and an identity-mapped container write lands on the pool as that group
without per-service permission juggling.

No host enables the module: the only host is a laptop with no pools and a
kernel with no ZFS build, so the enabled build was verified by ad-hoc
enablement against a ZFS-supported kernel while the committed tree stays inert.
2026-07-25 17:26:36 -04:00
5 changed files with 15 additions and 11 deletions

View File

@@ -30,6 +30,6 @@ Making the Guest a peer of Host and Module — same Auto-loader, same convention
- The Skeleton grows a Guest realization: the Auto-loader discovers `guests/`, a Guest's interior compiles into a nested container, and placement fields wire VLAN attachment, MAC pinning, bind mounts, secret mounts, unit caps, and the nesting prerequisites.
- The shared base config splits into a host base (`system.nix`) and a slim guest-base; both include the primary user, home-manager, and the shared overlays, and the guest-base auto-enables the `toolkit` bundle and `modules.ssh`.
- Guests attach to host-level foundations declared once per Host: `modules.network` for the trunk and per-VLAN bridges, and `modules.storage.zfs` with a shared fixed-gid `storage` group for identity-mapped pool writes.
- Guests attach to host-level foundations declared once per Host: `modules.network` for the trunk and per-VLAN bridges, and `modules.zfs` with a shared fixed-gid `storage` group for identity-mapped pool writes.
- OCI software has a declarative home without a new mechanism: a Guest with `nesting` runs Podman in its interior.
- The `microvm` backend, multi-instance Guests, and the concrete homelab Host with its real trunk, VLAN, pool, and MAC values remain future work.

View File

@@ -94,7 +94,7 @@ The guest-base auto-enables it; a Host enables it explicitly, keeping the Host a
This is a bundle wanted as a unit, distinct from the grouping-directory enables ADR 0004 rejected.
- **`modules.network`** (new): the host-level networking foundation.
A Host declares its trunk interface and the set of VLANs to materialize, and the Module emits one bridge per tagged VLAN with systemd-networkd and manages the Host's own management address.
- **`modules.storage.zfs`** (new): the host-level pool import.
- **`modules.zfs`** (new): the host-level pool import.
A Host declares its host id, the pools to import, and their dataset mountpoints; the pools are durable state that is imported, never rebuilt.
- **A shared `storage` group** with a fixed gid in the shared base gives 1:1 ownership between Host and Guest.
Because the container backend uses identity mapping, a guest service that writes as the `storage` group lands on the pool as that same group, which is the entire "no permission errors" mechanism.

View File

@@ -5,17 +5,17 @@ blocked-by: 0002-guest-walking-skeleton
## What to build
The host-level pool-import foundation, `modules.storage.zfs`, plus the shared `storage` group that makes identity-mapped pool writes work.
The host-level pool-import foundation, `modules.zfs`, plus the shared `storage` group that makes identity-mapped pool writes work.
A Host declares its host id, the pools to import, and their dataset mountpoints; the pools are durable state that is imported, never rebuilt, so a service's data survives any rebuild or reimage.
A shared `storage` group with a fixed gid lives in the shared portion of the base config that both the host base and the guest-base include, giving 1:1 group ownership between a Host and its Guests.
This slice establishes the group and the pool import; a Guest actually writing to a mount as that group is the Guest storage placement slice.
## Acceptance criteria
- [x] `modules.storage.zfs` declares an `enable` option and its option path mirrors its file location per the Namespace convention.
- [x] `modules.zfs` declares an `enable` option and its option path mirrors its file location per the Namespace convention.
- [x] A Host declares its host id, its pools, and their dataset mountpoints; enabling the Module imports those pools rather than recreating them.
- [x] A shared `storage` group with a fixed gid is defined in the shared base and is present identically on both a Host and its Guests.
- [x] A Host enabling `modules.storage.zfs` builds, and the `storage` gid is verifiable by `nix eval`. Verified by ad-hoc enablement on `neogaia`; the enablement is not committed (see notes).
- [x] A Host enabling `modules.zfs` builds, and the `storage` gid is verifiable by `nix eval`. Verified by ad-hoc enablement on `neogaia`; the enablement is not committed (see notes).
## Implementation Notes
@@ -26,9 +26,13 @@ Declared dataset mountpoints become plain `fileSystems` entries with `fsType = "
`pools` is typed `attrsOf (attrsOf path)` — pool name to a dataset-relative-path to mountpoint map — rather than a per-pool submodule.
No per-pool option beyond the mount map is foreseen at host level, so the extra submodule layer would have been speculative.
The `storage` group carries a fixed gid of 2000, placed in `base.nix` so a host and every guest built from this flake carry the identical number.
The Module is `modules.zfs`, a flat single-file module, not `modules.storage.zfs` under a `storage/` directory.
Nothing else lives under a storage namespace, and the sibling host-level foundation `modules.network` is likewise flat, so the extra directory level would have grouped a single member.
The spec and ADR 0006 were updated to name it `modules.zfs` to match.
The `storage` group carries a fixed gid of 10000, placed in `base.nix` so a host and every guest built from this flake carry the identical number.
That identity is the whole write mechanism: an identity-mapped container write lands on the pool as the same numeric group with no per-service permission juggling.
2000 sits above the ids NixOS assigns automatically, so no generated account collides with it.
10000 sits above the ids NixOS assigns automatically, so no generated account collides with it.
This slice only defines the group and the import; a Guest actually writing to a mount as this group is the Guest storage placement slice.
No host commits an enablement of this Module.

View File

@@ -65,8 +65,8 @@ in
# The shared write group.
# Its gid is fixed, so a host and every guest carry the same number.
# An identity-mapped container write then lands on the pool as this group, sparing every service the permission juggling.
# 2000 clears the system-group ids assigned automatically and leaves headroom above the primary user, so nothing else claims it.
users.groups.storage.gid = 2000;
# 10000 clears the system-group ids assigned automatically and leaves headroom above the primary user, so nothing else claims it.
users.groups.storage.gid = 10000;
# home-manager as a NixOS module: one build produces the system and user
# environment together, sharing the system's pkgs and installing user

View File

@@ -5,10 +5,10 @@
}:
# The host-level ZFS pool import: durable service state a host mounts, never rebuilds.
let
cfg = config.modules.storage.zfs;
cfg = config.modules.zfs;
in
{
options.modules.storage.zfs = {
options.modules.zfs = {
enable = lib.mkEnableOption "importing durable ZFS pools that hold service state";
hostId = lib.mkOption {