Research 038, ADR 0263 and to-be 52: let the mesh own shares and mounts, so no node depends on a hand-written export or fstab line
mesh/merge-gate pass: the change touches no module of the mesh's graph
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group feat/mounts-module rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
jochen
2026-10-08 17:23:52 +02:00
parent 72e0a851cf
commit 3edafd78ab
6 changed files with 644 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
---
layer: to-be
status: proposed
code: []
updated: 2026-10-08
decisions:
- 02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md
- 02-DECISIONS/0051-shared-data-is-the-operators.md
- 02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md
---
# 52 — Shares and mounts
**A node that shares a directory does it through one module, which provides the share as a provision
and opens its port to the private network only. A node that wants the files gets them through one
module, which writes a mount unit and an automount unit per share, so nothing mounts at boot and
nothing fails at boot. Only the nodes that ask get a share. A device that is not always there is a
share whose absence is a state, not a fault.**
([ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md),
from [research 038](../../01-RESEARCH/038-shares-and-mounts-the-mesh-manages/00-overview.md).)
A **share** is a directory one machine offers to others over the network. A **mount** is a share made
visible at a path on one node. An **occasional source** is a share on a device that comes and goes.
These three words are proposed for the glossary with this design.
## The parts
```
home server workstation, laptop (the nodes that ask)
nfs-server (seat nfs-share, node) mounts (seat node-mounts, node)
├ /etc/exports.d/<its file> ├ <path>.automount enabled, idle 10 min
│ one line per share, to the ├ <path>.mount not enabled, timeout 30 s
│ private network's range only, │ What = binding at:path, options
│ all_squash to the share's owner ├ credentials file (vault, ADR 0086),
├ nfs-server.service, NFS v4 only │ for an SMB source only
├ listens 2049/tcp from mesh ├ its process: probes each source,
└ its bucket: exports, clients │ keeps state in its bucket
▲ └ tools: mounts_list, mounts_exports,
│ provision nfs-share mount, unmount, test, adopt
│ (the controller wires it; ▲
│ binding: at, path, options) │ settings: shares wanted, occasional
└──────────────── request/reply on the bus ─────────┘ sources (mesh-wide and node layers)
gaming device (not a node) ── SMB share ──► an occasional source in the workstation's mounts settings
```
| part | where | does |
|---|---|---|
| seats `nfs-share` and `node-mounts` | the controller's closed set | node seats, capacity 1, with their verbs |
| `nfs-server` | the catalogue | holds `nfs-share`, provides the provision, writes its export file, runs the service, opens 2049 to the private network |
| `mounts` | the catalogue | holds `node-mounts`, writes a unit pair per share, keeps each share's state, serves the tools |
| the binding | the controller | gives a consumer the server's private-network name, the path and the options |
| the credential | the vault | an operator-delivered secret per SMB source, unsealed on the node as a file |
## 1. The server: `nfs-server`
- **Settings.** A list of shares, each with a name, the path as an `access` (the operator's data; the
module never creates, chowns or removes it), and `read-write` or `read-only`.
- **Its file.** One file in `/etc/exports.d`, written by the node-engine. One line per share:
the path, to `${machine:mesh-range}`, with `rw` or `ro`, `sync`, `secure`, `no_subtree_check`,
`all_squash`, and `anonuid` and `anongid` taken from the path's owner on the server
(`${access:<id>:uid}` and `:gid`). The pool's root is exported only if the operator asks, and then
with `crossmnt` as well.
- **The service.** `nfs-server.service`, running and enabled, restarted when the file changes.
NFS version 4 only, set in the module's drop-in for `nfs.conf`. So only 2049/tcp is needed, declared in
`listens` with `from: mesh`.
- **The provision.** `nfs-share`, one per share, with `path` and `options` in what it serves.
- **Health.** The service is active, and the kernel's export table holds every line of the module's
file.
- **Its bucket.** The exports as the kernel holds them, and the clients that have each mounted, with
since when.
- **Tools.** `exports` (r), `clients` (r), `reload` (a), and `test` (r): does this server export this
path to this node.
- **Taking over.** An act, `adopt`, clears a dataset's `sharenfs` property for a path the module now
exports, on a person's word, and says what it did.
## 2. The client: `mounts`
- **Settings.** `shares`: the names of the `nfs-share` provisions this node mounts, each with a mount
point and optional `read-only`. `sources`: occasional sources, each with a protocol (`smb` or `disk`),
a name or a disk's UUID, a share name, a mount point, and the name of its credential. Both may be set
at the mesh-wide layer, which is how "every node" is said, and changed at a node's own layer.
- **A share a module requires.** When a module on the node requires `nfs-share`, the controller adds that
share to the node's `mounts` set, with the mount point that module's assignment gives.
- **The unit pair.** For each share, a `.mount` and an `.automount`, named by the escaped mount point and
written in `/etc/systemd/system` by the node-engine:
- the `.automount` is enabled under `remote-fs.target`, with `TimeoutIdleSec=10min`;
- the `.mount` is not enabled; it has `TimeoutSec=30s` (10 s for an occasional source), `_netdev`,
and an order after the private network's link;
- for NFS: `vers=4.2`, `hard,timeo=150,retrans=3` when read-write, `soft` allowed when read-only;
- for SMB: `credentials=<the unsealed file>`, `uid` and `gid` of the node's account that uses it;
- for a disk: `BindsTo=` the disk's device unit, so the mount goes with the disk.
- **States**, kept in its bucket per share, with since when: `armed` (automount waiting), `mounted`,
`absent` (an occasional source not there), `unreachable` (should be there and is not), `failed`
(the mount was tried and refused, with the error).
- **Its process.** Probes each source's port every 30 s, and the device unit for a disk. It never
touches a mount point, so a hung server cannot hang it.
- **Health.** Every automount is armed, and every share that is not occasional is `armed` or `mounted`.
An occasional source that is `absent` is healthy.
- **Tools.** `mounts_list` (r), `mounts_exports` (r), `test` (r), `mount` and `unmount` (a), `adopt` (a).
## 3. Which nodes get which share
| node | gets | why |
|---|---|---|
| home server | none; it holds `nfs-server` | the files are local; the media modules keep their local bind mounts |
| workstation | the library and the downloads, read-write; the gaming device as an occasional source | the operator works on the library there |
| laptop | the library, read-only | it travels; the automount means it tries only when something looks |
| control-node | none | the stably reachable machine the controller runs on; it has no use for the library |
## 4. Conditions
| condition | raised when | cleared when |
|---|---|---|
| `share-unreachable` (warning) | a share that is not occasional has been `unreachable` or `failed` for 5 minutes | the next probe reaches it |
| the server's health | the export table lacks a declared line, or the service is down | the next look finds both right |
Each says the share, the node and since when: *"The home server's share `library` is not reachable
from the workstation since 19:21."* An absent occasional source raises nothing; `mounts_list` says
*"the share `retro-games` is not present since 18:02"*.
## 5. Migration
1. Build and deliver `mounts` (read-only first, already proposed) and `nfs-server`, and add the two seats.
2. Assign `nfs-server` to the home server with the library and the downloads. Check `exports` and
`test` from the workstation.
3. `adopt` on the home server clears the library's `sharenfs` property. The underlay range leaves the
export table; the private network's range is in it.
4. Assign `mounts` to the workstation. Its unit pair for the library takes precedence over the unit
generated from fstab. Check `mounts_list` and a file written from the workstation.
5. `adopt` on the workstation comments out the library's fstab line, keeping a copy of the file.
6. Accept the gaming device's credential into the vault with `secret accept`, add the gaming device as an
occasional source, and `adopt` its fstab line.
7. Assign `mounts` to the laptop, read-only.
Every `adopt` is a person's act. Nothing in this migration touches the library's files, owners or
modes, or the media modules' bind mounts.
## Not in this design
- **Samba.** Kept as found. An `smb-share` seat follows when a node needs it.
- **Clients on the underlay.** NFS for devices that are not nodes waits on issue 154.
- **Mounting when a device appears.** Waits on a local-link reach (issue 154), because the packet
filter drops mDNS. The automount already mounts on the next access.
+1
View File
@@ -51,6 +51,7 @@ document is written and this one's status becomes `implemented`.
| [`49-the-mesh-in-domains.md`](49-the-mesh-in-domains.md) | **In progress.** Every concept belongs to one of ten domains, which owns its one word; the glossary is organised by them and is the authority, a retired word is named on its replacement's line with its scope, and two checks hold this repository's documents and the catalogue's tool descriptions to it | [ADR 0244](../../02-DECISIONS/0244-the-mesh-is-described-in-domains-and-one-word-names-one-thing.md), [ADR 0006](../../02-DECISIONS/0006-the-substrate-and-the-control-plane.md), [ADR 0008](../../02-DECISIONS/0008-a-context-owns-its-store.md) |
| [`50-split-dns-on-a-machine-with-a-vpn-client.md`](50-split-dns-on-a-machine-with-a-vpn-client.md) | **In progress.** A machine whose VPN client pushes servers of its own runs a resolver of its own, on a node seat, that routes the VPN's domains over its link and every other name to the mesh's resolvers; the VPN client's module carries the adapter | [ADR 0247](../../02-DECISIONS/0247-a-machine-with-a-vpn-client-routes-names-by-domain-through-a-resolver-of-its-own.md), [ADR 0223](../../02-DECISIONS/0223-the-mesh-has-two-resolvers-and-a-machine-lists-only-them.md) |
| [`51-the-registries-keep-what-is-named.md`](51-the-registries-keep-what-is-named.md) | **In progress.** The artifact store, the package registry and each machine's container runtime get tools that say what they hold, what names each thing and what could go; every removal is a dry run unless a person asks and says why, and what decides is always a record | [ADR 0251](../../02-DECISIONS/0251-the-registries-say-what-they-hold-and-keep-what-is-named-and-a-machine-keeps-the-image-it-runs-and-the-one-before.md), [ADR 0189](../../02-DECISIONS/0189-the-store-keeps-what-the-records-name.md) |
| [`52-shares-and-mounts.md`](52-shares-and-mounts.md) | **Proposed.** A node shares a directory through `nfs-server`, which provides it as a provision to the private network only, squashing every client to the share's owner; a node that asks mounts it through `mounts`, a mount unit and an automount per share, so nothing mounts or fails at boot; a device that comes and goes is an occasional source whose absence is a state | [ADR 0263](../../02-DECISIONS/0263-a-share-is-a-provision-a-node-mounts-it-on-demand-and-only-nodes-that-ask-get-it.md), [ADR 0051](../../02-DECISIONS/0051-shared-data-is-the-operators.md), [ADR 0092](../../02-DECISIONS/0092-an-operator-delivers-a-pair-credential.md) |
## Not yet written