Mount a share at the access the server gave this machine, and not at all when it does not export it here

This commit is contained in:
jochen
2026-10-08 20:21:17 +02:00
parent e50978ca45
commit 53cc303440
5 changed files with 57 additions and 5 deletions
+3 -1
View File
@@ -86,7 +86,9 @@ password: the options, fstab lines and mountinfo the tools read are redacted.
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. Every five minutes the bundle also asks the controller's `data` verb where the mesh keeps data on
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).
+7 -1
View File
@@ -252,7 +252,13 @@ func (a *Applier) Look(ctx context.Context) Status {
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: fmt.Sprintf("the sharing machine gave the path %q, which the module does not mount", e.Path)}
default:
pairs = append(pairs, NFSPair(w, cfg.At, e))
mine, exported := e.ForNode(cfg.Node)
if !exported {
notSet[w.Name] = ShareStatus{Name: w.Name, Kind: "nfs", Mountpoint: w.Mountpoint, State: StateFailed,
Why: "the sharing machine " + st.Server + " does not export " + w.Name + " to this machine: its grants do not name " + cfg.Node}
continue
}
pairs = append(pairs, NFSPair(w, cfg.At, mine))
}
}
+21
View File
@@ -306,3 +306,24 @@ func TestTheBundleKeepsTheLastDataWhenAnAskFails(t *testing.T) {
t.Errorf("a failed ask keeps the last paths and says why: %+v %v %q", d, err, said)
}
}
// The server decides each node's access (finding 5): a share it exports read-write may reach this node
// read-only, and a share it exports to other nodes only does not reach this one at all. The mount takes
// the access the server gave this node, never the share's own.
func TestTheServersAccessForThisNodeIsTheMountsAccess(t *testing.T) {
library := ServerShare{Name: "library", Path: "/srv/library", Access: "rw", Live: true,
To: []ShareClient{{Node: "ws", Address: "10.0.0.3", Access: "rw"}, {Node: "laptop", Address: "10.0.0.4", Access: "ro"}}}
got, ok := library.ForNode("laptop")
if !ok || got.Access != "ro" {
t.Errorf("the laptop, granted read-only, gets %q (exported to it: %v)", got.Access, ok)
}
if p := NFSPair(Wanted{Name: "library", Mountpoint: "/srv/library"}, "192.0.2.10", got); !strings.Contains(p.Mount, "Options=ro,") {
t.Errorf("a read-only grant mounted read-write:\n%s", p.Mount)
}
if got, ok := library.ForNode("ws"); !ok || got.Access != "rw" {
t.Errorf("the workstation, granted read-write, gets %q (%v)", got.Access, ok)
}
if _, ok := library.ForNode("control"); ok {
t.Errorf("a node the server does not export to is treated as exported to")
}
}
+23
View File
@@ -71,6 +71,29 @@ type ServerShare struct {
GID int `json:"gid"`
Clients string `json:"clients,omitempty"`
Live bool `json:"live"`
// To is each node the server exports the share to, at the access it decided for that node.
To []ShareClient `json:"to,omitempty"`
}
// ShareClient is one node a share is exported to, as the server decided it.
type ShareClient struct {
Node string `json:"node"`
Address string `json:"address"`
Access string `json:"access"`
}
// ForNode is the share as the server exports it to one node: its access is the lower of the share's own
// and the node's, and false when the server does not export it to that node at all.
func (e ServerShare) ForNode(node string) (ServerShare, bool) {
for _, c := range e.To {
if c.Node == node {
if c.Access == "ro" || e.Access == "ro" {
e.Access = "ro"
}
return e, true
}
}
return e, false
}
func header(name, kind string) string {
+3 -3
View File
@@ -128,7 +128,7 @@ func (f *fakeSystem) everything(except ...string) string {
const config = "shares=library=/srv/library downloads=/srv/downloads:ro\n" +
"sources=games=smb://192.0.2.76/Share@/mnt/games usb=disk:1234-ABCD@/mnt/usb\n" +
"server=homeserver\nat=192.0.2.10\naccount=operator\n" +
"server=homeserver\nat=192.0.2.10\nnode=workstation\naccount=operator\n" +
"secret=/var/lib/mesh/mounts/smb-credentials.secret\nstate=/var/lib/mesh/mounts\n"
func exportsAnswer(t *testing.T, shares ...ServerShare) string {
@@ -140,8 +140,8 @@ func exportsAnswer(t *testing.T, shares ...ServerShare) string {
}
var (
library = ServerShare{Name: "library", Path: "/tank/media", Access: "rw", UID: 1001, GID: 2000, Live: true}
downloads = ServerShare{Name: "downloads", Path: "/tank/downloads", Access: "rw", UID: 1001, GID: 2000, Live: true}
library = ServerShare{Name: "library", Path: "/tank/media", Access: "rw", UID: 1001, GID: 2000, Live: true, To: []ShareClient{{Node: "workstation", Address: "10.0.0.3", Access: "rw"}}}
downloads = ServerShare{Name: "downloads", Path: "/tank/downloads", Access: "rw", UID: 1001, GID: 2000, Live: true, To: []ShareClient{{Node: "workstation", Address: "10.0.0.3", Access: "rw"}}}
)
func TestTheSettingsAreReadAndWhatCannotBeIsSaidPlainly(t *testing.T) {