A failed keeper call in a root process (hand acts, forget, Paused, every call) now adds one warning that the bus is taken as down, instead of being dropped silently. RunKeeper closes the connection acct.Dial opens when the keeper stops; the message id's comment says it is nanotime-pid. Builds: design 42 zfs-owns-zfs A-37
173 lines
6.8 KiB
Go
173 lines
6.8 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 []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
|
|
},
|
|
}
|
|
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"},
|
|
}
|
|
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(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")
|
|
}
|
|
}
|
|
|
|
// 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 := os.MkdirTemp("/var/tmp", "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.Findings == 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 := os.MkdirTemp("/var/tmp", "keeper-down")
|
|
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)
|
|
}
|
|
}
|