Fix ntp and dns status

This commit is contained in:
Mattias Walström
2026-06-27 08:39:48 +02:00
parent 02db44bb82
commit 72d3777d3f
4 changed files with 26 additions and 186 deletions
+1 -1
View File
@@ -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
}
+15 -3
View File
@@ -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) {
+6 -79
View File
@@ -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 {
+4 -103
View File
@@ -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))
}
}