206 lines
8.1 KiB
Go
206 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func hostidFile(v uint32) string {
|
|
b := make([]byte, 4)
|
|
binary.NativeEndian.PutUint32(b, v)
|
|
return string(b)
|
|
}
|
|
|
|
// applyFake is a machine whose pool tank is imported and idle, with the memory and the ARC files of a node.
|
|
func applyFake(t *testing.T, conf string) *fake {
|
|
f := newFake(t)
|
|
f.uid = 0
|
|
f.files[ConfigFile] = conf
|
|
f.files[MemInfoFile] = "MemTotal: 16384000 kB\n"
|
|
f.on(line("zpool", PoolsArgv()), fixture(t, "zpool-list.txt"))
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status.txt"))
|
|
f.files[ParametersDir+"zfs_arc_max"] = "0\n"
|
|
f.files[ParametersDir+"zfs_arc_min"] = "0\n"
|
|
return f
|
|
}
|
|
|
|
func hostidCase(f *fake, label, file string) {
|
|
f.on("zdb -C tank", "MOS Configuration:\n version: 5000\n name: 'tank'\n hostid: "+label+"\n")
|
|
if file != "" {
|
|
f.files[HostIDFile] = file
|
|
}
|
|
}
|
|
|
|
// §15, §31: unset: no zgenhostid; a label of another id or one that cannot be read: refused; 0 or equal: one
|
|
// run; a file holding another value after the write: refused; each refusal unhealthy and healthy again once
|
|
// the label and the file agree or hostid is unset.
|
|
func TestTheHostIDIsConvergedOnlyWhereTheLabelAllowsIt(t *testing.T) {
|
|
const want = "00bab10c" // 12235020
|
|
cases := []struct {
|
|
name, conf, label, file string
|
|
zdbErr bool
|
|
writes bool
|
|
event string
|
|
outcome string
|
|
}{
|
|
{name: "unset", conf: `{"pool":"tank"}`},
|
|
{name: "label of another id", conf: `{"pool":"tank","hostid":"` + want + `"}`, label: "12345", event: "config.refused", outcome: "refused"},
|
|
{name: "label cannot be read", conf: `{"pool":"tank","hostid":"` + want + `"}`, zdbErr: true, event: "config.refused", outcome: "refused"},
|
|
{name: "label 0", conf: `{"pool":"tank","hostid":"` + want + `"}`, label: "0", writes: true, event: "config.applied", outcome: "applied"},
|
|
{name: "label equal", conf: `{"pool":"tank","hostid":"` + want + `"}`, label: "12235020", writes: true, event: "config.applied", outcome: "applied"},
|
|
{name: "file already equal", conf: `{"pool":"tank","hostid":"` + want + `"}`, label: "12235020", file: hostidFile(12235020), outcome: "applied"},
|
|
{name: "another writer's value after the write", conf: `{"pool":"tank","hostid":"` + want + `"}`, label: "0", file: hostidFile(7), writes: true, event: "config.refused", outcome: "refused"},
|
|
}
|
|
for _, c := range cases {
|
|
f := applyFake(t, c.conf)
|
|
if c.zdbErr {
|
|
f.answers["zdb -C tank"] = Ran{Code: 1, Err: "cannot open"}
|
|
} else if c.label != "" {
|
|
hostidCase(f, c.label, c.file)
|
|
}
|
|
if c.writes {
|
|
f.on("zgenhostid -f "+want, "")
|
|
f.after["zgenhostid -f "+want] = func(f *fake) {
|
|
if c.name != "another writer's value after the write" {
|
|
f.files[HostIDFile] = hostidFile(12235020)
|
|
}
|
|
}
|
|
}
|
|
m := f.machine()
|
|
st := memStates{}
|
|
m.States = st
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Errorf("%s: %v", c.name, err)
|
|
}
|
|
if n := len(ran(f, "zgenhostid")); (n == 1) != c.writes || n > 1 {
|
|
t.Errorf("%s: zgenhostid ran %d times", c.name, n)
|
|
}
|
|
if c.event == "" && len(f.emitted) != 0 || c.event != "" && (len(f.emitted) != 1 || f.emitted[0]["_type"] != c.event) {
|
|
t.Errorf("%s: events %v, want %s", c.name, f.emitted, c.event)
|
|
}
|
|
if raw, ok := st["applied/hostid"]; c.outcome != "" && (!ok || !strings.Contains(string(raw), `"outcome":"`+c.outcome+`"`)) {
|
|
t.Errorf("%s: state %v", c.name, st)
|
|
}
|
|
if c.outcome == "refused" {
|
|
// Healthy again once hostid is unset.
|
|
f.files[ConfigFile] = `{"pool":"tank"}`
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Error(err)
|
|
}
|
|
if _, ok := st["applied/hostid"]; ok {
|
|
t.Errorf("%s: the refusal stayed after hostid was unset", c.name)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// §18: the lock is waited for; a scrub, resilver or unimported pool is a wait and the apply ends with exit 0;
|
|
// exit non-zero only for a failed command; ARC values are written live and read back, a differing module
|
|
// option is a warning; with the bus down it converges from the config file.
|
|
func TestTheApplyWaitsConvergesAndExitsAsADR0679Says(t *testing.T) {
|
|
t.Run("lock waited for, ARC written and read back", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","arc-max":4096,"arc-min":512}`)
|
|
m := f.machine()
|
|
m.States = memStates{}
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.locks) != 1 || !f.locks[0] {
|
|
t.Errorf("locks %v: the apply must wait for the lock", f.locks)
|
|
}
|
|
if f.files[ParametersDir+"zfs_arc_max"] != "4294967296" || f.files[ParametersDir+"zfs_arc_min"] != "536870912" {
|
|
t.Errorf("ARC %q %q", f.files[ParametersDir+"zfs_arc_max"], f.files[ParametersDir+"zfs_arc_min"])
|
|
}
|
|
if len(f.emitted) != 2 || f.emitted[0]["read-back"] != "4294967296" {
|
|
t.Errorf("events %v", f.emitted)
|
|
}
|
|
})
|
|
t.Run("a scrub running is a wait", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","arc-max":4096}`)
|
|
f.on(line("zpool", StatusArgv()), fixture(t, "zpool-status-scrubbing.txt"))
|
|
m := f.machine()
|
|
m.States = memStates{}
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.emitted) != 1 || f.emitted[0]["_type"] != "config.waiting" || f.files[ParametersDir+"zfs_arc_max"] != "0\n" {
|
|
t.Errorf("events %v", f.emitted)
|
|
}
|
|
})
|
|
t.Run("an unimported pool is a wait", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"nopool","arc-max":4096}`)
|
|
m := f.machine()
|
|
m.States = memStates{}
|
|
if _, err := m.Apply(context.Background()); err != nil || len(f.emitted) != 1 || f.emitted[0]["_type"] != "config.waiting" {
|
|
t.Errorf("%v %v", err, f.emitted)
|
|
}
|
|
})
|
|
t.Run("a refused value refuses the whole file, exit 0", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","arc-max":4096,"module-options":["bogus=1"]}`)
|
|
m := f.machine()
|
|
m.States = memStates{}
|
|
if _, err := m.Apply(context.Background()); err != nil || len(f.emitted) != 1 || f.emitted[0]["_type"] != "config.refused" ||
|
|
f.files[ParametersDir+"zfs_arc_max"] != "0\n" {
|
|
t.Errorf("%v %v", err, f.emitted)
|
|
}
|
|
})
|
|
t.Run("a failed command is the only non-zero exit", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","pool-properties":{"autotrim":"on"}}`)
|
|
f.on("zpool get -Hp -o value autotrim -- tank", "off\n")
|
|
f.answers["zpool set autotrim=on -- tank"] = Ran{Code: 1, Err: "cannot set"}
|
|
m := f.machine()
|
|
m.States = memStates{}
|
|
if _, err := m.Apply(context.Background()); err == nil {
|
|
t.Error("a failed zpool set exited 0")
|
|
}
|
|
})
|
|
t.Run("a differing module option is a warning, never a failure", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","module-options":["zfs_txg_timeout=10"]}`)
|
|
f.files[ParametersDir+"zfs_txg_timeout"] = "5\n"
|
|
m := f.machine()
|
|
st := memStates{}
|
|
m.States = st
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(m.Warnings) != 1 || !strings.Contains(m.Warnings[0], "next load of the kernel module") ||
|
|
!strings.Contains(string(st["applied/module-options.zfs_txg_timeout"]), OutcomeAwaitingLoad) || len(f.emitted) != 0 {
|
|
t.Errorf("%v %v %v", m.Warnings, st, f.emitted)
|
|
}
|
|
})
|
|
t.Run("with the bus down it converges from the config file", func(t *testing.T) {
|
|
f := applyFake(t, `{"pool":"tank","arc-max":4096}`)
|
|
m := f.machine()
|
|
m.Emit, m.States = nil, nil
|
|
if _, err := m.Apply(context.Background()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.files[ParametersDir+"zfs_arc_max"] != "4294967296" || len(m.Warnings) == 0 {
|
|
t.Errorf("ARC %q, warnings %v", f.files[ParametersDir+"zfs_arc_max"], m.Warnings)
|
|
}
|
|
})
|
|
}
|
|
|
|
// §18: FileLock holds a second taker out, and a second one that waits gets it once the first lets go.
|
|
func TestFileLockHoldsASecondTakerOut(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "apply.lock")
|
|
release, got, err := FileLock(path, false)
|
|
if err != nil || !got {
|
|
t.Fatal(err, got)
|
|
}
|
|
if _, got2, err := FileLock(path, false); err != nil || got2 {
|
|
t.Errorf("second taker: %v %v", got2, err)
|
|
}
|
|
release()
|
|
rel2, got3, err := FileLock(path, false)
|
|
if err != nil || !got3 {
|
|
t.Errorf("after release: %v %v", got3, err)
|
|
}
|
|
rel2()
|
|
_ = os.Remove(path)
|
|
}
|