Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
One commit with exactly the tree of the reviewed head35b4509c0a, on main12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
This commit is contained in:
@@ -14,12 +14,12 @@ package main
|
||||
// been read, what is exported stays exported and the status says the list is not known: the bus being
|
||||
// slow is no reason to take the library away, and the look below still guards against a repository.
|
||||
//
|
||||
// The parsing of mesh-controller.data's answer is a small copy of the mounts module's (DataFrom). Shared
|
||||
// code belongs in mesh-sdk: moving both there is mesh-sdk issue 13.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
@@ -46,68 +46,46 @@ 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 disk-load's readings
|
||||
// (clause 3). A machine whose backup seat nobody holds, and no readings at all, are errors: never an empty list read as
|
||||
// "nothing to fear".
|
||||
func BackupFrom(machine string, keys map[string][]byte, readings []health.Reading) (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 == "" {
|
||||
return "", nil, fmt.Errorf("no module holds %s on %s", BackupSeat, machine)
|
||||
}
|
||||
var d struct {
|
||||
Answer *struct {
|
||||
Data *[]struct {
|
||||
Machine string `json:"machine"`
|
||||
Module string `json:"module"`
|
||||
Path string `json:"path"`
|
||||
} `json:"data"`
|
||||
} `json:"answer"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &d); err != nil || d.Answer == nil || d.Answer.Data == nil {
|
||||
return "", nil, fmt.Errorf("the controller's data answer holds no data list (%v)", err)
|
||||
if len(readings) == 0 {
|
||||
return "", nil, fmt.Errorf("disk-load keeps no reading of this machine's data yet")
|
||||
}
|
||||
var paths []string
|
||||
for _, item := range *d.Answer.Data {
|
||||
if item.Module == holder && (item.Machine == "" || item.Machine == machine) && path.IsAbs(item.Path) {
|
||||
paths = append(paths, path.Clean(item.Path))
|
||||
for _, r := range readings {
|
||||
if r.Module == holder && (r.Node == "" || r.Node == machine) && path.IsAbs(r.Path) {
|
||||
paths = append(paths, path.Clean(r.Path))
|
||||
}
|
||||
}
|
||||
sort.Strings(paths)
|
||||
return holder, paths, nil
|
||||
}
|
||||
|
||||
// TakeBackup asks the controller over the bus, as the module, through the runtime.
|
||||
// TakeBackup reads the bus, as the module, through the runtime.
|
||||
func (m *Machine) TakeBackup(machine string) {
|
||||
m.TakeBackupWith(machine, askBus)
|
||||
m.TakeBackupWith(machine, readingsOf, 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, readings func(node string) ([]health.Reading, error),
|
||||
holderKeys func() (map[string][]byte, error)) {
|
||||
var last BackupFromBus
|
||||
if text, err := m.Read(BackupFile); err == nil {
|
||||
_ = json.Unmarshal([]byte(text), &last)
|
||||
@@ -115,10 +93,10 @@ 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{})
|
||||
var data json.RawMessage
|
||||
seats, err := holderKeys()
|
||||
var data []health.Reading
|
||||
if err == nil {
|
||||
data, err = ask("seat:mesh-controller.data", map[string]any{"machine": machine})
|
||||
data, err = readings(machine)
|
||||
}
|
||||
if err == nil {
|
||||
var holder string
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -81,8 +83,8 @@ 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}`)
|
||||
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}`)
|
||||
seats := backupHolders("ace", "restic")
|
||||
data := readingsOfPaths("ace", map[string]string{"restic/repository": "/storage/restic", "restic/state": "/var/lib/restic", "plex/data": "/mnt/plex/data"})
|
||||
holder, paths, err := BackupFrom("ace", seats, data)
|
||||
if err != nil || holder != "restic" || strings.Join(paths, " ") != "/storage/restic /var/lib/restic" {
|
||||
t.Errorf("got %q %v %v", holder, paths, err)
|
||||
@@ -90,8 +92,8 @@ func TestBackupFromTheSeatsAndTheData(t *testing.T) {
|
||||
if _, _, err := BackupFrom("g14", seats, data); err == nil {
|
||||
t.Errorf("a machine whose backup seat nobody holds is not said")
|
||||
}
|
||||
if _, _, err := BackupFrom("ace", seats, json.RawMessage(`{"ok":true}`)); err == nil {
|
||||
t.Errorf("an answer with no data list reads as an empty list")
|
||||
if _, _, err := BackupFrom("ace", seats, nil); err == nil {
|
||||
t.Errorf("no readings read as an empty list")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,13 +101,13 @@ func TestBackupFromTheSeatsAndTheData(t *testing.T) {
|
||||
func TestAFailedAskKeepsTheLastBackupPaths(t *testing.T) {
|
||||
f := newFake(t)
|
||||
m := f.machine()
|
||||
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
|
||||
m.TakeBackupWith("ace", func(string) ([]health.Reading, error) {
|
||||
return nil, errors.New("no responders")
|
||||
})
|
||||
}, noHolders)
|
||||
f.backup("restic", "/storage/restic")
|
||||
m.TakeBackupWith("ace", func(string, any) (json.RawMessage, error) {
|
||||
m.TakeBackupWith("ace", func(string) ([]health.Reading, 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)
|
||||
@@ -152,15 +154,13 @@ func TestAStaleOrUnknownBackupListIsSaidWithItsAge(t *testing.T) {
|
||||
t.Errorf("an unknown backup list is not a warning in the health: %+v", h)
|
||||
}
|
||||
|
||||
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
|
||||
ok := func(string) ([]health.Reading, error) {
|
||||
return readingsOfPaths("hs", map[string]string{"restic/repository": "/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) ([]health.Reading, 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 +171,21 @@ 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") }
|
||||
|
||||
// readingsOfPaths is disk-load's readings of one node's items, by "<module>/<item>", each at its path.
|
||||
func readingsOfPaths(node string, items map[string]string) []health.Reading {
|
||||
var out []health.Reading
|
||||
for k, p := range items {
|
||||
module, item, _ := strings.Cut(k, "/")
|
||||
out = append(out, health.Reading{V: 1, Node: node, PlacedDataItem: catalogue.PlacedDataItem{Module: module, ID: item, Path: p}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -63,12 +63,10 @@ func TestAFailedReloadIsTriedAgainWhenNothingElseAsks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackupFromRefusesAnAnswerWithoutADataList(t *testing.T) {
|
||||
seats := json.RawMessage(`{"answer":{"seats":[{"seat":"node-backup","holders":[{"module":"restic","node":"ace"}]}]}}`)
|
||||
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)
|
||||
}
|
||||
func TestBackupFromRefusesNoReadingsAtAll(t *testing.T) {
|
||||
seats := backupHolders("ace", "restic")
|
||||
if _, paths, err := BackupFrom("ace", seats, []health.Reading{}); err == nil {
|
||||
t.Errorf("no readings were read as the empty list %v: never \"nothing to fear\"", paths)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,13 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
)
|
||||
|
||||
// ExportsState is the state the exports are kept in on the bus; MountsState the `mounts` modules' state,
|
||||
@@ -88,9 +91,50 @@ func ShareExports(m *Machine) {
|
||||
}
|
||||
}
|
||||
|
||||
// askBus calls a seat's verb over the bus as this module, through the runtime.
|
||||
func askBus(key string, body any) (json.RawMessage, error) {
|
||||
return stdio.Ask(key, body)
|
||||
// readingsOf reads disk-load's readings of one machine's data items, every key `<node>.<module>.<item>`.
|
||||
func readingsOf(node string) ([]health.Reading, error) {
|
||||
state := stdio.State(health.ReadingsRead)
|
||||
names, err := state.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []health.Reading
|
||||
for _, k := range names {
|
||||
if !strings.HasPrefix(k, node+".") {
|
||||
continue
|
||||
}
|
||||
e, err := state.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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -137,7 +139,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) != 3 || full.Reads[0] != health.ReadingsRead || full.Reads[1] != delivery.SeatHoldersRead || full.Reads[2] != MountsState {
|
||||
t.Errorf("reads: %v", full.Reads)
|
||||
}
|
||||
roster := full.Facts["machines"]
|
||||
|
||||
Reference in New Issue
Block a user