From 03938f82449bb9f46a3d98ef6d58a8076dd5bba8 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 29 Oct 2026 23:26:36 +0100 Subject: [PATCH] mounts: learn where the mesh keeps data from disk-load's readings, not the controller's data (issue 1955, ADR 0571 clause 3) --- modules/mounts/cmd/mounts/bus.go | 15 +++---- modules/mounts/cmd/mounts/data.go | 46 +++++----------------- modules/mounts/cmd/mounts/main.go | 29 +++++++++++++- modules/mounts/cmd/mounts/manifest_test.go | 10 ++--- modules/mounts/cmd/mounts/review_test.go | 29 +++++--------- modules/mounts/go.mod | 4 +- modules/mounts/go.sum | 10 +---- modules/mounts/module.json | 30 +++++++------- 8 files changed, 76 insertions(+), 97 deletions(-) diff --git a/modules/mounts/cmd/mounts/bus.go b/modules/mounts/cmd/mounts/bus.go index ce3d0057..92505ce8 100644 --- a/modules/mounts/cmd/mounts/bus.go +++ b/modules/mounts/cmd/mounts/bus.go @@ -14,6 +14,7 @@ package main import ( "encoding/json" "fmt" + "git.novox.be/novox/mesh-sdk/go/health" "os" "path/filepath" "time" @@ -122,22 +123,22 @@ func (m *Machine) leave(p string, v any) error { // Asker calls a tool on the mesh as this module. type Asker func(key string, body any) (json.RawMessage, error) -// DataVerb is the controller's verb that says where the mesh keeps data on a machine. -const DataVerb = "seat:mesh-controller.data" +// Readings reads disk-load's readings of one machine's data items (its state `readings`, ADR 0571 clause 3). +type Readings func(node string) ([]health.Reading, error) -// LeaveData asks the controller where the mesh keeps data on this machine and leaves the paths for the +// LeaveData reads disk-load for where the mesh keeps data on this machine and leaves the paths for the // process (finding 4d). A failed ask keeps the last paths, with why; with none, the process sets up // nothing new. -func (m *Machine) LeaveData(node string, ask Asker, now time.Time) []string { - if node == "" || ask == nil { +func (m *Machine) LeaveData(node string, read Readings, now time.Time) []string { + if node == "" || read == nil { return nil } previous, _ := readJSON[DataPaths](m, DataFile) out := DataPaths{Machine: node, Asked: now.Format(time.RFC3339)} - raw, err := ask(DataVerb, map[string]any{"machine": node}) + readings, err := read(node) var paths []string if err == nil { - paths, err = DataFrom(raw) + paths, err = PathsFrom(readings) } var said []string if err != nil { diff --git a/modules/mounts/cmd/mounts/data.go b/modules/mounts/cmd/mounts/data.go index 803a1a84..02fc6da1 100644 --- a/modules/mounts/cmd/mounts/data.go +++ b/modules/mounts/cmd/mounts/data.go @@ -6,9 +6,9 @@ package main // mesh keeps data in on this machine, from the controller's `data` verb over the bus. import ( - "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-sdk/go/health" "io" "os" "path/filepath" @@ -63,44 +63,16 @@ func sortWants(w []Want) { } } -// DataFrom reads the paths out of mesh-controller.data's answer: `answer.data`, `data`, or the same as -// text in `output`. An answer with no data list is an error, never an empty list: an empty list would -// let every mount point through. -func DataFrom(raw json.RawMessage) ([]string, error) { - type item struct { - Path string `json:"path"` - } - var whole struct { - Answer *struct { - Data *[]item `json:"data"` - } `json:"answer"` - Data *[]item `json:"data"` - Output string `json:"output"` - } - if err := json.Unmarshal(raw, &whole); err != nil { - return nil, fmt.Errorf("the controller's data answer is not JSON: %v", err) - } - var items *[]item - switch { - case whole.Answer != nil && whole.Answer.Data != nil: - items = whole.Answer.Data - case whole.Data != nil: - items = whole.Data - case whole.Output != "": - var inner struct { - Data *[]item `json:"data"` - } - if err := json.Unmarshal([]byte(whole.Output), &inner); err == nil && inner.Data != nil { - items = inner.Data - } - } - if items == nil { - return nil, errors.New("the controller's data answer names no data list") +// PathsFrom is the paths disk-load's readings name: where the mesh keeps data here. No reading is an error, never an +// empty list: an empty list would let every mount point through. +func PathsFrom(readings []health.Reading) ([]string, error) { + if len(readings) == 0 { + return nil, errors.New("disk-load keeps no reading of this machine's data yet") } paths := []string{} - for _, it := range *items { - if strings.HasPrefix(it.Path, "/") { - paths = append(paths, filepath.Clean(it.Path)) + for _, r := range readings { + if strings.HasPrefix(r.Path, "/") { + paths = append(paths, filepath.Clean(r.Path)) } } return paths, nil diff --git a/modules/mounts/cmd/mounts/main.go b/modules/mounts/cmd/mounts/main.go index ea5b32c0..7a69b8d0 100644 --- a/modules/mounts/cmd/mounts/main.go +++ b/modules/mounts/cmd/mounts/main.go @@ -13,8 +13,10 @@ import ( "context" "encoding/json" "fmt" + "git.novox.be/novox/mesh-sdk/go/health" "os" "os/signal" + "strings" "syscall" "time" @@ -62,13 +64,36 @@ func shareOnTheBus(m *Machine) { _, err := stdio.State(SharesState).Put(key, value) return err } - ask := func(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) } + read := func(node string) ([]health.Reading, error) { + st := stdio.State(health.ReadingsRead) + keys, err := st.Keys() + if err != nil { + return nil, err + } + var out []health.Reading + for _, k := range keys { + if !strings.HasPrefix(k, node+".") { + continue + } + e, err := st.Get(k) + if err != nil { + return nil, err + } + if e == nil { + continue + } + if r, err := health.DecodeReading(e.Value); err == nil { + out = append(out, r) + } + } + return out, nil + } last, lastSaid := "", "" for n := 0; ; n++ { said := m.Share(node, get, put, &last, time.Now()) // Where the mesh keeps data here changes only with an assignment: asked at start and every five minutes. if n%10 == 0 { - said = append(said, m.LeaveData(node, ask, time.Now())...) + said = append(said, m.LeaveData(node, read, time.Now())...) } if s := fmt.Sprint(said); len(said) > 0 && s != lastSaid { fmt.Fprintln(os.Stderr, "mounts:", s) diff --git a/modules/mounts/cmd/mounts/manifest_test.go b/modules/mounts/cmd/mounts/manifest_test.go index 257ec6ce..43c6f67e 100644 --- a/modules/mounts/cmd/mounts/manifest_test.go +++ b/modules/mounts/cmd/mounts/manifest_test.go @@ -77,12 +77,10 @@ func TestTheManifestNamesWhatTheBundleServesAndWhatItNeeds(t *testing.T) { if strings.Join(m.Requires, ",") != "nfs-share" { t.Errorf("requires %v", m.Requires) } - if strings.Join(m.Reads, ",") != ExportsState { - t.Errorf("the module reads nfs-server's exports from the bus: reads %v", m.Reads) - } - // It calls one verb: the controller's, for where the mesh keeps data on this machine (review finding 4d). - if strings.Join(m.Invokes, ",") != DataVerb { - t.Errorf("invokes %v, want %s", m.Invokes, DataVerb) + // It reads nfs-server's exports, and disk-load's readings for where the mesh keeps data on this machine (review + // finding 4d, ADR 0571 clause 3); it calls no verb. + if strings.Join(m.Reads, ",") != "disk-load.readings,"+ExportsState || len(m.Invokes) != 0 { + t.Errorf("reads %v, invokes %v", m.Reads, m.Invokes) } if !strings.Contains(raw, `node=${machine:name}`) { t.Error("the config gives the process this machine's name, so it never mounts a share onto the machine that serves it") diff --git a/modules/mounts/cmd/mounts/review_test.go b/modules/mounts/cmd/mounts/review_test.go index 9e1c4c68..34778bf1 100644 --- a/modules/mounts/cmd/mounts/review_test.go +++ b/modules/mounts/cmd/mounts/review_test.go @@ -5,6 +5,8 @@ package main import ( "context" "encoding/json" + "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/health" "os" "path/filepath" "strings" @@ -198,18 +200,13 @@ func TestWithoutTheMeshsDataNothingNewIsSetUp(t *testing.T) { func TestTheBundleLeavesTheMeshsDataForTheProcess(t *testing.T) { f := newSystem(t) f.write(ConfigFile, libraryOnly) - answer := json.RawMessage(`{"answer":{"data":[{"machine":"workstation","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`) - paths, err := DataFrom(answer) + rs := []health.Reading{{V: 1, Node: "workstation", PlacedDataItem: catalogue.PlacedDataItem{Module: "plex", ID: "data", Path: "/mnt/plex/data"}}} + paths, err := PathsFrom(rs) if err != nil || len(paths) != 1 || paths[0] != "/mnt/plex/data" { t.Fatalf("paths %q %v", paths, err) } - // The controller's verbs also answer as text in output. - paths, err = DataFrom(json.RawMessage(`{"ok":true,"output":"{\"data\":[{\"path\":\"/srv/x\"}]}"}`)) - if err != nil || len(paths) != 1 || paths[0] != "/srv/x" { - t.Fatalf("paths from output %q %v", paths, err) - } - if _, err := DataFrom(json.RawMessage(`{"ok":false}`)); err == nil { - t.Error("an answer with no data list is an error, not an empty list") + if _, err := PathsFrom(nil); err == nil { + t.Error("no reading is an error, not an empty list") } } @@ -287,23 +284,17 @@ func callVerb(t *testing.T, m *Machine, name string, args map[string]any) (any, func TestTheBundleKeepsTheLastDataWhenAnAskFails(t *testing.T) { f := newSystem(t) m := f.machine() - var asked []string - ok := func(key string, body any) (json.RawMessage, error) { - raw, _ := json.Marshal(body) - asked = append(asked, key+" "+string(raw)) - return json.RawMessage(`{"answer":{"data":[{"path":"/mnt/plex/data"}]}}`), nil + ok := func(node string) ([]health.Reading, error) { + return []health.Reading{{V: 1, Node: node, PlacedDataItem: catalogue.PlacedDataItem{Module: "plex", ID: "data", Path: "/mnt/plex/data"}}}, nil } if said := m.LeaveData("workstation", ok, time.Now()); len(said) != 0 { t.Fatalf("said %q", said) } - if len(asked) != 1 || asked[0] != `seat:mesh-controller.data {"machine":"workstation"}` { - t.Errorf("asked %q", asked) - } - failing := func(string, any) (json.RawMessage, error) { return nil, os.ErrDeadlineExceeded } + failing := func(string) ([]health.Reading, error) { return nil, os.ErrDeadlineExceeded } said := m.LeaveData("workstation", failing, time.Now()) d, err := readJSON[DataPaths](m, DataFile) if err != nil || len(d.Paths) != 1 || d.Paths[0] != "/mnt/plex/data" || d.Error == "" || len(said) == 0 { - t.Errorf("a failed ask keeps the last paths and says why: %+v %v %q", d, err, said) + t.Errorf("a failed read keeps the last paths and says why: %+v %v %q", d, err, said) } } diff --git a/modules/mounts/go.mod b/modules/mounts/go.mod index 76732a34..3c0baeb5 100644 --- a/modules/mounts/go.mod +++ b/modules/mounts/go.mod @@ -1,5 +1,5 @@ module mounts -go 1.22 +go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277 diff --git a/modules/mounts/go.sum b/modules/mounts/go.sum index e655124d..fc894705 100644 --- a/modules/mounts/go.sum +++ b/modules/mounts/go.sum @@ -1,8 +1,2 @@ -git.novox.be/novox/mesh-sdk/go v0.1.6 h1:9qzdYONYbJdWcu6sxQcq9v1LI0JxcfkiKYkMUzJSkVQ= -git.novox.be/novox/mesh-sdk/go v0.1.6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= -git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w= -git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010190644-622b6b4f556d h1:BR8uwv4BqyLaBwITuLylrd9DRUnu0D5DjJw0RuQSXbk= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010190644-622b6b4f556d/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c h1:l5onJwoIeH8yE/PjVlEeoPyXBsHp2NQiWLllz2fwX7s= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277 h1:ajjsH4MWcx45MGYASjUSRQKhLzEringuk3TA0oxZRCU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029221656-196c5b6af277/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= diff --git a/modules/mounts/module.json b/modules/mounts/module.json index 488ebb97..dc1e3785 100644 --- a/modules/mounts/module.json +++ b/modules/mounts/module.json @@ -5,11 +5,9 @@ "nfs-share" ], "reads": [ + "disk-load.readings", "nfs-server.exports" ], - "invokes": [ - "seat:mesh-controller.data" - ], "capabilities": [ "package-manager", "service-manager" @@ -38,20 +36,20 @@ "per-machine": true } ], - "config": { - "shares": { - "type": "string", - "description": "The shares of another machine this machine mounts: space-separated name=mountpoint, each optionally :ro, or none (novox/hq ADR 0263 rule 5).", - "trusted": true, - "default": "none" - }, - "sources": { - "type": "string", - "description": "The occasional sources this machine mounts: space-separated name=smb://[@]/@ or name=disk:@, each optionally :ro, or none (novox/hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password- (novox/hq ADR 0283).", - "trusted": true, - "default": "none" - } + "config": { + "shares": { + "type": "string", + "description": "The shares of another machine this machine mounts: space-separated name=mountpoint, each optionally :ro, or none (novox/hq ADR 0263 rule 5).", + "trusted": true, + "default": "none" }, + "sources": { + "type": "string", + "description": "The occasional sources this machine mounts: space-separated name=smb://[@]/@ or name=disk:@, each optionally :ro, or none (novox/hq ADR 0263 rule 6); an SMB source's username is in its entry, its password the secret smb-password- (novox/hq ADR 0283).", + "trusted": true, + "default": "none" + } + }, "own-secrets": { "smb-password-*": { "path": "${dir:state}/smb-password-*.secret",