The second review of #147 found three blockers before the home server. 1. ${setting:} was not filled in a unit's name, so the distribution timers would have reached the machine as "zfs-scrub-weekly@${setting:scrub-pool}.timer". mesh-controller #171 fills it and refuses any value that is not a unit-name part. The module's own schedule needs no setting in a name now: the node-engine's timer for the scheduled process zfs-mesh-scrub fires `zfs-tools scrub-run` every quarter of an hour, and the run reads the pool, cadence, day and time from the configuration file. 2. The run does what upstream's zfs-scrub@.service does: if a scrub is running it waits for it with zpool wait -t scrub, otherwise it starts one and waits. It never pauses or stops a scrub, and the unit has no ExecStop. A resilver defers the scrub. The served slot is kept in /var/lib/zfs-tools. 3. Unassigning left the pool with no schedule. mesh-host #66 (hq ADR 0269, proposed) gives the distribution timer back running and enabled. The README says not to unassign before it is merged. Also: the run comes before the distribution timers in the declaration; the day and time are checked against fixed sets and reach no unit file (an invalid value runs nothing); a due scrub not started an hour after its time is a warning; the error counts outlive a restart in a small state file.
120 lines
4.6 KiB
Go
120 lines
4.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const scrubConf = "pool=tank\ncadence=weekly\nday=Mon\ntime=00:00\n"
|
|
|
|
// scheduled is the healthy fixtures with the module's scrub schedule for tank, its timer waiting and its
|
|
// last run a success.
|
|
func scheduled(t *testing.T) *fake {
|
|
f := withZFS(t)
|
|
f.files[ScrubConfig] = scrubConf
|
|
f.on(line("systemctl", TimerShowArgv(ScrubTimer)),
|
|
"ActiveState=active\nUnitFileState=enabled\nNextElapseUSecRealtime=@1791842400\nLastTriggerUSec=@1791237600\n")
|
|
f.on(line("systemctl", ServiceShowArgv(ScrubService)),
|
|
"ActiveState=inactive\nResult=success\nExecMainStartTimestamp=@1791237600\nExecMainExitTimestamp=@1791331200\n")
|
|
return f
|
|
}
|
|
|
|
func TestTheScheduleIsTheConfigAndWhatTheTimerSays(t *testing.T) {
|
|
f := scheduled(t)
|
|
got, err := f.machine().Scrubs(context.Background(), false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a := got.(ScrubAnswer)
|
|
s := a.Schedule
|
|
if s == nil || s.Pool != "tank" || s.Cadence != "weekly" || s.Timer != "zfs-mesh-scrub.timer" || s.TimerState != "active" ||
|
|
s.Enabled != "enabled" || s.Next == nil || !s.Next.Equal(time.Date(2026, 10, 12, 0, 0, 0, 0, time.Local)) || s.LastResult != "success" {
|
|
t.Fatalf("schedule: %+v", s)
|
|
}
|
|
if want := s.NextSlot(f.now).Format("2006-01-02 15:04"); !strings.Contains(strings.Join(a.Said, "\n"), "next scheduled scrub of tank: "+want) || want != "2026-10-12 00:00" {
|
|
t.Errorf("said: %v", a.Said)
|
|
}
|
|
}
|
|
|
|
func TestAScheduleThatIsNotSetIsSaidAndAnInvalidOneRefused(t *testing.T) {
|
|
f := withZFS(t)
|
|
got, _ := f.machine().Scrubs(context.Background(), false)
|
|
if a := got.(ScrubAnswer); a.Schedule != nil || !strings.Contains(strings.Join(a.Said, " "), "no scrub schedule") {
|
|
t.Errorf("no config: %+v", a)
|
|
}
|
|
for _, conf := range []string{
|
|
"pool=tank\ncadence=daily\nday=Mon\ntime=00:00\n",
|
|
"pool=tank\ncadence=weekly\nday=Monday\ntime=00:00\n",
|
|
"pool=tank\ncadence=weekly\nday=Mon\ntime=25:00\n",
|
|
"pool=tank x\ncadence=weekly\nday=Mon\ntime=00:00\n",
|
|
} {
|
|
if _, err := ParseScrubConfig(conf); err == nil {
|
|
t.Errorf("accepted %q", conf)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheScheduleWarnsOfAnOldScrubAFailedRunAndAStoppedTimer(t *testing.T) {
|
|
f := scheduled(t)
|
|
h := f.machine().Health(context.Background())
|
|
if !h.Healthy {
|
|
t.Fatalf("%+v", h)
|
|
}
|
|
w := strings.Join(h.Warnings, " ")
|
|
// tank's last scrub finished 2026-09-14, 25 days before the fake clock: past weekly plus its margin.
|
|
if !strings.Contains(w, "Pool tank was last scrubbed 25 days ago, more than its weekly schedule allows (9 days)") {
|
|
t.Errorf("warnings: %v", h.Warnings)
|
|
}
|
|
f.on(line("systemctl", ServiceShowArgv(ScrubService)),
|
|
"ActiveState=failed\nResult=exit-code\nExecMainStartTimestamp=@1791237600\nExecMainExitTimestamp=@1791237601\n")
|
|
f.on(line("systemctl", TimerShowArgv(ScrubTimer)),
|
|
"ActiveState=inactive\nUnitFileState=disabled\nNextElapseUSecRealtime=\nLastTriggerUSec=@1791237600\n")
|
|
w = strings.Join(f.machine().Health(context.Background()).Warnings, " ")
|
|
for _, want := range []string{"The last scheduled scrub of tank failed (exit-code)", "The scrub schedule's timer zfs-mesh-scrub.timer is inactive and disabled"} {
|
|
if !strings.Contains(w, want) {
|
|
t.Errorf("lacks %q: %s", want, w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheScrubActsAreFixedCommandsWithWhyReadBackAndRecorded(t *testing.T) {
|
|
for act, cmd := range map[string]string{
|
|
"start": "sudo -n zpool scrub -- tank",
|
|
"pause": "sudo -n zpool scrub -p -- tank",
|
|
"stop": "sudo -n zpool scrub -s -- tank",
|
|
} {
|
|
f := withZFS(t)
|
|
f.on(cmd, "")
|
|
a, err := f.machine().ScrubAct(context.Background(), act, "tank", "the operator asked")
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", act, err)
|
|
}
|
|
if strings.Join(a.Ran, " ") != cmd || a.After == "" || !a.Done {
|
|
t.Errorf("%s: %+v", act, a)
|
|
}
|
|
if len(f.emitted) != 1 || f.emitted[0]["_type"] != ScrubEvent || f.emitted[0]["act"] != act || f.emitted[0]["why"] != "the operator asked" {
|
|
t.Errorf("%s recorded: %+v", act, f.emitted)
|
|
}
|
|
}
|
|
f := withZFS(t)
|
|
for _, c := range []struct{ act, pool, why, says string }{
|
|
{"start", "nope", "why", "no pool"},
|
|
{"start", "tank", "", "say why"},
|
|
{"start", "-s", "why", "no pool"},
|
|
{"destroy", "tank", "why", "not an act"},
|
|
} {
|
|
if _, err := f.machine().ScrubAct(context.Background(), c.act, c.pool, c.why); err == nil || !strings.Contains(err.Error(), c.says) {
|
|
t.Errorf("%+v: %v", c, err)
|
|
}
|
|
}
|
|
if n := len(ran(f, "sudo")); n != 0 {
|
|
t.Errorf("a refusal ran %v", f.calls)
|
|
}
|
|
f.answers["sudo -n zpool scrub -- tank"] = Ran{Code: -1, TimedOut: true}
|
|
if _, err := f.machine().ScrubAct(context.Background(), "start", "tank", "why"); err == nil || !strings.Contains(err.Error(), "outcome unknown; check zfs_scrub") {
|
|
t.Errorf("timed out: %v", err)
|
|
}
|
|
}
|