diff --git a/modules/mounts/README.md b/modules/mounts/README.md index 063f61bf..c6815e27 100644 --- a/modules/mounts/README.md +++ b/modules/mounts/README.md @@ -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). diff --git a/modules/mounts/cmd/mounts/apply.go b/modules/mounts/cmd/mounts/apply.go index f284fc41..c2df2630 100644 --- a/modules/mounts/cmd/mounts/apply.go +++ b/modules/mounts/cmd/mounts/apply.go @@ -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)) } } diff --git a/modules/mounts/cmd/mounts/review_test.go b/modules/mounts/cmd/mounts/review_test.go index c0893f5c..9e1c4c68 100644 --- a/modules/mounts/cmd/mounts/review_test.go +++ b/modules/mounts/cmd/mounts/review_test.go @@ -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") + } +} diff --git a/modules/mounts/cmd/mounts/units.go b/modules/mounts/cmd/mounts/units.go index 9c76bc2c..bac0357e 100644 --- a/modules/mounts/cmd/mounts/units.go +++ b/modules/mounts/cmd/mounts/units.go @@ -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 { diff --git a/modules/mounts/cmd/mounts/write_test.go b/modules/mounts/cmd/mounts/write_test.go index 4b126c2d..bf06d3d4 100644 --- a/modules/mounts/cmd/mounts/write_test.go +++ b/modules/mounts/cmd/mounts/write_test.go @@ -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) {