package host import ( "errors" "fmt" "os" "path/filepath" "strings" "testing" ) // systemctlExit is the shape of every answer the two probes get on a real // box: systemd prints the state and exits non-zero to say which state it is // — 3 for is-active's "inactive", 1 for is-enabled's "disabled". A fake that // returned a nil error here would test a systemctl that does not exist. func systemctlExit(status int) error { return fmt.Errorf("exit status %d", status) } func TestEnsureSystemUnitsInstallsAndArmsWhatIsMissing(t *testing.T) { etc := t.TempDir() os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755) lockPath := filepath.Join(t.TempDir(), "lock") var calls []string systemctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } probe := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) switch args[0] { case "is-active": return "inactive\n", systemctlExit(3) case "is-enabled": return "disabled\n", systemctlExit(1) } return "", nil } ensureSystemUnits(etc, lockPath, systemctl, probe, t.Logf) for _, u := range []string{"carlos-system-update.path", "carlos-system-update.service", "carlos-system-recover.service"} { if _, err := os.Stat(filepath.Join(etc, "systemd", "system", u)); err != nil { t.Errorf("%s not installed", u) } } joined := strings.Join(calls, "\n") for _, want := range []string{"daemon-reload", "enable carlos-system-update.path carlos-system-recover.service", "start carlos-system-update.path"} { if !strings.Contains(joined, want) { t.Errorf("calls lack %q:\n%s", want, joined) } } if strings.Contains(joined, "start carlos-system-recover.service") || strings.Contains(joined, "start carlos-system-update.service") { t.Fatal("a service was started") } // All present and armed: no writes, no systemctl beyond the five checks // (is-active x3: the self-update path unit, the control socket and the // db-read socket; is-enabled x2: the recover service and the db-read // socket). The db-read socket is wanted because host.env arms db reads. dbReadsHostEnv(t, etc) calls = nil probe2 := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) switch args[0] { case "is-active": return "active\n", nil case "is-enabled": return "enabled\n", nil } return "", nil } ensureSystemUnits(etc, lockPath, systemctl, probe2, t.Logf) if len(calls) != 5 { t.Fatalf("an armed box got %v", calls) } } // The unit files are not on disk at all, which is what a box the roll script // forgot looks like: is-enabled prints NOTHING and exits 1 for a unit file // systemd cannot find. That silence is "not enabled", not "no systemctl". func TestEnsureSystemUnitsArmsAUnitSystemdHasNeverHeardOf(t *testing.T) { etc := t.TempDir() lockPath := filepath.Join(t.TempDir(), "lock") var calls []string systemctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } probe := func(args ...string) (string, error) { switch args[0] { case "is-active": return "inactive\n", systemctlExit(3) case "is-enabled": return "", systemctlExit(1) } return "", nil } ensureSystemUnits(etc, lockPath, systemctl, probe, t.Logf) for _, u := range []string{"carlos-system-update.path", "carlos-system-update.service", "carlos-system-recover.service"} { if _, err := os.Stat(filepath.Join(etc, "systemd", "system", u)); err != nil { t.Errorf("%s not installed", u) } } if !strings.Contains(strings.Join(calls, "\n"), "start carlos-system-update.path") { t.Errorf("the path unit was never started: %v", calls) } } // An enabled recover unit whose path unit is enabled but never started — // the flagship's own failure — is armed, not left alone. func TestEnsureSystemUnitsStartsAnEnabledButUnstartedPathUnit(t *testing.T) { etc := t.TempDir() unitDir := filepath.Join(etc, "systemd", "system") os.MkdirAll(unitDir, 0o755) for _, u := range systemUnitFiles { os.WriteFile(filepath.Join(unitDir, u), []byte("[Unit]\n"), 0o644) } lockPath := filepath.Join(t.TempDir(), "lock") var calls []string systemctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } probe := func(args ...string) (string, error) { switch args[0] { case "is-active": return "inactive\n", systemctlExit(3) case "is-enabled": return "enabled\n", nil } return "", nil } ensureSystemUnits(etc, lockPath, systemctl, probe, t.Logf) if !strings.Contains(strings.Join(calls, "\n"), "start carlos-system-update.path") { t.Errorf("an enabled-but-inactive path unit was left unstarted: %v", calls) } } func TestEnsureSystemUnitsLeavesAConcurrentApplyAlone(t *testing.T) { etc := t.TempDir() os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755) lockPath := filepath.Join(t.TempDir(), "lock") // Hold the lock as `carlos system apply` would. held, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0o600) if err != nil { t.Fatal(err) } defer held.Close() if err := flockExclusiveNonblock(held); err != nil { t.Skipf("flock unsupported on this platform: %v", err) } var calls []string record := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")) return "", nil } ensureSystemUnits(etc, lockPath, record, record, t.Logf) if len(calls) != 0 { t.Fatalf("a locked-out pass called systemctl: %v", calls) } for _, u := range []string{"carlos-system-update.path", "carlos-system-update.service", "carlos-system-recover.service"} { if _, err := os.Stat(filepath.Join(etc, "systemd", "system", u)); err == nil { t.Errorf("%s was installed by a locked-out pass", u) } } } // No systemctl at all: the probe prints no word, and a box we cannot ask is // a box we do not touch. func TestEnsureSystemUnitsTrustsNoBrokenSystemctl(t *testing.T) { etc := t.TempDir() os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755) lockPath := filepath.Join(t.TempDir(), "lock") systemctl := func(args ...string) (string, error) { return "", errors.New("systemctl: command not found") } ensureSystemUnits(etc, lockPath, systemctl, systemctl, t.Logf) for _, u := range []string{"carlos-system-update.path", "carlos-system-update.service", "carlos-system-recover.service"} { if _, err := os.Stat(filepath.Join(etc, "systemd", "system", u)); err == nil { t.Errorf("%s was installed despite systemctl being unavailable", u) } } } func TestParseIsEnabled(t *testing.T) { yes, no := true, false for _, c := range []struct { out string want *bool }{ {"enabled\n", &yes}, {"enabled-runtime\n", &yes}, {"static\n", &yes}, {"disabled\n", &no}, {"masked\n", &no}, {"", nil}, {"something-systemd-invents\n", nil}, } { got := parseIsEnabled(c.out) switch { case got == nil && c.want != nil: t.Errorf("parseIsEnabled(%q) = nil, want %v", c.out, *c.want) case got != nil && c.want == nil: t.Errorf("parseIsEnabled(%q) = %v, want nil", c.out, *got) case got != nil && *got != *c.want: t.Errorf("parseIsEnabled(%q) = %v, want %v", c.out, *got, *c.want) } } }