diff --git a/src/yangerd/internal/collector/ntp.go b/src/yangerd/internal/collector/ntp.go index f3d7912e..262520f1 100644 --- a/src/yangerd/internal/collector/ntp.go +++ b/src/yangerd/internal/collector/ntp.go @@ -247,7 +247,7 @@ func (c *NTPCollector) addSources(sourcesOut []byte) map[string]interface{} { } stratum, err := strconv.Atoi(parts[3]) - if err != nil || stratum < 1 || stratum > 16 { + if err != nil || stratum > 16 { continue } diff --git a/src/yangerd/internal/collector/ntp_test.go b/src/yangerd/internal/collector/ntp_test.go index 5a6b9e09..954264d7 100644 --- a/src/yangerd/internal/collector/ntp_test.go +++ b/src/yangerd/internal/collector/ntp_test.go @@ -159,9 +159,9 @@ func TestNTPSources(t *testing.T) { t.Fatal("missing source list in sources") } - // 5 sources minus GPS refclock (#) minus stratum-0 (10.0.0.4) = 3 - if len(sources) != 3 { - t.Fatalf("expected 3 sources, got %d", len(sources)) + // 5 sources minus GPS refclock (#) = 4 (stratum 0 is kept) + if len(sources) != 4 { + t.Fatalf("expected 4 sources, got %d", len(sources)) } byAddr := make(map[string]map[string]interface{}) @@ -211,6 +211,18 @@ func TestNTPSources(t *testing.T) { if s3["mode"] != "peer" { t.Fatalf("10.0.0.3 mode: expected peer, got %v", s3["mode"]) } + + // 10.0.0.4: unreachable server (stratum 0) + s4 := byAddr["10.0.0.4"] + if s4 == nil { + t.Fatal("missing source 10.0.0.4") + } + if s4["state"] != "unusable" { + t.Fatalf("10.0.0.4 state: expected unusable, got %v", s4["state"]) + } + if toInt(s4["stratum"]) != 0 { + t.Fatalf("10.0.0.4 stratum: expected 0, got %v", s4["stratum"]) + } } func TestNTPSourcesEmpty(t *testing.T) { diff --git a/src/yangerd/internal/collector/system.go b/src/yangerd/internal/collector/system.go index 17c210c0..fdae4c41 100644 --- a/src/yangerd/internal/collector/system.go +++ b/src/yangerd/internal/collector/system.go @@ -3,9 +3,7 @@ package collector import ( "context" "encoding/json" - "net" "strconv" - "strings" "time" "github.com/kernelkit/infix/src/yangerd/internal/tree" @@ -36,14 +34,15 @@ func (c *SystemCollector) Name() string { return "system" } // Interval implements Collector. func (c *SystemCollector) Interval() time.Duration { return c.interval } -// Collect implements Collector. It merges DNS and service data into -// "ietf-system:system-state". Other system-state subtrees (platform, -// software, users, hostname, timezone, clock, memory, load, filesystems) -// are populated by boot-once, reactive, or on-demand providers. +// Collect implements Collector. It merges service data into +// "ietf-system:system-state". DNS is handled reactively by +// fswatcher on /var/lib/misc/resolv.conf. Other system-state +// subtrees (platform, software, users, hostname, timezone, clock, +// memory, load, filesystems) are populated by boot-once, reactive, +// or on-demand providers. func (c *SystemCollector) Collect(ctx context.Context, t *tree.Tree) error { state := make(map[string]interface{}) - c.addDNS(ctx, state) c.addServices(ctx, state) if data, err := json.Marshal(state); err == nil { @@ -52,78 +51,6 @@ func (c *SystemCollector) Collect(ctx context.Context, t *tree.Tree) error { return nil } -func (c *SystemCollector) addDNS(ctx context.Context, state map[string]interface{}) { - dns := make(map[string]interface{}) - servers := make([]interface{}, 0) - var search []string - options := make(map[string]interface{}) - - headData, err := c.fs.ReadFile("/etc/resolv.conf.head") - if err == nil { - for _, line := range strings.Split(string(headData), "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "nameserver") { - ip := strings.TrimSpace(strings.TrimPrefix(line, "nameserver")) - if net.ParseIP(ip) != nil { - servers = append(servers, map[string]interface{}{ - "address": ip, - "origin": "static", - }) - } - } else if strings.HasPrefix(line, "search") { - search = append(search, strings.Fields(line)[1:]...) - } else if strings.HasPrefix(line, "options") { - for _, opt := range strings.Fields(line)[1:] { - if strings.HasPrefix(opt, "timeout:") { - if v, err := strconv.Atoi(strings.TrimPrefix(opt, "timeout:")); err == nil { - options["timeout"] = v - } - } else if strings.HasPrefix(opt, "attempts:") { - if v, err := strconv.Atoi(strings.TrimPrefix(opt, "attempts:")); err == nil { - options["attempts"] = v - } - } - } - } - } - } - - resolvOut, err := c.cmd.Run(ctx, "/sbin/resolvconf", "-l") - if err == nil { - for _, line := range strings.Split(string(resolvOut), "\n") { - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "nameserver") { - hashParts := strings.SplitN(line, "#", 2) - ip := strings.TrimSpace(strings.TrimPrefix(hashParts[0], "nameserver")) - if net.ParseIP(ip) == nil { - continue - } - entry := map[string]interface{}{ - "address": ip, - "origin": "dhcp", - } - if len(hashParts) > 1 { - entry["interface"] = strings.TrimSpace(hashParts[1]) - } - servers = append(servers, entry) - } else if strings.HasPrefix(line, "search") { - hashParts := strings.SplitN(line, "#", 2) - search = append(search, strings.Fields(hashParts[0])[1:]...) - } - } - } - - if len(options) > 0 { - dns["options"] = options - } - dns["server"] = servers - if len(search) > 0 { - dns["search"] = search - } - - state["infix-system:dns-resolver"] = dns -} - func (c *SystemCollector) addServices(ctx context.Context, state map[string]interface{}) { out, err := c.cmd.Run(ctx, "initctl", "-j") if err != nil { diff --git a/src/yangerd/internal/collector/system_test.go b/src/yangerd/internal/collector/system_test.go index d3f29e02..a1fe43e3 100644 --- a/src/yangerd/internal/collector/system_test.go +++ b/src/yangerd/internal/collector/system_test.go @@ -12,17 +12,6 @@ import ( ) const ( - testResolvHead = `nameserver 8.8.8.8 -nameserver 1.1.1.1 -search example.com local.lan -options timeout:2 attempts:3 -` - - testResolvconfOutput = `nameserver 10.0.0.1 # eth0 -nameserver 10.0.0.2 # eth1 -search dhcp.example.com # eth0 -` - testInitctlJSON = `[ { "identity": "sshd", @@ -48,16 +37,13 @@ search dhcp.example.com # eth0 func newTestCollector() (*SystemCollector, *testutil.MockRunner, *testutil.MockFileReader) { runner := &testutil.MockRunner{ Results: map[string][]byte{ - "initctl -j": []byte(testInitctlJSON), - "/sbin/resolvconf -l": []byte(testResolvconfOutput), + "initctl -j": []byte(testInitctlJSON), }, Errors: map[string]error{}, } fs := &testutil.MockFileReader{ - Files: map[string][]byte{ - "/etc/resolv.conf.head": []byte(testResolvHead), - }, + Files: map[string][]byte{}, Globs: map[string][]string{}, } @@ -98,59 +84,6 @@ func TestSystemCollectorInterval(t *testing.T) { } } -func TestSystemCollectorDNS(t *testing.T) { - c, _, _ := newTestCollector() - state := collectToState(t, c) - - dns, ok := state["infix-system:dns-resolver"].(map[string]interface{}) - if !ok { - t.Fatal("missing infix-system:dns-resolver in system-state") - } - - servers, ok := dns["server"].([]interface{}) - if !ok { - t.Fatal("missing server list in dns-resolver") - } - - // 2 static (resolv.conf.head) + 2 DHCP (resolvconf -l) = 4 total - if len(servers) != 4 { - t.Fatalf("expected 4 DNS servers, got %d: %v", len(servers), servers) - } - - s0 := servers[0].(map[string]interface{}) - if s0["address"] != "8.8.8.8" || s0["origin"] != "static" { - t.Fatalf("server[0]: expected 8.8.8.8/static, got %v", s0) - } - s1 := servers[1].(map[string]interface{}) - if s1["address"] != "1.1.1.1" || s1["origin"] != "static" { - t.Fatalf("server[1]: expected 1.1.1.1/static, got %v", s1) - } - - s2 := servers[2].(map[string]interface{}) - if s2["address"] != "10.0.0.1" || s2["origin"] != "dhcp" { - t.Fatalf("server[2]: expected 10.0.0.1/dhcp, got %v", s2) - } - if s2["interface"] != "eth0" { - t.Fatalf("server[2] interface: expected eth0, got %v", s2["interface"]) - } - - search, ok := dns["search"].([]interface{}) - if !ok || len(search) < 2 { - t.Fatalf("expected search domains, got %v", dns["search"]) - } - - options, ok := dns["options"].(map[string]interface{}) - if !ok { - t.Fatal("missing options in dns-resolver") - } - if int(options["timeout"].(float64)) != 2 { - t.Fatalf("dns timeout: expected 2, got %v", options["timeout"]) - } - if int(options["attempts"].(float64)) != 3 { - t.Fatalf("dns attempts: expected 3, got %v", options["attempts"]) - } -} - func TestSystemCollectorServices(t *testing.T) { c, _, _ := newTestCollector() state := collectToState(t, c) @@ -189,8 +122,7 @@ func TestSystemCollectorCommandFailureGraceful(t *testing.T) { runner := &testutil.MockRunner{ Results: map[string][]byte{}, Errors: map[string]error{ - "initctl -j": fmt.Errorf("not available"), - "/sbin/resolvconf -l": fmt.Errorf("not available"), + "initctl -j": fmt.Errorf("not available"), }, } @@ -228,8 +160,7 @@ func TestSystemCollectorTreeKeys(t *testing.T) { func TestSystemCollectorServicesNilFields(t *testing.T) { runner := &testutil.MockRunner{ Results: map[string][]byte{ - "initctl -j": []byte(`[{"identity":"minimal","pid":999,"status":"running","description":"Minimal service"}]`), - "/sbin/resolvconf -l": []byte(""), + "initctl -j": []byte(`[{"identity":"minimal","pid":999,"status":"running","description":"Minimal service"}]`), }, Errors: map[string]error{}, } @@ -261,33 +192,3 @@ func TestSystemCollectorServicesNilFields(t *testing.T) { t.Fatalf("nil restarts should become 0, got %v", stats["restart-count"]) } } - -func TestSystemCollectorNoDNSEmptyArray(t *testing.T) { - runner := &testutil.MockRunner{ - Results: map[string][]byte{ - "initctl -j": []byte(testInitctlJSON), - "/sbin/resolvconf -l": []byte(""), - }, - Errors: map[string]error{}, - } - - fs := &testutil.MockFileReader{ - Files: map[string][]byte{}, - Globs: map[string][]string{}, - } - - c := NewSystemCollector(runner, fs, 60*time.Second) - state := collectToState(t, c) - - dns, ok := state["infix-system:dns-resolver"].(map[string]interface{}) - if !ok { - t.Fatal("missing infix-system:dns-resolver") - } - servers, ok := dns["server"].([]interface{}) - if !ok { - t.Fatal("dns server should be an array, not null") - } - if len(servers) != 0 { - t.Fatalf("expected empty server array, got %d servers", len(servers)) - } -}