Files
mesh-catalog/modules/zfs/cmd/zfs-tools/keeper_test.go
T
jschoubben 53351dd30d
mesh/delivery merged: source.main moved to novox/mesh-catalog@6ba73d4e2a4f
mesh/delivery-group group feat/47-G-join12 merged: every member is merged
mesh/check failed (2): mesh-delivery-checker: no published of the tagged version of go-meshconditions of me…; and 1 more | 53351dd30df2 on b1125c02cfe6
Merge main into feat/47-G-join11: the zfs module and pacman's config first (issue 1979); zfs's findings in G's form
mesh-conditions keeps G's form: the keeper reads no observer's state (no reads, no Observers list), so main's zfs entry there goes. The zfs follower says its findings through G's findings writer over its keeper: the keeper gains the publish of the writer's three events on the finding stream finding-events and the hear of mesh-conditions.findings-asked on its own durable, nothing else; each pool or device not ONLINE is scope module, id zfs.<pool>.<device> (the device's own name), kind not-online, on the node. The manifest asks its state findings by template in requires (G §529, §577), and gains the finding-events stream and the consumes, and the keeper's bus the three events and the consumes.

Builds: design 47 G §383, §384, §387, §529, §577, A-339, A-340, A-341, A-342; design 42 zfs-owns-zfs §27, A-32, A-37, A-39, A-41

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 09:32:48 +01:00

