package main import ( "strings" "testing" ) const timesyncStatus = ` Server: 185.51.192.63 (0.arch.pool.ntp.org) Poll interval: 8min 32s (min: 32s; max 34min 8s) Leap: normal Version: 4 Stratum: 2 Reference: C0AB0196 Precision: 1us (-21) Root distance: 1.418ms (max: 5s) Offset: -1.949ms Delay: 27.986ms Jitter: 1.648ms Packet count: 4 Frequency: -8.704ppm ` func show(load, active, boot string) Ran { return Ran{Stdout: "LoadState=" + load + "\nActiveState=" + active + "\nUnitFileState=" + boot + "\n"} } func unitLine(u string) string { return "systemctl show " + u + " --no-pager --property=LoadState --property=ActiveState --property=UnitFileState" } func TestDurationsAreMilliseconds(t *testing.T) { for in, want := range map[string]float64{"-1.949ms": -1.949, "+27us": 0.027, "1.5s": 1500, "2min": 120000, "500ns": 0.0005} { if got := Millis(in); got == nil || *got-want > 1e-9 || want-*got > 1e-9 { t.Errorf("%s: %v", in, got) } } if Millis("n/a") != nil { t.Error("not a duration") } } func TestStatusReadsTimesyncAndNamesAnotherDaemon(t *testing.T) { m := machine(byLine(map[string]Ran{ "timedatectl show": {Stdout: "NTP=yes\nNTPSynchronized=yes\n"}, unitLine(Daemon): show("loaded", "active", "enabled"), unitLine("ntpd.service"): show("loaded", "inactive", "disabled"), unitLine("chronyd.service"): show("not-found", "inactive", ""), unitLine("openntpd.service"): show("not-found", "inactive", ""), "timedatectl timesync-status": {Stdout: timesyncStatus}, }, nil), 1000) s, err := m.Status() if err != nil { t.Fatal(err) } if !s.Synchronized || !s.NTPEnabled || s.Timesyncd.Active != "active" || s.Server != "185.51.192.63 (0.arch.pool.ntp.org)" { t.Fatalf("%+v", s) } if *s.OffsetMS != -1.949 || *s.DelayMS != 27.986 || s.Stratum != 2 || s.PacketCount != 4 || s.Raw["Leap"] != "normal" { t.Fatalf("%+v", s) } if len(s.Others) != 1 || s.Others[0].Unit != "ntpd.service" { t.Fatalf("others: %+v", s.Others) } } func TestTimesyncNotRunningIsSaidBesideTheRest(t *testing.T) { m := machine(byLine(map[string]Ran{ "timedatectl show": {Stdout: "NTP=no\nNTPSynchronized=yes\n"}, unitLine(Daemon): show("loaded", "inactive", "disabled"), unitLine("ntpd.service"): show("loaded", "active", "enabled"), unitLine("chronyd.service"): show("not-found", "inactive", ""), unitLine("openntpd.service"): show("not-found", "inactive", ""), "timedatectl timesync-status": {Status: 1, Stderr: "Command requires systemd-timesyncd.service, but it is not available: unknown unit\n"}, }, nil), 1000) s, err := m.Status() if err != nil { t.Fatal(err) } if !strings.Contains(s.Error, "requires systemd-timesyncd.service") || s.Others[0].Active != "active" || s.Server != "" { t.Fatalf("%+v", s) } } const catConfigAnchor = `# /etc/systemd/timesyncd.conf # This file is part of systemd. # # See timesyncd.conf(5) for details. [Time] #NTP= #FallbackNTP=0.arch.pool.ntp.org # /etc/systemd/timesyncd.conf.d/50-mesh.conf # The mesh's (module time-sync, novox/hq to-be 42) [Time] NTP=0.europe.pool.ntp.org 1.europe.pool.ntp.org FallbackNTP=0.arch.pool.ntp.org # /etc/systemd/timesyncd.conf.d/provider.conf [Time] NTP=ntp1.provider.example ntp2.provider.example ` func TestServersNameTheFileThatDecidesAndAProvidersDropInWinning(t *testing.T) { m := machine(byLine(map[string]Ran{ "timedatectl show-timesync --all": {Stdout: "LinkNTPServers=\nSystemNTPServers=ntp1.provider.example ntp2.provider.example\nRuntimeNTPServers=\nFallbackNTPServers=0.arch.pool.ntp.org\nServerName=ntp2.provider.example\nServerAddress=2001:db8::2\n"}, "systemd-analyze cat-config systemd/timesyncd.conf": {Stdout: catConfigAnchor}, }, nil), 1000) s, err := m.Servers() if err != nil { t.Fatal(err) } if len(s.Files) != 3 || !s.Files[1].Mesh || s.Files[0].SetsNTP || len(s.Files[1].NTP) != 2 { t.Fatalf("files: %+v", s.Files) } if s.NTPDecidedBy != "/etc/systemd/timesyncd.conf.d/provider.conf" || s.FallbackDecidedBy != MeshDropIn { t.Fatalf("decided: %s / %s", s.NTPDecidedBy, s.FallbackDecidedBy) } if !strings.Contains(s.Note, "provider.conf sorts after the mesh's drop-in") || s.ServerName != "ntp2.provider.example" || len(s.System) != 2 || len(s.Link) != 0 { t.Fatalf("%+v", s) } } func TestAnEmptyAssignmentResetsTheList(t *testing.T) { f := ParseCatConfig("# /etc/a.conf\n[Time]\nNTP=a b\nNTP=\nNTP=c\n") if len(f) != 1 || strings.Join(f[0].NTP, " ") != "c" { t.Fatalf("%+v", f) } } func TestSyncNowRestartsThroughSudoAndWaitsForAPacket(t *testing.T) { var calls []call packets := "0" m := machine(fake(func(c call) Ran { switch { case c.String() == "sudo -n systemctl restart "+Daemon: return Ran{} case c.String() == "timedatectl show": return Ran{Stdout: "NTP=yes\nNTPSynchronized=yes\n"} case c.name == "systemctl": return show("not-found", "inactive", "") case c.String() == "timedatectl timesync-status": r := Ran{Stdout: strings.Replace(timesyncStatus, "Packet count: 4", "Packet count: "+packets, 1)} packets = "1" return r } return Ran{Status: 99} }, &calls), 1000) s, err := m.SyncNow() if err != nil || s.PacketCount != 1 { t.Fatalf("%+v %v", s, err) } if calls[0].String() != "sudo -n systemctl restart "+Daemon { t.Fatalf("first: %s", calls[0]) } } func TestRetireDisablesAnInstalledDaemonAndTakesOutOnlyDanglingLinks(t *testing.T) { var calls []call m := machine(byLine(map[string]Ran{ unitLine("ntpd.service"): show("loaded", "active", "enabled"), unitLine("ntpdate.service"): show("loaded", "inactive", "disabled"), "systemctl disable --now ntpd.service": {}, "systemctl daemon-reload": {}, }, &calls), 0) links := map[string][]string{ "ntpd.service": {"/etc/systemd/system/multi-user.target.wants/ntpd.service"}, "ntpdate.service": {"/etc/systemd/system/multi-user.target.wants/ntpdate.service"}, } gone := map[string]bool{"/etc/systemd/system/multi-user.target.wants/ntpdate.service": true} var removed []string r, err := m.Retire([]string{"ntpd.service", "ntpdate.service"}, func(u string) ([]string, error) { return links[u], nil }, func(p string) bool { return gone[p] }, func(p string) error { removed = append(removed, p); return nil }) if err != nil { t.Fatal(err) } if strings.Join(r.Disabled, ",") != "ntpd.service" || strings.Join(removed, ",") != "/etc/systemd/system/multi-user.target.wants/ntpdate.service" { t.Fatalf("%+v %v", r, removed) } for _, c := range calls { if c.name == "sudo" { t.Fatal("the step runs as root") } } } func TestRetireOnAMachineWithoutTheDaemonDoesNothing(t *testing.T) { var calls []call m := machine(byLine(map[string]Ran{unitLine("ntpd.service"): show("not-found", "inactive", "")}, &calls), 0) r, err := m.Retire([]string{"ntpd.service"}, func(string) ([]string, error) { return nil, nil }, func(string) bool { return false }, nil) if err != nil || len(r.Disabled)+len(r.Removed) != 0 || len(calls) != 1 { t.Fatalf("%+v %v %v", r, err, calls) } if _, err := m.Retire([]string{"../x"}, nil, nil, nil); err == nil { t.Fatal("a path was taken for a unit") } }