Files
mesh-catalog/modules/mounts/README.md
T
jochen a12b5908aa
mesh/merge-gate pass: builds mounts → shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered
mounts: name a malformed entry of sources by its position, never by its name
A password holding '=' and a space or comma splits the entry, and the tail
read as name=... was said as "the source <tail>", carrying part of the
secret into the health answer and the operator's conditions (review of
mesh-catalog#190, issue 386).
2026-10-10 21:38:17 +02:00

213 lines
14 KiB
Markdown

# mounts
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
- 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 secrets are placed.
- 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 credentials file per source in the module's state directory, readable by root only,
written from the source's username and password.
- `/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 mount of the module's own that failed because its source was not there — an occasional source that is
off, or a node's share whose server is away, tried when someone opened the mount point — is cleared
(`systemctl reset-failed`) by `apply` within five seconds, and so is the failure a unit of the same name
carried before the module wrote it (the unit fstab's generator makes until the line is adopted). The module
says such a share as `absent` or `unreachable`; left failed, the unit would also be said as a failed unit of
the machine's own, with a notification each time it is touched. A mount that failed while its source answers
is left failed: that is a fault, and the share is said `failed`. An automount that gave up after repeated
failed mounts (`mount-start-limit-hit`) while its source was away is cleared and armed again the same way.
Only a unit the module takes over has its earlier failure cleared regardless of the source; a pair of its
own that it rewrites keeps its failure.
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`, names 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://[<user>@]<host>/<share>@<mountpoint>` or `name=disk:<filesystem uuid>@<mountpoint>`.
For example `games=smb://root@192.0.2.76/Share@/mnt/games`; the host is a name or an IPv4 address. An SMB
source's username is in its entry: it is not a secret (novox/hq ADR 0283). It is percent-decoded, so a device that needs a domain takes it as
`WORKGROUP%5Cuser` (for `WORKGROUP\user`), and an `@` in it is written `%40`; it holds no space, comma or
`=`. A password never goes in the entry: `user:password@` is refused, saying so and never what was
written. An entry that cannot be read whole is said as malformed by its position alone ("entry 2 of sources
is malformed: …"), never with its name or anything else it holds.
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.
Both are trusted settings: an agent proposes them (`settings propose mounts … --node <node>`) and the
operator approves the change on their phone; it is set on that approval and pushed after (ADR 0277).
## The secrets
Each SMB source's password is its own secret, `smb-password-<source>`: a member of the module's secret
family `smb-password-*`, issued by an outside party (the device's owner). The mesh never makes a value for
a member (ADR 0283), so one not given is no file, and the module reads that as not given.
The operator gives it at the desk: an agent asks for it with the controller's `secret-ask` verb (or the
operator runs `secret ask <node> mounts smb-password-<source>`), a prompt opens on that machine's desk that
shows nothing of what is typed and names the module, the secret and who asked, and the password is typed
as one line (ADR 0277). It may also be given from a file at the controller's terminal with
`secret accept <node> mounts smb-password-<source> --from <file>`.
The file is read as one line: one trailing line break is dropped and everything else is kept as typed, so
a password may hold spaces and `=`. An empty file is not given. A file of more than one line is refused,
saying so and never what it holds; give it again. No tool answers a password: the options, fstab lines and
mountinfo the tools read are redacted.
A source whose username or password is not given is not mounted. It is said `waiting`, naming the setting
or the secret it waits for, and the module's health answers *waits* (below).
## The processes and the bus
- `apply` (root, judged by its unit) writes and removes the unit pairs and the credentials files, 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, with `wants`: each share this
machine asks of which server and whether read-write or read-only (`[{"server", "share", "access"}]`).
The server exports a share only to the machines that want it and that it grants, and decides the access
itself: `apply` mounts a share at the access the server's answer gives this machine (`to[]`), read-only
when either the share or this machine's grant is, and not at all when the server does not export it
here (said as failed, naming the grant that is missing). Every five minutes the bundle also asks the controller's `data` verb where the mesh keeps data on
this machine, and leaves the paths for `apply`; a failed ask keeps the last paths. No module reads
another's files (ADR 0260).
## What it refuses
Each refusal is said as the share's state `failed`, with why, and nothing is written for it:
- a mount point at, under or above a path where the mesh keeps data on this machine (the controller's
`data` verb: Plex's library and configuration, a backup repository, an agent's home). Until that list
has been read once, nothing new is set up; a pair the module already has stays;
- a mount point the system owns (`/`, `/etc`, `/usr`, `/boot`, `/proc`, `/sys`, `/dev`, `/run` and what
is under them, and `/var`, `/home`, `/root`, `/tmp` themselves);
- a mount point that something else is mounted at, or an automount that is not the module's;
- a mount point whose unit someone made at run time (`/run/systemd/transient`, from `systemd-mount` or the
like): that is somebody's mount now;
- a folder that holds files: a mount would hide them. Only a folder not in the kernel's mount table is
opened, so a server that is away cannot hold the look;
- a share that this machine serves itself.
And what it never does:
- remove a pair while an entry of `shares` or `sources` cannot be read: nobody can tell which mount point
the mistyped entry meant, so every pair stays until the settings read again;
- stop or remove a pair whose mount point has a filesystem mounted that is not the pair's own source; the
verb `unmount` refuses the same.
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), `waiting` (an SMB source whose username or password the operator has not
given; `waits` names the setting or the secret).
## Health
The answer is mesh-sdk's (`go/health`): healthy, unhealthy, or waits (novox/hq ADR 0283).
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 unit file that is not the module's, a
password of more than one line), or when `watch` has not looked for two minutes. An occasional source that
is absent is healthy, and said as not present since when.
A source waiting for its username or password is not a fault. With nothing else found, the answer is
*waits*, with one wait per thing not given: the setting `sources` ("the username of the source games, in
its entry of sources") or the secret `smb-password-games` ("the password of the source games"). The node-engine states the
resource `waiting`, the controller says it to the operator as needing them, naming the act, and the
first-node gate does not fail a build for it. Beside a fault, the waits are said and the answer is
unhealthy.
## 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
- `/etc/fstab`, every line but swap. A credential in the options (`password=`, `pass=`, `passwd=`) is
answered as `(redacted)`; a `credentials=` file is named and never opened.
- The service manager's mount and automount units, with `systemctl list-units` and `systemctl show`:
state, result, timeout, and whether each was generated from fstab or comes from a unit file. The
kernel's own filesystems (proc, tmpfs, mqueue and the like) are left out.
- `/proc/self/mountinfo`, for what is mounted now. A mount point is never opened or statted, so a
network filesystem whose server is away cannot hang a call.
- `/etc/exports`, `/etc/exports.d/*.exports` and the kernel's live export table `/var/lib/nfs/etab`.
- The ZFS `sharenfs` and `sharesmb` properties, where `zfs` is installed.
- The share sections of `/etc/samba/smb.conf`: path, valid users, read only or writable, force user and
the like. The `[global]` section and every other setting are left out, so no password or password
backend is answered.
A file that cannot be read, and a command that does not answer, is said under `notes`, never answered
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 |
## Root acts and sudo
The tools run as the operator account and act as root through `sudo -n`. The `sudo` module grants that
account every command, so **this module's code is the only line** between a tool call and root: each act
checks again what it may touch before it runs a command, and a test holds each check. Narrowing what sudo
grants the account to the commands these modules need is a separate decision, not made here.