Three machines ran timesyncd and one ran ntpd. The module declares timesyncd running with a 50-mesh.conf drop-in (European pool) and ntp absent (hq ADR 0180). A run-once step of its Go binary stops and disables ntpd first and takes out only dangling wants-links, so removing the package leaves no enabled unit pointing at nothing. A provider's drop-in sorting after the mesh's still wins and is reported, not removed. Tools: time_sync_status, _servers, _sync_now (to-be 42 Phase 1).
200 lines
7.1 KiB
Go
200 lines
7.1 KiB
Go
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")
|
|
}
|
|
}
|