package main import ( "encoding/json" "os" "path/filepath" "regexp" "sort" "strings" "testing" "time" ) type manifest struct { Tools []string `json:"tools"` Claims []any `json:"claims"` Emits []string `json:"emits"` Replaces map[string][]string `json:"replaces"` Resources []map[string]any `json:"resources"` } func readManifest(t *testing.T) (manifest, string) { t.Helper() raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m manifest if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } return m, string(raw) } // The manifest lists exactly the tools the bundle serves; every one but zfs_snapshot says it only reads, // and zfs_snapshot says it acts. Each says what it replaces (novox/hq ADR 0245), but the health check, // which replaces nothing. No seat: a new mesh or node seat is the controller's to define (ADR 0121). func TestTheManifestNamesTheToolsTheBundleServesAndWhatEachReplaces(t *testing.T) { m, raw := readManifest(t) var served []string for _, tl := range Tools(newFake(t).machine()) { served = append(served, tl.Name) want := "(r)" if tl.Name == "zfs_snapshot" || strings.HasPrefix(tl.Name, "zfs_scrub_") { want = "(a)" } if !strings.HasSuffix(tl.Description, want) { t.Errorf("%s does not end with %s", tl.Name, want) } if tl.Name != "zfs_health" && len(m.Replaces[tl.Name]) == 0 { t.Errorf("%s says nothing of what it replaces", tl.Name) } for _, c := range m.Replaces[tl.Name] { if !strings.Contains(tl.Description, c) { t.Errorf("%s replaces %q in the manifest and not in its description", tl.Name, c) } } } sort.Strings(served) listed := append([]string(nil), m.Tools...) sort.Strings(listed) if strings.Join(served, ",") != strings.Join(listed, ",") { t.Fatalf("served %v, listed %v", served, listed) } if _, ok := m.Replaces["zfs_health"]; ok { t.Errorf("the health check replaces no command") } if len(m.Claims) != 0 { t.Errorf("the module claims no seat: %v", m.Claims) } if strings.Join(m.Emits, ",") != SnapshotEvent+","+ScrubEvent { t.Errorf("emits %v", m.Emits) } if hit := regexp.MustCompile(`\b(g14|novox|ace|shanks|jochen)\b|/home/`).FindString(strings.ReplaceAll(raw, "novox/hq", "")); hit != "" { t.Errorf("the manifest says %q", hit) } } // ADR 0240: zfs_health on the watch, which runs as the operator account; the follower judged by its // unit. The scheduled run is not long-running and declares none. The module installs nothing, so it never // puts ZFS on a machine. func TestTheHealthChecksAndWhoRunsWhat(t *testing.T) { m, raw := readManifest(t) byID := map[string]map[string]any{} for _, r := range m.Resources { byID[r["id"].(string)] = r } w := byID["watch"] if h, _ := w["health"].(map[string]any); h["kind"] != "tool" || h["tool"] != "zfs_health" || w["user"] != "${machine:account}" { t.Errorf("the watch runs as the operator account and carries zfs_health: %v", w) } e := byID["events"] if h, _ := e["health"].(map[string]any); h["kind"] != "unit" || e["user"] != nil { t.Errorf("the follower runs as root, judged by its unit: %v", e) } if d := byID["state-dir"]; d["path"] != "/var/lib/zfs-tools" || d["owner"] != "${machine:account}" || filepath.Dir(ErrorsFile) != d["path"] { t.Errorf("the state the account keeps: %v", d) } // The scheduled run's state is root's alone, apart from the account's (third review). if d := byID["scrub-state-dir"]; d["path"] != filepath.Dir(ServedFile) || d["mode"] != "0700" || d["owner"] != nil { t.Errorf("the state the run keeps: %v", d) } if strings.Contains(raw, `"package"`) { t.Errorf("the module installs no package: ZFS is the machine's, and its kernel module is built against its kernel") } } // One schedule per pool, and no setting in any unit's name but the distribution's timers', which the // controller fills (mesh-controller #171) and checks. The module's own schedule is the node-engine's timer // for the scheduled run, a fixed name; the day and time reach only the configuration, which the run checks. // The run comes before the distribution's timers are stopped, so a run that cannot be installed stops the // apply before the timer that scrubs the pool today is touched. func TestExactlyOneScrubScheduleAndNoSettingInAUnitFile(t *testing.T) { m, raw := readManifest(t) byID := map[string]map[string]any{} order := map[string]int{} for i, r := range m.Resources { byID[r["id"].(string)] = r order[r["id"].(string)] = i } run := byID["scrub-run"] args, _ := run["run"].([]any) if run["type"] != "process" || run["name"] != "zfs-mesh-scrub" || run["schedule"] != "*/15 * * * *" || run["user"] != nil || len(args) != 2 || args[1] != "scrub-run" { t.Errorf("the scheduled run: %v", run) } if ScrubTimer != run["name"].(string)+".timer" || ScrubService != run["name"].(string)+".service" { t.Errorf("the tools read %s and %s; the node-engine names them after %v", ScrubTimer, ScrubService, run["name"]) } for _, c := range []string{"weekly", "monthly"} { d := byID["distribution-"+c] if d["unit"] != "zfs-scrub-"+c+"@${setting:scrub-pool}.timer" || d["state"] != "stopped" || d["boot"] != "disabled" { t.Errorf("the distribution's %s timer: %v", c, d) } if order["scrub-run"] > order["distribution-"+c] { t.Errorf("the distribution's %s timer is stopped before the run is installed", c) } } for _, r := range m.Resources { p, _ := r["path"].(string) if strings.HasPrefix(p, "/etc/systemd/") { t.Errorf("the module writes a unit file of its own: %s", p) } } if strings.Contains(raw, "scrub-day}") && !strings.Contains(byID["scrub-config"]["content"].(string), "${setting:scrub-day}") { t.Errorf("the day reaches something other than the configuration") } if n := strings.Count(raw, "${setting:scrub-day}") + strings.Count(raw, "${setting:scrub-time}") + strings.Count(raw, "${setting:scrub-cadence}"); n != 3 { t.Errorf("the day, time and cadence are written %d times; only into the configuration", n) } var settings struct { Settings map[string]map[string]any `json:"settings"` } json.Unmarshal([]byte(raw), &settings) if settings.Settings["scrub-cadence"]["default"] != "weekly" || settings.Settings["scrub-day"]["default"] != "Mon" || settings.Settings["scrub-time"]["default"] != "00:00" { t.Errorf("defaults: %v", settings.Settings) } if _, has := settings.Settings["scrub-pool"]; has { t.Errorf("the pool is the operator's value, with no default (ADR 0112, ADR 0262)") } } // The home server, composed as the controller fills its settings (pool storage, cadence monthly, the // operator's choice): the units the node-engine is sent and the configuration the run reads. func TestTheHomeServersComposedUnitsAndConfiguration(t *testing.T) { m, _ := readManifest(t) fill := strings.NewReplacer("${setting:scrub-pool}", "storage", "${setting:scrub-cadence}", "monthly", "${setting:scrub-day}", "Mon", "${setting:scrub-time}", "00:00") var units []string var conf string for _, r := range m.Resources { switch { case r["type"] == "service": units = append(units, fill.Replace(r["unit"].(string))+" "+r["state"].(string)+"/"+r["boot"].(string)) case r["type"] == "process" && r["schedule"] != nil: units = append(units, r["name"].(string)+".timer running/enabled (the node-engine's, every quarter of an hour)") case r["id"] == "scrub-config": conf = fill.Replace(r["content"].(string)) } } want := []string{"zfs-mesh-scrub.timer running/enabled (the node-engine's, every quarter of an hour)", "zfs-scrub-weekly@storage.timer stopped/disabled", "zfs-scrub-monthly@storage.timer stopped/disabled"} if strings.Join(units, "\n") != strings.Join(want, "\n") { t.Errorf("units:\n%s", strings.Join(units, "\n")) } for _, u := range units { t.Log(u) } s, err := ParseScrubConfig(conf) if err != nil || s.Pool != "storage" || s.Cadence != "monthly" { t.Fatalf("configuration %q: %v", conf, err) } t.Logf("next scrub of storage after 2026-10-09 12:00: %s", s.NextSlot(time.Date(2026, 10, 9, 12, 0, 0, 0, time.Local)).Format("2006-01-02 15:04")) if next := s.NextSlot(time.Date(2026, 10, 9, 12, 0, 0, 0, time.Local)); !next.Equal(time.Date(2026, 11, 2, 0, 0, 0, 0, time.Local)) { t.Errorf("next monthly slot %s, want the first Monday of November", next) } } // Nothing but reading, a snapshot and a scrub's start, pause or stop. Every command the bundle runs goes // through ask, AsRoot, Escalated, Run, exec.Command or exec.CommandContext with an argument list from one // of the functions below, each held to its program and verb; a call site that builds its own list fails // this test. A new verb needs a decision first, and then a line here. func TestTheBundleRunsOnlyReadingVerbsSnapshotAndScrub(t *testing.T) { allowed := map[string][2]string{ "DatasetsArgv": {"zfs", "list"}, "SnapshotsArgv": {"zfs", "list"}, "PropertiesArgv": {"zfs", "get"}, "SnapshotArgv": {"zfs", "snapshot"}, "PoolsArgv": {"zpool", "list"}, "StatusArgv": {"zpool", "status"}, "IOArgv": {"zpool", "iostat"}, "EventsArgv": {"zpool", "events"}, "HistoryArgv": {"zpool", "history"}, "FollowArgv": {"zpool", "events"}, "ScrubArgv": {"zpool", "scrub"}, "WaitArgv": {"zpool", "wait"}, "TimerShowArgv": {"systemctl", "show"}, "ServiceShowArgv": {"systemctl", "show"}, } verbs := map[string][]string{"DatasetsArgv": DatasetsArgv(), "SnapshotsArgv": SnapshotsArgv(), "PoolsArgv": PoolsArgv(), "StatusArgv": StatusArgv(), "IOArgv": IOArgv(3), "EventsArgv": EventsArgv(), "HistoryArgv": HistoryArgv("tank"), "PropertiesArgv": PropertiesArgv("tank"), "SnapshotArgv": SnapshotArgv("tank", "x"), "FollowArgv": FollowArgv(), "TimerShowArgv": TimerShowArgv("x.timer"), "ServiceShowArgv": ServiceShowArgv("x.service"), "WaitArgv": WaitArgv("tank")} for fn, argv := range verbs { if argv[0] != allowed[fn][1] { t.Errorf("%s runs %q, not %q", fn, argv[0], allowed[fn][1]) } } for _, act := range []string{"start", "pause", "stop", "anything"} { argv := ScrubArgv(act, "tank") if argv[0] != "scrub" || argv[len(argv)-2] != "--" || argv[len(argv)-1] != "tank" || len(argv) > 4 { t.Errorf("%s: %v", act, argv) } if len(argv) == 4 && argv[1] != "-p" && argv[1] != "-s" { t.Errorf("%s: %v", act, argv) } } call := regexp.MustCompile(`(\.(ask|AsRoot|Escalated|Run|Start|Output|CombinedOutput)\(|exec\.Command(Context)?\(|os\.StartProcess\(|syscall\.(Exec|ForkExec)\()`) site := regexp.MustCompile(`(?:\.(?:ask|AsRoot|Escalated)|exec\.Command)\((?:\w+, )?"(zfs|zpool|systemctl)", (\w+)\(`) // The bodies that run what they are given, each once. bodies := map[string]bool{ "machine.go: r := m.Run(ctx, name, args...)": true, "machine.go: r := m.Run(ctx, program, argv...)": true, "machine.go: program, argv := m.Escalated(name, args...)": true, "machine.go: cmd := exec.Command(name, args...)": true, "machine.go: if err := cmd.Start(); err != nil {": true, "follow.go: if err := cmd.Start(); err != nil {": true, } files, _ := filepath.Glob("*.go") sites := 0 for _, file := range files { if strings.HasSuffix(file, "_test.go") { continue } b, err := os.ReadFile(file) if err != nil { t.Fatal(err) } for i, l := range strings.Split(string(b), "\n") { trimmed := strings.TrimSpace(l) if !call.MatchString(l) || strings.HasPrefix(trimmed, "//") || bodies[file+":"+l] { continue } m := site.FindStringSubmatch(l) if m == nil { t.Errorf("%s:%d runs a command not built by an argument function: %s", file, i+1, trimmed) continue } want, ok := allowed[m[2]] if !ok { t.Errorf("%s:%d runs %s's list, which is not held here", file, i+1, m[2]) } else if want[0] != m[1] { t.Errorf("%s:%d runs %s with %s's list", file, i+1, m[1], m[2]) } sites++ } } if sites < 14 { t.Errorf("found %d call sites; the test no longer sees them", sites) } }