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) }