Let mounts write each share's mount and automount, so no node depends on a hand-written fstab line (hq ADR 0263)

A machine mounts what its assignment names: a node's share from the
provision nfs-share, whose path it reads from nfs-server's state on the
bus, and occasional sources (an SMB device, a disk) whose absence is a
state, not a fault. apply writes and arms the unit pairs as root; watch
tries each source and judges the health; the operator's fstab lines are
taken over only through adopt, on their word, once the replacement is
armed. SMB logins are an outside party's secret, given with secret accept.
This commit is contained in:
jochen
2026-10-08 18:36:01 +02:00
parent 3251758340
commit 2b1b9b0235
16 changed files with 2583 additions and 18 deletions
+112 -7
View File
@@ -1,15 +1,111 @@
# mounts
What a machine mounts and what it shares over the network, read as tools (novox/hq research 038, shares
and mounts managed by the mesh).
The shares and the occasional sources a machine mounts, and what it mounts and shares, read as tools
(novox/hq ADR 0263, to-be design 52 "Shares and mounts", from research 038). It holds the node seat
`node-mounts`, one holder per machine.
A machine mounts only what its assignment names (ADR 0263 rule 5): nothing by default, and "every
machine" is the same setting given once at the mesh-wide layer. A share of another machine comes from the
provision `nfs-share`, which `nfs-server` provides on the machine that shares.
## What it owns
Nothing yet. This is the read half of the shares-and-mounts design: it was built because no tool could
read a machine's fstab, its mount units or its NFS exports, which two failing fstab mounts on the
workstation needed. The to-be design in novox/hq makes this module write the mount and automount units
for the shares a node requires; until that design is decided, it declares no resource and changes
nothing.
- The packages `nfs-utils` and `cifs-utils`.
- `/etc/mesh-mounts/mounts.conf`, placed by the mesh from the assignment: the settings `shares` and
`sources`, the binding of `nfs-share` (`from`, the sharing machine, and `at`, where it is reached on
the private network), the operator account, and where the module's secret is.
- One `.mount` and one `.automount` unit per share or source, in `/etc/systemd/system`, named as
`systemd-escape --path` names the mount point and opened by the marker
`# Written by the mesh (module mounts, novox/hq ADR 0263).` A unit file without that marker is not the
module's: it is never written over, stopped or removed, and the share whose mount point it holds is
said as not set up.
- The SMB logins, one file per source in the module's state directory, readable by root only.
- `/var/lib/mesh-mounts/from-bus` (the bundle's: what the sharing machine exports) and `/run/mesh-mounts`
(the processes': what was applied and what was found).
What it never does: chown or chmod a mount point (it makes one that is missing, and nothing else), touch a
mounted filesystem to look at it, or remove a pair because the sharing machine did not answer. A pair is
removed only when the settings stop naming its mount point. The files a share serves are the operator's
(ADR 0051): the module mounts them and owns nothing in them.
## The unit pair
Only the automount is enabled (under `remote-fs.target`; a disk's under `local-fs.target`). The mount
is never enabled: it is made at the first use of its mount point and released after ten idle minutes
(`TimeoutIdleSec=600`). So nothing mounts at boot and nothing can fail a boot, and a mount point is always
an autofs mount, so nothing writes into the empty directory under it.
| kind | What | Options | TimeoutSec |
|---|---|---|---|
| a node's share, read-write | `<at>:<path>`, `Type=nfs4` | `vers=4.2,hard,timeo=150,retrans=3,_netdev` | 30 |
| a node's share, read-only | `<at>:<path>`, `Type=nfs4` | `ro,vers=4.2,soft,timeo=150,retrans=3,_netdev` | 30 |
| an SMB source | `//<host>/<share>`, `Type=cifs` | `credentials=<file>,iocharset=utf8,_netdev,nofail,uid=…,gid=…` | 10 |
| a disk | `/dev/disk/by-uuid/<uuid>`; the mount `BindsTo=` the disk's device | `nofail` | 10 |
A share is read-only when the setting says `:ro` or the sharing machine exports it read-only. A
read-write share is mounted hard, so a write is never dropped while the server is away.
## Settings
Both are preferences whose default, `none`, mounts nothing (ADR 0262 allows no empty default).
- `shares`: space-separated `name=mountpoint`, each optionally `:ro`. The name is the share's on the
sharing machine; its path there comes from that machine over the bus, never from this setting.
For example `library=/srv/library downloads=/srv/downloads:ro`.
- `sources`: space-separated occasional sources, each optionally `:ro`:
`name=smb://<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
For example `games=smb://192.0.2.76/Share@/mnt/games`.
A mount point is an absolute, plainly written path, not a directory of the system's own (`/etc`, `/usr`,
`/run`, …). An entry that cannot be read is said, and only that entry is left out.
## The secret
`smb-credentials` is the module's own secret, issued by an outside party (the device's owner), so the mesh
never makes one in its place. The operator gives it with
`secret accept <node> mounts smb-credentials --from <file>`; the file holds one section per SMB source:
```
[games]
username=…
password=…
domain=… (optional)
```
A source whose section is missing is not mounted, and is said with that remedy. No tool answers a
password: the options, fstab lines and mountinfo the tools read are redacted.
## The processes and the bus
- `apply` (root, judged by its unit) writes and removes the unit pairs and the logins, makes a missing
mount point, reloads the service manager when anything changed and arms every automount, every 30 s.
- `watch` (root, judged by `mounts_health`) tries each source without touching its mount point — TCP 2049
for a node's share, 445 for an SMB source, the device for a disk — and says each one's state.
- The bundle (the operator account) reads what the sharing machine exports from `nfs-server`'s state
`exports`, keyed by that machine, and leaves it for `apply`; a failed read keeps the last answer. It puts
what `watch` found in the module's state `shares`, keyed by this machine. No module reads another's
files (ADR 0260).
The states: `armed` (the automount waits), `mounted`, `absent` (an occasional source that is not there —
a state, not a fault), `unreachable` (should be there and is not), `failed` (tried and refused, or not
set up; `why` says which).
## Health
Unhealthy when a share that should always be there has been unreachable or failed for five minutes ("The
share library of <machine> is not reachable from this machine since 19:21."), when an automount is not
armed, when something the settings name cannot be set up (a missing login, a unit file that is not the
module's), or when `watch` has not looked for two minutes. An occasional source that is absent is
healthy, and said as not present since when.
## Taking over a hand-written line
The operator's own fstab lines for a share are taken over only through `adopt`, on their word, and only
once the module's own pair for the same mount point is written and its automount armed: the replacement
works first, then the line goes. Without `confirm` it is a dry run. With it (and `why`), the line is
commented out as `# adopted by the mesh (module mounts) on <date>: <the line, any password redacted>`,
the file as it was is kept as `/etc/fstab.mesh-adopted-<date-time>` readable by root only, and the
service manager reloads. The module never takes a line over on its own.
## What it reads
@@ -31,7 +127,16 @@ as empty.
## Tools
What only root may do (`mount`, `unmount`, `adopt`) goes through `sudo -n`, as the packet filter's and the
service manager's tools do; a refusal by sudo is said as one.
| tool | | answers |
|---|---|---|
| `mounts_list` | r | every mount point, its fstab line, its mount and automount units and whether it is mounted now; network filesystems marked; a network mount without automount or nofail said. Narrowed with `network` and `match` |
| `mounts_exports` | r | the NFS exports declared and live, the ZFS share properties, the Samba shares, and whether nfs-server and smb are active; an export declared and not live, or live and declared nowhere, said |
| `mounts_health` | r | the health check: healthy or not, with why, one sentence per finding |
| `node-mounts.list` | r | the machine's mount points, and every share and source the module is asked to mount with its state, since when and why |
| `node-mounts.test` | r | whether one share's or source's server answers now, tried without touching its mount point |
| `node-mounts.mount` | a | mount one share or source now |
| `node-mounts.unmount` | a | release one now; its automount stays armed |
| `node-mounts.adopt` | a | take over one hand-written fstab line, as above |