diff --git a/modules/nfs-server/cmd/nfs-server/backup.go b/modules/nfs-server/cmd/nfs-server/backup.go index 8bf3d6fd..e8d3dfde 100644 --- a/modules/nfs-server/cmd/nfs-server/backup.go +++ b/modules/nfs-server/cmd/nfs-server/backup.go @@ -20,6 +20,7 @@ package main import ( "encoding/json" "fmt" + "git.novox.be/novox/mesh-sdk/go/delivery" "os" "path" "sort" @@ -46,33 +47,19 @@ type BackupFromBus struct { Error string `json:"error,omitempty"` } -// BackupFrom reads the holder of node-backup on one machine from mesh-controller.seats' answer, and the -// paths that module keeps data in on that machine from mesh-controller.data's. A machine whose backup seat -// nobody holds, and an answer with no data list, are errors: never an empty list read as "nothing to fear". -func BackupFrom(machine string, seats, data json.RawMessage) (string, []string, error) { - var s struct { - Answer *struct { - Seats []struct { - Seat string `json:"seat"` - Holders []struct { - Module string `json:"module"` - Node string `json:"node"` - } `json:"holders"` - } `json:"seats"` - } `json:"answer"` - } - if err := json.Unmarshal(seats, &s); err != nil || s.Answer == nil { - return "", nil, fmt.Errorf("the controller's seats answer could not be read: %v", err) +// BackupFrom reads the holder of node-backup on one machine from the placement seat's state of who holds which seat +// (novox/hq ADR 0571 clause 4), and the paths that module keeps data in on that machine from mesh-controller.data's +// answer. A machine whose backup seat nobody holds, and an answer with no data list, are errors: never an empty list +// read as "nothing to fear". +func BackupFrom(machine string, keys map[string][]byte, data json.RawMessage) (string, []string, error) { + holders, err := delivery.HoldersOf(keys, BackupSeat, machine) + if err != nil { + return "", nil, err } holder := "" - for _, seat := range s.Answer.Seats { - if seat.Seat != BackupSeat { - continue - } - for _, h := range seat.Holders { - if h.Node == machine { - holder = h.Module - } + for _, h := range holders { + if h.Node == machine { + holder = h.Module } } if holder == "" { @@ -102,12 +89,13 @@ func BackupFrom(machine string, seats, data json.RawMessage) (string, []string, // TakeBackup asks the controller over the bus, as the module, through the runtime. func (m *Machine) TakeBackup(machine string) { - m.TakeBackupWith(machine, askBus) + m.TakeBackupWith(machine, askBus, seatHolderKeys) } -// TakeBackupWith is TakeBackup with the ask given, for tests. A failed ask keeps the last holder and +// TakeBackupWith is TakeBackup with the ask and the reading of the seat-holders state given, for tests. A failed ask keeps the last holder and // paths and adds why. -func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) (json.RawMessage, error)) { +func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) (json.RawMessage, error), + holderKeys func() (map[string][]byte, error)) { var last BackupFromBus if text, err := m.Read(BackupFile); err == nil { _ = json.Unmarshal([]byte(text), &last) @@ -115,7 +103,7 @@ func (m *Machine) TakeBackupWith(machine string, ask func(key string, body any) next := last next.Looked = m.Now().UTC().Format(time.RFC3339) next.Error = "" - seats, err := ask("seat:mesh-controller.seats", map[string]any{}) + seats, err := holderKeys() var data json.RawMessage if err == nil { data, err = ask("seat:mesh-controller.data", map[string]any{"machine": machine}) diff --git a/modules/nfs-server/cmd/nfs-server/backup_test.go b/modules/nfs-server/cmd/nfs-server/backup_test.go index 7ae3298b..4efe26a5 100644 --- a/modules/nfs-server/cmd/nfs-server/backup_test.go +++ b/modules/nfs-server/cmd/nfs-server/backup_test.go @@ -81,7 +81,7 @@ func TestWithoutTheBackupListTheExportsStayAndItIsSaid(t *testing.T) { } func TestBackupFromTheSeatsAndTheData(t *testing.T) { - seats := json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","scope":"node","holders":[{"module":"restic","node":"ace"},{"module":"borg","node":"shanks"}]}]},"ok":true}`) + seats := backupHolders("ace", "restic") data := json.RawMessage(`{"answer":{"data":[{"machine":"ace","module":"restic","item":"repository","path":"/storage/restic"},{"machine":"ace","module":"restic","item":"state","path":"/var/lib/restic"},{"machine":"ace","module":"plex","item":"data","path":"/mnt/plex/data"}]},"ok":true}`) holder, paths, err := BackupFrom("ace", seats, data) if err != nil || holder != "restic" || strings.Join(paths, " ") != "/storage/restic /var/lib/restic" { @@ -101,11 +101,11 @@ func TestAFailedAskKeepsTheLastBackupPaths(t *testing.T) { m := f.machine() m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) { return nil, errors.New("no responders") - }) + }, noHolders) f.backup("restic", "/storage/restic") m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) { return nil, errors.New("timeout") - }) + }, func() (map[string][]byte, error) { return backupHolders("ace", "restic"), nil }) var b BackupFromBus if err := json.Unmarshal([]byte(f.read(BackupFile)), &b); err != nil { t.Fatal(err) @@ -153,14 +153,12 @@ func TestAStaleOrUnknownBackupListIsSaidWithItsAge(t *testing.T) { } ok := func(key string, body any) (json.RawMessage, error) { - if strings.Contains(key, "seats") { - return json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","holders":[{"module":"restic","node":"hs"}]}]}}`), nil - } return json.RawMessage(`{"answer":{"data":[{"machine":"hs","module":"restic","path":"/srv/restic"}]}}`), nil } - m.TakeBackupWith("hs", ok) + m.TakeBackupWith("hs", ok, func() (map[string][]byte, error) { return backupHolders("hs", "restic"), nil }) now = now.Add(2 * time.Hour) - m.TakeBackupWith("hs", func(string, any) (json.RawMessage, error) { return nil, errors.New("the bus did not answer") }) + m.TakeBackupWith("hs", func(string, any) (json.RawMessage, error) { return nil, errors.New("the bus did not answer") }, + func() (map[string][]byte, error) { return backupHolders("hs", "restic"), nil }) s := m.Pass(context.Background(), nil) said := strings.Join(append(append([]string{}, s.Said...), s.Warnings...), "\n") if !strings.Contains(said, "2h0m0s") || !strings.Contains(said, "the bus did not answer") { @@ -171,3 +169,11 @@ func TestAStaleOrUnknownBackupListIsSaidWithItsAge(t *testing.T) { t.Errorf("the health does not say the backup list's age: %+v", h) } } + +// backupHolders is the placement seat's state with one holder of node-backup on one node. +func backupHolders(node, module string) map[string][]byte { + return map[string][]byte{"node-backup." + node: []byte(`{"v":1,"seat":"node-backup","scope":"node","node":"` + node + + `","holders":[{"module":"` + module + `","node":"` + node + `"}]}`)} +} + +func noHolders() (map[string][]byte, error) { return nil, errors.New("the state is not read yet") } diff --git a/modules/nfs-server/cmd/nfs-server/guards_test.go b/modules/nfs-server/cmd/nfs-server/guards_test.go index add729ff..5ea2b715 100644 --- a/modules/nfs-server/cmd/nfs-server/guards_test.go +++ b/modules/nfs-server/cmd/nfs-server/guards_test.go @@ -64,7 +64,7 @@ func TestAFailedReloadIsTriedAgainWhenNothingElseAsks(t *testing.T) { } func TestBackupFromRefusesAnAnswerWithoutADataList(t *testing.T) { - seats := json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","holders":[{"module":"restic","node":"ace"}]}]}}`) + seats := backupHolders("ace", "restic") for _, data := range []string{`{"answer":{}}`, `{"answer":{"data":null}}`} { if _, paths, err := BackupFrom("ace", seats, json.RawMessage(data)); err == nil { t.Errorf("%s was read as the empty list %v: never \"nothing to fear\"", data, paths) diff --git a/modules/nfs-server/cmd/nfs-server/main.go b/modules/nfs-server/cmd/nfs-server/main.go index 3406d5a8..8a9db1a1 100644 --- a/modules/nfs-server/cmd/nfs-server/main.go +++ b/modules/nfs-server/cmd/nfs-server/main.go @@ -20,6 +20,7 @@ import ( "time" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/delivery" ) // ExportsState is the state the exports are kept in on the bus; MountsState the `mounts` modules' state, @@ -93,6 +94,26 @@ func askBus(key string, body any) (json.RawMessage, error) { return stdio.Ask(key, body) } +// seatHolderKeys reads the placement seat's state of who holds which seat, every key. +func seatHolderKeys() (map[string][]byte, error) { + state := stdio.State(delivery.SeatHoldersRead) + names, err := state.Keys() + if err != nil { + return nil, err + } + keys := map[string][]byte{} + for _, k := range names { + e, err := state.Get(k) + if err != nil { + return nil, err + } + if e != nil { + keys[k] = e.Value + } + } + return keys, nil +} + // TakeWants reads every node's `mounts` state from the bus and writes what is asked of this machine to // the wants file, for the root process. When the bus does not answer, the last wants stay. func (m *Machine) TakeWants(machine string) { diff --git a/modules/nfs-server/cmd/nfs-server/manifest_test.go b/modules/nfs-server/cmd/nfs-server/manifest_test.go index d43d5776..c4bbedfb 100644 --- a/modules/nfs-server/cmd/nfs-server/manifest_test.go +++ b/modules/nfs-server/cmd/nfs-server/manifest_test.go @@ -2,6 +2,7 @@ package main import ( "encoding/json" + "git.novox.be/novox/mesh-sdk/go/delivery" "os" "regexp" "sort" @@ -137,7 +138,7 @@ func TestTheManifestCarriesGrantsTheRosterTheWantsAndTheRemovalOrder(t *testing. if err := json.Unmarshal([]byte(raw), &full); err != nil { t.Fatal(err) } - if len(full.Reads) != 1 || full.Reads[0] != MountsState { + if len(full.Reads) != 2 || full.Reads[0] != delivery.SeatHoldersRead || full.Reads[1] != MountsState { t.Errorf("reads: %v", full.Reads) } roster := full.Facts["machines"] diff --git a/modules/nfs-server/go.mod b/modules/nfs-server/go.mod index 6e122025..833e932c 100644 --- a/modules/nfs-server/go.mod +++ b/modules/nfs-server/go.mod @@ -1,5 +1,5 @@ module nfsserver -go 1.22 +go 1.26.0 -require git.novox.be/novox/mesh-sdk/go v0.1.7 +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121 diff --git a/modules/nfs-server/go.sum b/modules/nfs-server/go.sum index b474419e..05839926 100644 --- a/modules/nfs-server/go.sum +++ b/modules/nfs-server/go.sum @@ -1,2 +1,14 @@ -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.20261029220947-f9a496d3e121 h1:OsA1v6n736a7RqEV8Nkip8pf5ILMbzj0PWuKqcBOhK0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261029220947-f9a496d3e121/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/modules/nfs-server/module.json b/modules/nfs-server/module.json index d9210c12..a33fb88f 100644 --- a/modules/nfs-server/module.json +++ b/modules/nfs-server/module.json @@ -48,10 +48,10 @@ } ], "reads": [ + "mesh-delivery-assigner.seat-holders", "mounts.shares" ], "invokes": [ - "seat:mesh-controller.seats", "seat:mesh-controller.data" ], "config": {