125 lines
5.1 KiB
Go
125 lines
5.1 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}
|
|
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 keeper: the machine keeps no bus and says so.
|
|
t.Setenv(KeeperSocketEnv, filepath.Join(dir, "none.sock"))
|
|
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)
|
|
}
|
|
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")
|
|
}
|
|
}
|