summaryrefslogtreecommitdiff
path: root/chrony.go
blob: 2c50a78a2154e6e93657721db58905dae73b6787 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package main

import (
	"context"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
	"time"
)

// Tracking mirrors `chronyc -c tracking`.
type Tracking struct {
	RefID          string  `json:"refId"`
	RefName        string  `json:"refName"`
	Stratum        int     `json:"stratum"`
	RefTime        float64 `json:"refTime"`
	SystemOffset   float64 `json:"systemOffset"` // seconds; positive = system clock slow of NTP time
	LastOffset     float64 `json:"lastOffset"`
	RMSOffset      float64 `json:"rmsOffset"`
	FreqPPM        float64 `json:"freqPpm"` // positive = clock runs slow
	ResidFreqPPM   float64 `json:"residFreqPpm"`
	SkewPPM        float64 `json:"skewPpm"`
	RootDelay      float64 `json:"rootDelay"`
	RootDispersion float64 `json:"rootDispersion"`
	UpdateInterval float64 `json:"updateInterval"`
	Leap           string  `json:"leap"`
}

// Source merges a `chronyc -c sources` row with its `sourcestats` row.
type Source struct {
	Mode      string  `json:"mode"`  // ^ server, = peer, # refclock
	State     string  `json:"state"` // * + - x ~ ?
	Name      string  `json:"name"`
	Stratum   int     `json:"stratum"`
	Poll      int     `json:"poll"`
	Reach     int     `json:"reach"`
	LastRx    int64   `json:"lastRx"` // seconds ago, -1 if never
	Offset    float64 `json:"offset"` // adjusted offset of last sample
	RawOffset float64 `json:"rawOffset"`
	Error     float64 `json:"error"`

	NP     int      `json:"np,omitempty"`
	NR     int      `json:"nr,omitempty"`
	Span   int      `json:"span,omitempty"`
	Freq   *float64 `json:"freqPpm,omitempty"`
	Skew   *float64 `json:"skewPpm,omitempty"`
	StdDev *float64 `json:"stdDev,omitempty"`
}

// ServerStats is the subset of `chronyc -c serverstats` we show. Requires
// access to chronyd's privileged command socket.
type ServerStats struct {
	NTPReceived uint64  `json:"ntpReceived"`
	NTPDropped  uint64  `json:"ntpDropped"`
	RatePerSec  float64 `json:"ratePerSec"`
}

type ChronySnap struct {
	Tracking *Tracking    `json:"tracking,omitempty"`
	Sources  []Source     `json:"sources"`
	Server   *ServerStats `json:"server,omitempty"`
	Error    string       `json:"error,omitempty"`
}

type chronyClient struct {
	path string

	serverStatsDenied bool
	lastServerRx      uint64
	lastServerAt      time.Time
}

func (c *chronyClient) run(ctx context.Context, cmd string) ([][]string, error) {
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()
	out, err := exec.CommandContext(ctx, c.path, "-c", "-n", cmd).Output()
	if err != nil {
		msg := strings.TrimSpace(string(out))
		if ee, ok := err.(*exec.ExitError); ok && len(ee.Stderr) > 0 {
			msg = strings.TrimSpace(string(ee.Stderr))
		}
		if msg == "" {
			msg = err.Error()
		}
		return nil, fmt.Errorf("chronyc %s: %s", cmd, msg)
	}
	var rows [][]string
	for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
		if line = strings.TrimSpace(line); line != "" {
			rows = append(rows, strings.Split(line, ","))
		}
	}
	return rows, nil
}

func f64(s string) float64 { v, _ := strconv.ParseFloat(s, 64); return v }
func atoi(s string) int    { v, _ := strconv.Atoi(s); return v }

func (c *chronyClient) Poll(ctx context.Context) ChronySnap {
	snap := ChronySnap{Sources: []Source{}}
	var errs []string

	if rows, err := c.run(ctx, "tracking"); err != nil {
		errs = append(errs, err.Error())
	} else if len(rows) > 0 && len(rows[0]) >= 14 {
		r := rows[0]
		snap.Tracking = &Tracking{
			RefID: r[0], RefName: r[1], Stratum: atoi(r[2]), RefTime: f64(r[3]),
			SystemOffset: f64(r[4]), LastOffset: f64(r[5]), RMSOffset: f64(r[6]),
			FreqPPM: f64(r[7]), ResidFreqPPM: f64(r[8]), SkewPPM: f64(r[9]),
			RootDelay: f64(r[10]), RootDispersion: f64(r[11]),
			UpdateInterval: f64(r[12]), Leap: r[13],
		}
	}

	if rows, err := c.run(ctx, "sources"); err != nil {
		errs = append(errs, err.Error())
	} else {
		for _, r := range rows {
			if len(r) < 10 {
				continue
			}
			reach, _ := strconv.ParseInt(r[5], 8, 32)
			lastRx, perr := strconv.ParseInt(r[6], 10, 64)
			if perr != nil {
				lastRx = -1
			}
			snap.Sources = append(snap.Sources, Source{
				Mode: r[0], State: r[1], Name: r[2], Stratum: atoi(r[3]), Poll: atoi(r[4]),
				Reach: int(reach), LastRx: lastRx, Offset: f64(r[7]), RawOffset: f64(r[8]), Error: f64(r[9]),
			})
		}
	}

	if rows, err := c.run(ctx, "sourcestats"); err != nil {
		errs = append(errs, err.Error())
	} else {
		byName := map[string]*Source{}
		for i := range snap.Sources {
			byName[snap.Sources[i].Name] = &snap.Sources[i]
		}
		for _, r := range rows {
			if len(r) < 8 {
				continue
			}
			if s := byName[r[0]]; s != nil {
				fr, sk, sd := f64(r[4]), f64(r[5]), f64(r[7])
				s.NP, s.NR, s.Span = atoi(r[1]), atoi(r[2]), atoi(r[3])
				s.Freq, s.Skew, s.StdDev = &fr, &sk, &sd
			}
		}
	}

	if !c.serverStatsDenied {
		if rows, err := c.run(ctx, "serverstats"); err != nil {
			if strings.Contains(err.Error(), "Not authorised") {
				c.serverStatsDenied = true // don't keep spawning a doomed command
			}
		} else if len(rows) > 0 && len(rows[0]) >= 2 {
			rx, _ := strconv.ParseUint(rows[0][0], 10, 64)
			dr, _ := strconv.ParseUint(rows[0][1], 10, 64)
			ss := &ServerStats{NTPReceived: rx, NTPDropped: dr}
			now := time.Now()
			if !c.lastServerAt.IsZero() && rx >= c.lastServerRx {
				ss.RatePerSec = float64(rx-c.lastServerRx) / now.Sub(c.lastServerAt).Seconds()
			}
			c.lastServerRx, c.lastServerAt = rx, now
			snap.Server = ss
		}
	}

	snap.Error = strings.Join(errs, "; ")
	return snap
}