195 lines
7.9 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"net"
"os"
"path/filepath"
"strings"
"testing"
)
// A-37: the keeper answers a closed list of calls: an event of the module (never scrub.requested), a key of its three
// states, and one of three invokes (the service manager only for the module's two units); anything else is refused.
func TestTheKeeperAnswersOnlyItsClosedListOfCalls(t *testing.T) {
var emitted []string
var called, said []string
st := memStates{}
k := &Keeper{
Emit: func(kind string, body any) error { emitted = append(emitted, kind); return nil },
States: st,
Call: func(key string, body any) (json.RawMessage, error) {
called = append(called, key)
return json.RawMessage(`{"ok":true}`), nil
},
Finding: func(event string, body json.RawMessage, id string) error {
said = append(said, event+"/"+id)
return nil
},
}
req := func(op, kind, state, key, body string) KeeperRequest {
return KeeperRequest{Op: op, Kind: kind, State: state, Key: key, Body: json.RawMessage(body)}
}
cases := []struct {
name string
r KeeperRequest
refused string
}{
{"emit a kind", req(OpEmit, EventApplied, "", "", `{"key":"x"}`), ""},
{"emit the scrub's kind", req(OpEmit, EventScrubRequested, "", "", `{}`), "not an event"},
{"emit another module's kind", req(OpEmit, "package.changed", "", "", `{}`), "not an event"},
{"put applied", req(OpPut, "", StateApplied, "hostid", `{"outcome":"applied"}`), ""},
{"put schedule", req(OpPut, "", StateSchedule, "paused", `{}`), ""},
{"put another state", req(OpPut, "", "holds", "x", `{}`), "not a state"},
{"get findings", req(OpGet, "", StateFindings, "k", ``), ""},
{"keys of another state", req(OpKeys, "", "mirrors", "", ``), "not a state"},
{"delete schedule", req(OpDelete, "", StateSchedule, "paused", ``), ""},
{"invoke record", req(OpInvoke, "", "", ToolRecord, `{}`), ""},
{"invoke schedule", req(OpInvoke, "", "", ToolSchedule, `{}`), ""},
{"start the run", req(OpInvoke, "", "", "seat:node-service-manager.start@ace", `{"scope":"system","unit":"zfs-mesh-run.service"}`), ""},
{"start the apply", req(OpInvoke, "", "", "seat:node-service-manager.start@ace", `{"scope":"system","unit":"zfs-config-apply.service"}`), ""},
{"start another unit", req(OpInvoke, "", "", "seat:node-service-manager.start@ace", `{"scope":"system","unit":"sshd.service"}`), "starts only"},
{"stop a unit", req(OpInvoke, "", "", "seat:node-service-manager.stop@ace", `{"scope":"system","unit":"zfs-mesh-run.service"}`), "not one of the three"},
{"invoke a tool of the module", req(OpInvoke, "", "", "zfs.zfs_snapshot", `{}`), "not one of the three"},
{"another op", req("shell", "", "", "", ``), "not a call"},
// A-339, A-340: the findings writer's three events, and no other, on the finding stream.
{"say a finding written", KeeperRequest{Op: OpFinding, Kind: "finding-written", Body: json.RawMessage(`{}`), ID: "w1"}, ""},
{"say the findings stated", KeeperRequest{Op: OpFinding, Kind: "findings-stated", Body: json.RawMessage(`{}`), ID: "s1"}, ""},
{"say another event as a finding", KeeperRequest{Op: OpFinding, Kind: EventApplied, Body: json.RawMessage(`{}`)}, "not an event of the findings writer"},
}
for _, c := range cases {
a := k.Do(c.r)
if (c.refused == "") != (a.Error == "") || (c.refused != "" && !strings.Contains(a.Error, c.refused)) {
t.Errorf("%s: %+v", c.name, a)
}
}
if len(said) != 2 || said[0] != "finding-written/w1" {
t.Errorf("said %v", said)
}
if len(emitted) != 1 || len(called) != 4 {
t.Errorf("emitted %v, called %v", emitted, called)
}
if _, left := st["applied/hostid"]; !left {
t.Errorf("the put was not kept")
}
}
// shortTempDir is a directory in the test's temporary directory, or in /tmp when a socket in it (`k/k.sock`) would pass 100
// characters, the most a unix socket path takes, so a long TMPDIR cannot fail the bind.
func shortTempDir(prefix string) (string, error) {
dir, err := os.MkdirTemp("", prefix)
if err != nil || len(filepath.Join(dir, "k", "k.sock")) <= 100 {
return dir, err
}
os.RemoveAll(dir)
return os.MkdirTemp("/tmp", prefix)
}
// A-37: only root opens the socket; a root process says and reads through it as through a bus of its own, and with no
// keeper answering it acts as with the bus down, with a warning (A-8).
func TestOnlyRootOpensTheKeepersSocketAndANeedlessKeeperIsTheBusDown(t *testing.T) {
st := memStates{}
uid := 0
k := &Keeper{Emit: func(string, any) error { return nil }, States: st,
Call: func(string, any) (json.RawMessage, error) { return nil, errors.New("down") },
PeerOf: func(net.Conn) (int, error) { return uid, nil }}
dir, err := shortTempDir("zk")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
sock := filepath.Join(dir, "k", "k.sock")
l, err := Listen(sock)
if err != nil {
t.Fatal(err)
}
ctx, stop := context.WithCancel(context.Background())
defer stop()
go k.Serve(ctx, l)
c := keeperClient{sock: sock}
if err := c.Put(StateSchedule, "snapshots", map[string]string{"by": "x"}); err != nil {
t.Fatal(err)
}
if keys, err := c.Keys(StateSchedule); err != nil || len(keys) != 1 || keys[0] != "snapshots" {
t.Errorf("%v %v", keys, err)
}
if v, ok, err := c.Get(StateSchedule, "snapshots"); err != nil || !ok || !strings.Contains(string(v), `"by":"x"`) {
t.Errorf("%s %v %v", v, ok, err)
}
if _, err := c.call(ToolSchedule, map[string]any{}); err == nil || !strings.Contains(err.Error(), "down") {
t.Errorf("an invoke that failed: %v", err)
}
uid = 1000
if err := c.Put(StateSchedule, "x", 1); err == nil || !strings.Contains(err.Error(), "only root") {
t.Errorf("a peer that is not root: %v", err)
}
if _, left := st["schedule/x"]; left {
t.Errorf("a refused peer's call was read")
}
// No socket variable: the machine keeps no bus and says so.
t.Setenv(KeeperSocketEnv, "")
m := newFake(t).machine()
m.Emit = nil
m.JoinBus()
if m.Emit != nil || m.States != nil || len(m.Warnings) != 1 || !strings.Contains(m.Warnings[0], "bus is taken as down") {
t.Errorf("no keeper: %+v", m.Warnings)
}
// A keeper not yet up: the process joins, its calls fail, and the same process uses the keeper once it is up.
late := filepath.Join(dir, "late.sock")
t.Setenv(KeeperSocketEnv, late)
ml := newFake(t).machine()
ml.JoinBus()
if ml.States == nil {
t.Fatalf("a keeper not up yet must not take the bus as down: %+v", ml.Warnings)
}
if _, err := ml.States.Keys(StateSchedule); err == nil {
t.Errorf("no keeper up, yet a call answered")
}
ll, err := Listen(late)
if err != nil {
t.Fatal(err)
}
lctx, lcancel := context.WithCancel(context.Background())
defer lcancel()
go (&Keeper{Emit: func(string, any) error { return nil }, States: memStates{}, PeerOf: func(net.Conn) (int, error) { return 0, nil },
Call: func(string, any) (json.RawMessage, error) { return nil, nil }}).Serve(lctx, ll)
if _, err := ml.States.Keys(StateSchedule); err != nil {
t.Errorf("a keeper that came up later is not used: %v", err)
}
t.Setenv(KeeperSocketEnv, sock)
uid = 0
m2 := newFake(t).machine()
m2.JoinBus()
if m2.Emit == nil || m2.States == nil || m2.Call == nil || m2.keeper == nil {
t.Errorf("a keeper answering: the bus is up")
}
}
// A-37: a root process whose keeper does not answer acts as with the bus down and says so, once, in a warning, for the
// hand acts, the forgetting of applied keys and the pause alike.
func TestAKeeperThatDoesNotAnswerIsWarnedOnce(t *testing.T) {
dir, err := shortTempDir("kd")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
t.Setenv(KeeperSocketEnv, filepath.Join(dir, "none.sock"))
m := newFake(t).machine()
m.JoinBus()
if got := m.handActs(); got != nil {
t.Errorf("hand acts with no keeper: %v", got)
}
if len(m.Warnings) != 1 || !strings.Contains(m.Warnings[0], "bus is taken as down") {
t.Fatalf("hand acts: %v", m.Warnings)
}
m.forget(&Config{})
if m.Paused() {
t.Errorf("paused with no keeper")
}
if len(m.Warnings) != 1 {
t.Errorf("said more than once: %v", m.Warnings)
}
}