diff --git a/README.md b/README.md index f3f332e34..9af331506 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ Go.d.plugin is shipped with [`Netdata`](https://github.com/netdata/netdata). | [activemq](https://github.com/netdata/go.d.plugin/tree/master/modules/activemq) | `ActiveMQ` | | [apache](https://github.com/netdata/go.d.plugin/tree/master/modules/apache) | `Apache` | | [bind](https://github.com/netdata/go.d.plugin/tree/master/modules/bind) | `ISC Bind` | +| [chrony](https://github.com/netdata/go.d.plugin/tree/master/modules/chrony) | `Chrony` | | [cockroachdb](https://github.com/netdata/go.d.plugin/tree/master/modules/cockroachdb) | `CockroachDB` | | [consul](https://github.com/netdata/go.d.plugin/tree/master/modules/consul) | `Consul` | | [coredns](https://github.com/netdata/go.d.plugin/tree/master/modules/coredns) | `CoreDNS` | diff --git a/config/go.d.conf b/config/go.d.conf index 8441e75f1..236d659b9 100644 --- a/config/go.d.conf +++ b/config/go.d.conf @@ -18,7 +18,7 @@ modules: # activemq: yes # apache: yes # bind: yes -# chrony: no +# chrony: yes # cockroachdb: yes # consul: yes # coredns: yes diff --git a/config/go.d/chrony.conf b/config/go.d/chrony.conf index 7293b1832..b66634cf0 100644 --- a/config/go.d/chrony.conf +++ b/config/go.d/chrony.conf @@ -61,11 +61,6 @@ # Syntax: # address: 127.0.0.1:53 # -# - protocol -# DNS query transport protocol. Valid options: udp, tcp, tcp-tls. -# Syntax: -# protocol: udp -# # - timeout # DNS query timeout (dial, write and read) in seconds. # Syntax: @@ -90,10 +85,8 @@ jobs: - name: local - protocol: udp address: '127.0.0.1:323' timeout: 1 # - name: remote -# protocol: udp # address: '203.0.113.0:323' diff --git a/modules/chrony/README.md b/modules/chrony/README.md index 9e7479f0f..65302ba19 100644 --- a/modules/chrony/README.md +++ b/modules/chrony/README.md @@ -1,19 +1,77 @@ -# chrony monitoring with Netdata - -[`chrony`](https://chrony.tuxfamily.org/) is a versatile implementation of the Network Time Protocol (NTP). - -The modules will monitor local host `chrony` server. - -This module use golang to collect chrony and produces: -* stratum -* frequency -* last offset -* RMS offset -* residual freq -* root delay -* root dispersion -* skew -* leap status -* update interval -* current correction -* current source server address \ No newline at end of file + + +# Chrony monitoring with Netdata + +[chrony](https://chrony.tuxfamily.org/) is a versatile implementation of the Network Time Protocol (NTP). + +This module monitors the system's clock performance and peers activity status using Chrony communication protocol v6. + +## Charts + +It produces the following charts: + +- Distance to the reference clock +- Current correction +- Network path delay to stratum-1 +- Dispersion accumulated back to stratum-1 +- Offset on the last clock update +- Long-term average of the offset value +- Frequency +- Residual frequency +- Skew +- Interval between the last two clock updates +- Time since the last measurement +- Leap status +- Peers activity + +## Configuration + +Edit the `go.d/chrony.conf` configuration file using `edit-config` from the +Netdata [config directory](https://learn.netdata.cloud/docs/configure/nodes), which is typically at `/etc/netdata`. + +```bash +cd /etc/netdata # Replace this path with your Netdata config directory, if different +sudo ./edit-config go.d/chrony.conf +``` + +Configuration example: + +```yaml +jobs: + - name: local + address: '127.0.0.1:323' + timeout: 1 + + - name: remote + address: '203.0.113.0:323' + timeout: 3 +``` + +For all available options please see +module [configuration file](https://github.com/netdata/go.d.plugin/blob/master/config/go.d/chrony.conf). + +--- + +## Troubleshooting + +To troubleshoot issues with the `chrony` collector, run the `go.d.plugin` with the debug option enabled. The +output should give you clues as to why the collector isn't working. + +First, navigate to your plugins directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on your +system, open `netdata.conf` and look for the setting `plugins directory`. Once you're in the plugin's directory, switch +to the `netdata` user. + +```bash +cd /usr/libexec/netdata/plugins.d/ +sudo -u netdata -s +``` + +You can now run the `go.d.plugin` to debug the collector: + +```bash +./go.d.plugin -d -m chrony +``` diff --git a/modules/chrony/charts.go b/modules/chrony/charts.go index 472be6754..7160452b2 100644 --- a/modules/chrony/charts.go +++ b/modules/chrony/charts.go @@ -4,146 +4,148 @@ package chrony import ( "github.com/netdata/go.d.plugin/agent/module" - "net" + "github.com/netdata/go.d.plugin/modules/chrony/client" ) +const scaleFactor = client.ScaleFactor + var charts = module.Charts{ - { - ID: "running", - Title: "chrony is functional and can be monitored", - Units: "hop", - Type: module.Area, - Ctx: "chrony.running", - Dims: module.Dims{ - {ID: "running", Name: "running", Algo: module.Absolute, Div: 1, Mul: 1}, - }, - }, { ID: "stratum", - Title: "distance from reference clock", + Title: "Distance to the reference clock", Units: "level", - Type: module.Area, + Fam: "stratum", Ctx: "chrony.stratum", Dims: module.Dims{ - {ID: "stratum", Name: "stratum", Algo: module.Absolute, Div: 1, Mul: 1}, + {ID: "stratum", Name: "stratum"}, }, }, { - ID: "leap_status", - // LEAP_Normal = 0, - // LEAP_InsertSecond = 1, - // LEAP_DeleteSecond = 2, - // LEAP_Unsynchronised = 3 - Title: "Leap status can be Normal, Insert second, Delete second or Not synchronised.", - Units: "hop", - Ctx: "chrony.leap_status", + ID: "current_correction", + Title: "Current correction", + Units: "seconds", + Fam: "correction", + Ctx: "chrony.current_correction", Dims: module.Dims{ - {ID: "leap_status", Name: "leap_status", Algo: module.Absolute, Div: 1, Mul: 1}, + {ID: "current_correction", Div: scaleFactor}, }, }, { ID: "root_delay", - Title: "the total of the network path delays to the stratum-1 computer", + Title: "Network path delay to stratum-1", Units: "seconds", - Type: module.Area, + Fam: "root", Ctx: "chrony.root_delay", Dims: module.Dims{ - {ID: "root_delay", Name: "root_delay", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "root_delay", Div: scaleFactor}, }, }, { ID: "root_dispersion", - Title: "total dispersion accumulated through all the computers back to the stratum-1 computer", + Title: "Dispersion accumulated back to stratum-1", Units: "seconds", - Type: module.Area, + Fam: "root", Ctx: "chrony.root_dispersion", Dims: module.Dims{ - {ID: "root_dispersion", Name: "root_dispersion", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "root_dispersion", Div: scaleFactor}, }, }, { - ID: "skew", - Title: "estimated error bound on the frequency", - Units: "ppm", - Type: module.Area, - Ctx: "chrony.skew", + ID: "last_offset", + Title: "Offset on the last clock update", + Units: "seconds", + Fam: "offset", + Ctx: "chrony.last_offset", + Dims: module.Dims{ + {ID: "last_offset", Name: "offset", Div: scaleFactor}, + }, + }, + { + ID: "rms_offset", + Title: "Long-term average of the offset value", + Units: "seconds", + Fam: "offset", + Ctx: "chrony.rms_offset", Dims: module.Dims{ - {ID: "skew", Name: "skew", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "rms_offset", Name: "offset", Div: scaleFactor}, }, }, { ID: "frequency", - Title: "the rate by which the system’s clock would be would be wrong", + Title: "Frequency", Units: "ppm", - Type: module.Area, + Fam: "frequency", Ctx: "chrony.frequency", Dims: module.Dims{ - {ID: "frequency", Name: "frequency", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "frequency", Div: scaleFactor}, }, }, { - ID: "offset", - Title: "the offset between clock update", - Units: "seconds", - Type: module.Area, - Ctx: "chrony.offset", + ID: "residual_frequency", + Title: "Residual frequency", + Units: "ppm", + Fam: "frequency", + Ctx: "chrony.residual_frequency", Dims: module.Dims{ - {ID: "last_offset", Name: "last", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, - {ID: "rms_offset", Name: "rms", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "residual_frequency", Div: scaleFactor}, }, }, { - ID: "update_interval", - Title: "last clock update interval", - Units: "seconds", - Type: module.Area, - Ctx: "chrony.update_interval", + ID: "skew", + Title: "Skew", + Units: "ppm", + Fam: "frequency", + Ctx: "chrony.skew", Dims: module.Dims{ - {ID: "update_interval", Name: "update_interval", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "skew", Div: scaleFactor}, }, }, { - ID: "current_correction", - Title: "last clock update interval", + ID: "update_interval", + Title: "Interval between the last two clock updates", Units: "seconds", - Type: module.Area, - Ctx: "chrony.current_correction", + Fam: "updates", + Ctx: "chrony.update_interval", Dims: module.Dims{ - {ID: "current_correction", Name: "current_correction", Algo: module.Absolute, Div: scaleFactor, Mul: 1}, + {ID: "update_interval", Div: scaleFactor}, }, }, { - ID: "ref_timestamp", - Title: "last clock update interval", + ID: "ref_measurement_time", + Title: "Time since the last measurement", Units: "seconds", - Type: module.Line, - Ctx: "chrony.ref_timestamp", + Fam: "updates", + Ctx: "chrony.ref_measurement_time", Dims: module.Dims{ - {ID: "ref_timestamp", Name: "ref_timestamp", Algo: module.Absolute, Div: 1, Mul: 1}, + {ID: "ref_measurement_time"}, }, }, { - ID: "activity", - Title: "activity status", - Units: "count", - Ctx: "chrony.activity", - Type: module.Area, + ID: "leap_status", + Title: "Leap status", + Units: "status", + Fam: "leap status", + Ctx: "chrony.leap_status", Dims: module.Dims{ - {ID: "online_sources", Name: "online_sources", Algo: module.Absolute, Div: 1, Mul: 1}, - {ID: "offline_sources", Name: "offline_sources", Algo: module.Absolute, Div: 1, Mul: 1}, - {ID: "burst_online_sources", Name: "burst_online_sources", Algo: module.Absolute, Div: 1, Mul: 1}, - {ID: "burst_offline_sources", Name: "burst_offline_sources", Algo: module.Absolute, Div: 1, Mul: 1}, - {ID: "unresolved_sources", Name: "unresolved_sources", Algo: module.Absolute, Div: 1, Mul: 1}, + {ID: "leap_status_normal", Name: "normal"}, + {ID: "leap_status_insert_second", Name: "insert_second"}, + {ID: "leap_status_delete_second", Name: "delete_second"}, + {ID: "leap_status_unsynchronised", Name: "unsynchronised"}, }, }, { - ID: "source", - Title: "Activity Source Server", - Units: "hop", - Ctx: "chrony.source", - Type: module.Area, + ID: "activity", + Title: "Peers activity", + Units: "sources", + Fam: "activity", + Ctx: "chrony.activity", + Type: module.Stacked, Dims: module.Dims{ - {ID: net.IPv4zero.String(), Name: net.IPv4zero.String(), Algo: module.Absolute, Div: 1, Mul: 1}, + {ID: "online_sources", Name: "online"}, + {ID: "offline_sources", Name: "offline"}, + {ID: "burst_online_sources", Name: "burst_online"}, + {ID: "burst_offline_sources", Name: "burst_offline"}, + {ID: "unresolved_sources", Name: "unresolved"}, }, }, } diff --git a/modules/chrony/chrony.go b/modules/chrony/chrony.go index c2df59c5f..9528e18d2 100644 --- a/modules/chrony/chrony.go +++ b/modules/chrony/chrony.go @@ -3,195 +3,89 @@ package chrony import ( - "net" "time" "github.com/netdata/go.d.plugin/agent/module" -) - -type ( - Config struct { - Protocol string `yaml:"protocol"` - Address string `yaml:"address"` - Timeout int `yaml:"timeout"` // Millisecond - } - - // Chrony is the main collector for chrony - Chrony struct { - module.Base // should be embedded by every module - Config `yaml:",inline"` - chronyVersion uint8 - latestSource net.IP - conn net.Conn - charts *module.Charts - } -) - -var ( - // chronyCmdAddr is the chrony local port - chronyDefaultProtocol = "udp" - chronyDefaultCmdAddr = "127.0.0.1:323" - chronyDefaultTimeout = 1 + "github.com/netdata/go.d.plugin/modules/chrony/client" + "github.com/netdata/go.d.plugin/pkg/web" ) func init() { - creator := module.Creator{ - Defaults: module.Defaults{ - Disabled: true, - }, + module.Register("chrony", module.Creator{ Create: func() module.Module { return New() }, - } - - module.Register("chrony", creator) + }) } -// New creates Chrony exposing local status of a chrony daemon func New() *Chrony { return &Chrony{ Config: Config{ - Protocol: chronyDefaultProtocol, - Address: chronyDefaultCmdAddr, - Timeout: 1, + Address: "127.0.0.1:323", + Timeout: web.Duration{Duration: time.Second}, + }, + charts: charts.Copy(), + newClient: func(c *Chrony) (chronyClient, error) { + return client.New(c.Logger, client.Config{ + Address: c.Config.Address, + Timeout: c.Config.Timeout.Duration, + }) }, - charts: &charts, - latestSource: net.IPv4zero, } } -// Cleanup makes cleanup -func (c *Chrony) Cleanup() { +type Config struct { + Address string `yaml:"address"` + Timeout web.Duration `yaml:"timeout"` } -// Init makes initialization -func (c *Chrony) Init() bool { - if c.Timeout <= 0 { - c.Timeout = chronyDefaultTimeout +type ( + Chrony struct { + module.Base + Config `yaml:",inline"` + + charts *module.Charts + + newClient func(c *Chrony) (chronyClient, error) + client chronyClient } + chronyClient interface { + Tracking() (*client.TrackingPayload, error) + Activity() (*client.ActivityPayload, error) + Close() + } +) - conn, err := net.DialTimeout(c.Protocol, c.Address, time.Duration(c.Timeout)*time.Millisecond) - if err != nil { - c.Errorf( - "unable connect to chrony addr %s:%s err: %s, is chrony up and running?", - c.Protocol, c.Address, err) +func (c *Chrony) Init() bool { + if err := c.validateConfig(); err != nil { + c.Errorf("config validation: %v", err) return false } - c.conn = conn return true } -// Check makes check func (c *Chrony) Check() bool { - err := c.applyChronyVersion() - if err != nil { - c.Errorf("get chrony version failed with err: %s", err) - return false - } - - return true + return len(c.Collect()) > 0 } -// Charts creates Charts dynamically func (c *Chrony) Charts() *module.Charts { return c.charts } -// Collect collects metrics func (c *Chrony) Collect() map[string]int64 { - // collect all we need and sent Exception to sentry - res := map[string]int64{"running": 0} - - if !c.running() { - return res - } - res["running"] = 1 - - tra := c.collectTracking() - for k, v := range tra { - res[k] = v - } - - act := c.collectActivity() - for k, v := range act { - res[k] = v - } - - return res -} - -func (c *Chrony) running() bool { - err := c.submitEmptyRequest() + mx, err := c.collect() if err != nil { - c.Errorf("contract chrony failed with err: %s", err) - return false + c.Error(err) } - return true -} - -func (c *Chrony) collectTracking() (res map[string]int64) { - res = make(map[string]int64) - tracking, err := c.fetchTracking() - if err != nil { - c.Errorf("fetch tracking status failed: %s", err) - res["running"] = 0 - return - } - c.Debugf(tracking.String()) - - res["running"] = 1 - res["stratum"] = (int64)(tracking.Stratum) - res["leap_status"] = (int64)(tracking.LeapStatus) - res["root_delay"] = (int64)(tracking.RootDelay.Int64()) - res["root_dispersion"] = (int64)(tracking.RootDispersion.Int64()) - res["skew"] = (int64)(tracking.SkewPpm.Int64()) - res["frequency"] = (int64)(tracking.FreqPpm.Int64()) - res["last_offset"] = (int64)(tracking.LastOffset.Int64()) - res["rms_offset"] = (int64)(tracking.RmsOffset.Int64()) - res["update_interval"] = (int64)(tracking.LastUpdateInterval.Int64()) - res["current_correction"] = (int64)(tracking.LastUpdateInterval.Int64()) - res["ref_timestamp"] = tracking.RefTime.Time().Unix() - - sourceIp := tracking.Ip.Ip() - - if !sourceIp.Equal(c.latestSource) { - chart := c.charts.Get("source") - _ = chart.AddDim(&module.Dim{ - ID: sourceIp.String(), Name: sourceIp.String(), Algo: module.Absolute, Div: 1, Mul: 1, - }) - _ = chart.RemoveDim(c.latestSource.String()) - - // you should let go.d.plugin know that something has been changed, and print dimension again. - chart.MarkNotCreated() - - c.Debugf("source change from %s to %s", c.latestSource, sourceIp) - c.latestSource = sourceIp - } - res[c.latestSource.String()] = 1 - if sourceIp.Equal(net.IPv4zero) || sourceIp.Equal(net.IPv6zero) { - c.Warningf("chrony not select valid upstream") + if len(mx) == 0 { + return nil } - - return + return mx } -func (c *Chrony) collectActivity() (res map[string]int64) { - res = make(map[string]int64) - activity, err := c.fetchActivity() - if err != nil { - c.Errorf("fetch activity status failed: %s", err) - return - } - c.Debug(activity.String()) - - res["online_sources"] = int64(activity.Online) - res["offline_sources"] = int64(activity.Offline) - res["burst_online_sources"] = int64(activity.BurstOnline) - res["burst_offline_sources"] = int64(activity.BurstOffline) - res["unresolved_sources"] = int64(activity.Unresolved) - - if activity.Online == 0 { - c.Warningf("chrony have no available upstream") +func (c *Chrony) Cleanup() { + if c.client != nil { + c.client.Close() + c.client = nil } - return res } diff --git a/modules/chrony/chrony_cmd.go b/modules/chrony/chrony_cmd.go deleted file mode 100644 index 7a2f69122..000000000 --- a/modules/chrony/chrony_cmd.go +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package chrony - -import ( - "bytes" - "encoding/binary" - "fmt" - "time" -) - -func (c *Chrony) submitRequest(req *requestPacket) (*replyPacket, *bytes.Reader, error) { - conn := c.conn - var err error - - var seqNumber uint32 - if req.SeqNumber != 0 { - seqNumber = req.SeqNumber - } else { - seqNumber = uint32(time.Now().Unix()) - req.SeqNumber = seqNumber - } - - // request marshal then write - if err := binary.Write(conn, binary.BigEndian, req); err != nil { - return nil, nil, fmt.Errorf("failed to write request: %s", err) - } - - // get rsp - var rspLen int - dgram := make([]byte, 10240) - rspLen, err = conn.Read(dgram) - if err != nil { - return nil, nil, err - } - c.Debugf("read %d byte from response", rspLen) - - rd := bytes.NewReader(dgram) - var reply replyPacket - if err := binary.Read(rd, binary.BigEndian, &reply); err != nil { - return nil, nil, fmt.Errorf("failed to get relay from conn: %s", err) - } - c.Debugf("req: %+v rsp:%+v\n", req, reply) - - // check every fields - if reply.SeqNum != seqNumber { - return &reply, rd, fmt.Errorf("unexpected tracking packet seqNumber: %d", reply.SeqNum) - } - - if reply.Version != req.Version { - return &reply, rd, fmt.Errorf("unexpected chrony protocol version: %d", reply.Version) - } - - return &reply, rd, nil -} - -func (c *Chrony) parseChronyReply(reply *replyPacket, rd *bytes.Reader, err error) (*replyPacket, interface{}, error) { - switch reply.PktType { - case pktTypeCMDReply: - default: - return reply, nil, fmt.Errorf("unexpected chrony reply type: %d", reply.PktType) - } - - // get command from relay then apply - var payload interface{} - switch reply.Command { - case reqActivity: - payload = &activityPayload{} - case reqTracking: - payload = &trackingPayload{} - default: - payload = make([]byte, rd.Len()) - err = fmt.Errorf("unexpected reply command: %d", reply.Command) - } - - // get rsp body - if err := binary.Read(rd, binary.BigEndian, payload); err != nil { - return reply, nil, fmt.Errorf("failed reading payload: %s", err) - } - - return reply, payload, err -} - -func (c *Chrony) fetchTracking() (*trackingPayload, error) { - req := c.emptyRequest() - req.Command = reqTracking - - _, trackingPtr, err := c.parseChronyReply(c.submitRequest(req)) - if err != nil { - return nil, err - } - - return trackingPtr.(*trackingPayload), nil -} - -func (c *Chrony) fetchActivity() (*activityPayload, error) { - req := c.emptyRequest() - req.Command = reqActivity - - _, activityPtr, err := c.parseChronyReply(c.submitRequest(req)) - if err != nil { - return nil, err - } - - return activityPtr.(*activityPayload), nil -} - -func (c *Chrony) emptyRequest() *requestPacket { - // Check() func would init the value. - if c.chronyVersion == 0 { - err := c.applyChronyVersion() - if err != nil { - panic(err) // unexpected chrony protocol version, we can't collect data correct. - } - } - return &requestPacket{ - Version: c.chronyVersion, - PktType: pktTypeCMDRequest, - } -} - -func (c *Chrony) submitEmptyRequest() error { - _, _, err := c.submitRequest(c.emptyRequest()) - return err -} - -func (c *Chrony) applyChronyVersion() error { - - tryProtocolVersion := []uint8{ - protoVersionNumber6, - protoVersionNumber5, - } - for _, version := range tryProtocolVersion { - rpy, _, err := c.submitRequest(&requestPacket{ - Version: version, - PktType: pktTypeCMDRequest, - Command: 0, - }) - if err != nil { - c.Debugf("contact chrony failed with err: %+v", err) - continue - } - - c.Debugf("chrony reply protocol version: %d", rpy.Version) - if version == rpy.Version { - c.chronyVersion = version - return nil - } - } - - c.Warningf("will use default chrony protocol version") - c.chronyVersion = protoVersionNumber - return nil - //return fmt.Errorf("unexpected chrony protocol version") -} diff --git a/modules/chrony/chrony_test.go b/modules/chrony/chrony_test.go index 6475fe4b5..8d8dd27e3 100644 --- a/modules/chrony/chrony_test.go +++ b/modules/chrony/chrony_test.go @@ -3,56 +3,281 @@ package chrony import ( + "errors" "testing" + "github.com/netdata/go.d.plugin/modules/chrony/client" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -// We can't fake a reply based on the current request, -// otherwise we don't know if chrony's reply can really be processed, -// so all of these test need chrony running, and listen at default port. +func TestChrony_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantFail bool + }{ + "default config": { + config: New().Config, + }, + "unset 'address'": { + wantFail: true, + config: Config{ + Address: "", + }, + }, + } -func TestNew(t *testing.T) { - assert.IsType(t, (*Chrony)(nil), New()) -} + for name, test := range tests { + t.Run(name, func(t *testing.T) { + chrony := New() + chrony.Config = test.config -func TestChrony_Init(t *testing.T) { - assert.True(t, New().Init()) + if test.wantFail { + assert.False(t, chrony.Init()) + } else { + assert.True(t, chrony.Init()) + } + }) + } } func TestChrony_Check(t *testing.T) { - mod := New() - mod.Init() - assert.True(t, mod.Check()) + tests := map[string]struct { + prepare func() *Chrony + wantFail bool + }{ + "tracking: success, activity: success": { + wantFail: false, + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{}) }, + }, + "tracking: success, activity: fail": { + wantFail: false, + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnActivity: true}) }, + }, + "tracking: fail, activity: success": { + wantFail: true, + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnTracking: true}) }, + }, + "tracking: fail, activity: fail": { + wantFail: true, + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnTracking: true}) }, + }, + "fail on creating client": { + wantFail: true, + prepare: func() *Chrony { return prepareChronyWithMock(nil) }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + chrony := test.prepare() + + require.True(t, chrony.Init()) + + if test.wantFail { + assert.False(t, chrony.Check()) + } else { + assert.True(t, chrony.Check()) + } + }) + } } func TestChrony_Charts(t *testing.T) { - assert.NotNil(t, New().Charts()) + assert.Equal(t, len(charts), len(*New().Charts())) } func TestChrony_Cleanup(t *testing.T) { - New().Cleanup() -} - -//func TestChrony_Collect(t *testing.T) { -// mod := New() -// mod.Init() -// -// ans := mod.Collect() -// -// // should have something in result -// assert.NotNil(t, mod.Collect()) -// // chrony should be running -// if ans["running"] == 1 { -// // in most cases, the leap second status should be 0 -// assert.EqualValues(t, 0, ans["leap_status"]) -// -// // should collect source server -// assert.True(t, mod.Charts().Has("source")) -// // if chrony syncs upstream normally, the source should not be 0.0.0.0 -// assert.False(t, mod.Charts().Get("source").HasDim(net.IPv4zero.String())) -// // if chrony syncs upstream normally, should at least one online source -// assert.NotEqualValues(t, 0, ans["online_sources"]) -// } -// -//} + tests := map[string]struct { + prepare func(c *Chrony) + wantClose bool + }{ + "after New": { + wantClose: false, + prepare: func(c *Chrony) {}, + }, + "after Init": { + wantClose: false, + prepare: func(c *Chrony) { c.Init() }, + }, + "after Check": { + wantClose: true, + prepare: func(c *Chrony) { c.Init(); c.Check() }, + }, + "after Collect": { + wantClose: true, + prepare: func(c *Chrony) { c.Init(); c.Collect() }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + m := &mockClient{} + chrony := prepareChronyWithMock(m) + test.prepare(chrony) + + require.NotPanics(t, chrony.Cleanup) + + if test.wantClose { + assert.True(t, m.closeCalled) + } else { + assert.False(t, m.closeCalled) + } + }) + } +} + +func TestChrony_Collect(t *testing.T) { + tests := map[string]struct { + prepare func() *Chrony + expected map[string]int64 + }{ + "tracking: success, activity: success": { + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{}) }, + expected: map[string]int64{ + "burst_offline_sources": 3, + "burst_online_sources": 4, + "current_correction": 111249, + "frequency": 51036781311, + "last_offset": -88888, + "leap_status_delete_second": 0, + "leap_status_insert_second": 1, + "leap_status_normal": 0, + "leap_status_unsynchronised": 0, + "offline_sources": 2, + "online_sources": 8, + "ref_measurement_time": 1667, + "residual_frequency": -3401879, + "rms_offset": 359872, + "root_delay": 51769230, + "root_dispersion": 1243559, + "skew": 67318372, + "stratum": 3, + "unresolved_sources": 1, + "update_interval": 1038400390625, + }, + }, + "tracking: success, activity: fail": { + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnActivity: true}) }, + expected: map[string]int64{ + "current_correction": 111249, + "frequency": 51036781311, + "last_offset": -88888, + "leap_status_delete_second": 0, + "leap_status_insert_second": 1, + "leap_status_normal": 0, + "leap_status_unsynchronised": 0, + "ref_measurement_time": 1667, + "residual_frequency": -3401879, + "rms_offset": 359872, + "root_delay": 51769230, + "root_dispersion": 1243559, + "skew": 67318372, + "stratum": 3, + "update_interval": 1038400390625, + }, + }, + "tracking: fail, activity: success": { + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnTracking: true}) }, + expected: nil, + }, + "tracking: fail, activity: fail": { + prepare: func() *Chrony { return prepareChronyWithMock(&mockClient{errOnTracking: true}) }, + expected: nil, + }, + "fail on creating client": { + prepare: func() *Chrony { return prepareChronyWithMock(nil) }, + expected: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + chrony := test.prepare() + + require.True(t, chrony.Init()) + _ = chrony.Check() + + collected := chrony.Collect() + copyRefMeasurementTime(collected, test.expected) + + assert.Equal(t, test.expected, collected) + }) + } +} + +func prepareChronyWithMock(m *mockClient) *Chrony { + c := New() + if m == nil { + c.newClient = func(c *Chrony) (chronyClient, error) { return nil, errors.New("mock.newClient error") } + } else { + c.newClient = func(c *Chrony) (chronyClient, error) { return m, nil } + } + return c +} + +type mockClient struct { + errOnTracking bool + errOnActivity bool + closeCalled bool +} + +func (m mockClient) Tracking() (*client.TrackingPayload, error) { + if m.errOnTracking { + return nil, errors.New("mockClient.Tracking call error") + } + tp := client.TrackingPayload{ + RefID: 1540987708, + Ip: client.IPAddr{ + IPAddrHigh: 6618491809397997568, + IPAddrLow: 0, + Family: 1, + Pad: 0, + }, + Stratum: 3, + LeapStatus: 1, + RefTime: client.ChronyTimespec{ + TvSecHigh: 0, + TvSecLow: 1657633575, + TvNSec: 895532067, + }, + CurrentCorrection: -387363189, + LastOffset: -381315542, + RmsOffset: -323179191, + FreqPpm: 255056470, + ResidFreqPpm: -215937554, + SkewPpm: -58073545, + RootDelay: -86766599, + RootDispersion: -257753360, + LastUpdateInterval: 411159760, + } + return &tp, nil +} + +func (m mockClient) Activity() (*client.ActivityPayload, error) { + if m.errOnActivity { + return nil, errors.New("mockClient.Activity call error") + } + ap := client.ActivityPayload{ + Online: 8, + Offline: 2, + BurstOnline: 4, + BurstOffline: 3, + Unresolved: 1, + } + return &ap, nil +} + +func (m *mockClient) Close() { + m.closeCalled = true +} + +func copyRefMeasurementTime(dst, src map[string]int64) { + if _, ok := dst["ref_measurement_time"]; !ok { + return + } + if _, ok := src["ref_measurement_time"]; !ok { + return + } + dst["ref_measurement_time"] = src["ref_measurement_time"] +} diff --git a/modules/chrony/client/client.go b/modules/chrony/client/client.go new file mode 100644 index 000000000..2327bcc16 --- /dev/null +++ b/modules/chrony/client/client.go @@ -0,0 +1,173 @@ +package client + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "net" + "time" + + "github.com/netdata/go.d.plugin/logger" +) + +const ( + // protoVersionNumber is the protocol version for this client + protoVersionNumber = protoVersionNumber6 + protoVersionNumber6 = uint8(6) + protoVersionNumber5 = uint8(5) +) + +type Config struct { + Address string + Timeout time.Duration +} + +func New(l *logger.Logger, c Config) (*Client, error) { + conn, err := net.DialTimeout("udp", c.Address, c.Timeout) + if err != nil { + return nil, err + } + + client := &Client{ + Logger: l, + conn: conn, + timeout: c.Timeout, + } + client.chronyVersion = client.guessChronyVersion() + + return client, nil +} + +type Client struct { + *logger.Logger + conn net.Conn + timeout time.Duration + chronyVersion uint8 +} + +func (c *Client) Close() { + if c.conn != nil { + _ = c.conn.Close() + c.conn = nil + } +} + +func (c *Client) Tracking() (*TrackingPayload, error) { + req := &requestHead{ + Version: c.chronyVersion, + PktType: pktTypeCMDRequest, + Command: reqTracking, + } + + _, payload, err := c.query(req) + if err != nil { + return nil, err + } + + var tp TrackingPayload + if err := binary.Read(payload, binary.BigEndian, &tp); err != nil { + return nil, fmt.Errorf("failed reading tracking payload: %s", err) + } + + return &tp, nil +} + +func (c *Client) Activity() (*ActivityPayload, error) { + req := &requestHead{ + Version: c.chronyVersion, + PktType: pktTypeCMDRequest, + Command: reqActivity, + } + + _, payload, err := c.query(req) + if err != nil { + return nil, err + } + + var ap ActivityPayload + if err := binary.Read(payload, binary.BigEndian, &ap); err != nil { + return nil, fmt.Errorf("failed reading activity reply: %s", err) + } + + return &ap, nil +} + +func (c *Client) Ping() error { + req := &requestHead{ + Version: c.chronyVersion, + PktType: pktTypeCMDRequest, + } + _, _, err := c.query(req) + return err +} + +func (c *Client) guessChronyVersion() uint8 { + versions := []uint8{ + protoVersionNumber6, + protoVersionNumber5, + } + for _, ver := range versions { + req := &requestHead{ + Version: ver, + PktType: pktTypeCMDRequest, + } + rpy, _, err := c.query(req) + if err != nil { + c.Debugf("contact chrony failed with err: %+v", err) + continue + } + if ver == rpy.Version { + c.Debugf("chrony reply protocol version: %d", ver) + return ver + } + } + + c.Warningf("will use default chrony protocol version: %d", protoVersionNumber) + return protoVersionNumber +} + +func (c *Client) query(req *requestHead) (*replyHead, *bytes.Reader, error) { + if req.Version == 0 { + return nil, nil, errors.New("request version is not set") + } + + if req.SeqNumber == 0 { + req.SeqNumber = uint32(time.Now().Unix()) + } + + if err := c.conn.SetWriteDeadline(time.Now().Add(c.timeout)); err != nil { + return nil, nil, err + } + + if err := binary.Write(c.conn, binary.BigEndian, req); err != nil { + return nil, nil, fmt.Errorf("failed to write request: %v", err) + } + + if err := c.conn.SetReadDeadline(time.Now().Add(c.timeout)); err != nil { + return nil, nil, err + } + + dgram := make([]byte, 1024) + if _, err := c.conn.Read(dgram); err != nil { + return nil, nil, err + } + + payload := bytes.NewReader(dgram) + var rpy replyHead + if err := binary.Read(payload, binary.BigEndian, &rpy); err != nil { + return nil, nil, fmt.Errorf("failed to read rpy: %v", err) + } + + if rpy.PktType != pktTypeCMDReply { + return &rpy, payload, fmt.Errorf("unexpected packet type: want=%d, got=%d", pktTypeCMDReply, rpy.PktType) + } + if rpy.SeqNum != req.SeqNumber { + return &rpy, payload, fmt.Errorf("unexpected rpy seqNumber: want=%d, got=%d", req.SeqNumber, rpy.SeqNum) + } + if rpy.Version != req.Version { + return &rpy, payload, fmt.Errorf("unexpected rpy protocol version: want=%d, got=%d", req.Version, rpy.Version) + } + + return &rpy, payload, nil +} diff --git a/modules/chrony/client/protocol.go b/modules/chrony/client/protocol.go new file mode 100644 index 000000000..01c564677 --- /dev/null +++ b/modules/chrony/client/protocol.go @@ -0,0 +1,204 @@ +package client + +import ( + "fmt" + "math" + "net" + "strings" + "time" +) + +const ( + // // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L375-L376 + // pktTypeCMDRequest is the request packet type + pktTypeCMDRequest = uint8(1) + // pktTypeCMDReply is the reply packet type + pktTypeCMDReply = uint8(2) +) + +const ( + // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L39 + // reqTracking identifies a tracking request (REQ_TRACKING) + reqTracking = uint16(33) + // reqActivity identifies an activity check request (REQ_ACTIVITY) + reqActivity = uint16(44) +) + +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L431 +// requestHead represents CMD_Request +type requestHead struct { + Version uint8 + PktType uint8 + Res1 uint8 + Res2 uint8 + Command uint16 + Attempt uint16 + SeqNumber uint32 + Pad [396]byte +} + +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L784 +// replyHead represents CMD_Reply +type replyHead struct { + Version uint8 + PktType uint8 + Res1 uint8 + Res2 uint8 + Command uint16 + Reply uint16 + Status uint16 + Pad1 uint16 + Pad2 uint16 + Pad3 uint16 + SeqNum uint32 + Pad4 uint32 + Pad5 uint32 +} + +// TrackingPayload is the payload for tracking replies (RPY_Tracking) +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L581 +type TrackingPayload struct { + RefID uint32 + Ip IPAddr + Stratum uint16 + LeapStatus uint16 + RefTime ChronyTimespec + CurrentCorrection ChronyFloat + LastOffset ChronyFloat + RmsOffset ChronyFloat + FreqPpm ChronyFloat + ResidFreqPpm ChronyFloat + SkewPpm ChronyFloat + RootDelay ChronyFloat + RootDispersion ChronyFloat + LastUpdateInterval ChronyFloat +} + +func (tp *TrackingPayload) String() string { + var b strings.Builder + b.WriteString("\n") + b.WriteString(fmt.Sprintf("RefID: %d\n", tp.RefID)) + b.WriteString(fmt.Sprintf("ActiveServer: %s\n", tp.Ip)) + b.WriteString(fmt.Sprintf("Stratum: %d\n", tp.Stratum)) + b.WriteString(fmt.Sprintf("RefTime: %s\n", tp.RefTime.Time().Format(time.RFC3339))) + b.WriteString(fmt.Sprintf("CurrentCorrection: %f\n", tp.CurrentCorrection.Float64())) + b.WriteString(fmt.Sprintf("FreqPpm: %f\n", tp.FreqPpm.Float64())) + b.WriteString(fmt.Sprintf("ResidFreqPpm: %f\n", tp.ResidFreqPpm.Float64())) + b.WriteString(fmt.Sprintf("SkewPpm: %f\n", tp.SkewPpm.Float64())) + b.WriteString(fmt.Sprintf("RootDelay: %f\n", tp.RootDelay.Float64())) + b.WriteString(fmt.Sprintf("RootDispersion: %f\n", tp.RootDispersion.Float64())) + b.WriteString(fmt.Sprintf("LeapStatus: %d\n", tp.LeapStatus)) + b.WriteString(fmt.Sprintf("LastUpdateInterval: %f\n", tp.LastUpdateInterval.Float64())) + b.WriteString(fmt.Sprintf("LastOffset: %f\n", tp.LastOffset.Float64())) + b.WriteString(fmt.Sprintf("RmsOffset: %f", tp.RmsOffset.Float64())) + return b.String() +} + +// ActivityPayload is the payload for activity replies (RPY_Activity) +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L685 +type ActivityPayload struct { + Online int32 + Offline int32 + BurstOnline int32 + BurstOffline int32 + Unresolved int32 +} + +// ChronyTimespec is the custom chrony timespec type (Timespec) +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/candm.h#L115 +type ChronyTimespec struct { + TvSecHigh uint32 + TvSecLow uint32 + TvNSec uint32 +} + +func (ct ChronyTimespec) Time() time.Time { + nsec := uint32(999999999) + if ct.TvNSec < nsec { + nsec = ct.TvNSec + } + return time.Unix(int64(uint64(ct.TvSecHigh)<<32+uint64(ct.TvSecLow)), int64(nsec)) +} + +const ( + // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/util.c#L891 + // floatExpBits represents 32-bit floating-point format consisting of 7-bit signed exponent + floatExpBits = 7 + + // floatCoefBits represents chronyFloat 25-bit signed coefficient without hidden bit + floatCoefBits = 25 + + ScaleFactor = 1000000000 +) + +// ChronyFloat is 32-bit floating-point format consisting of 7-bit signed exponent +// and 25-bit signed coefficient without hidden bit. +// The result is calculated as: 2^(exp - 25) * coef. +type ChronyFloat int32 + +// Float64 does magic to decode float from int32. +// https://github.com/mlichvar/chrony/blob/2ac22477563581ae3bc39c4ff28464059c0a73be/util.c#L900 +func (cf ChronyFloat) Float64() float64 { + var exp, coef int32 + + x := uint32(cf) + + exp = int32(x >> floatCoefBits) + if exp >= 1<<(floatExpBits-1) { + exp -= 1 << floatExpBits + } + exp -= floatCoefBits + + coef = int32(x % (1 << floatCoefBits)) + if coef >= 1<<(floatCoefBits-1) { + coef -= 1 << floatCoefBits + } + + return float64(coef) * math.Pow(2.0, float64(exp)) +} + +// Int64 returns the 64bits float value +func (cf ChronyFloat) Int64() int64 { return int64(cf.Float64() * ScaleFactor) } + +// IPAddr represents IPAddr structure. +// https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/addressing.h#L41 +type IPAddr struct { + IPAddrHigh uint64 + IPAddrLow uint64 + Family uint16 + Pad uint16 +} + +func (ia IPAddr) String() string { return ia.IP().String() } + +func (ia IPAddr) IP() net.IP { + const ipAddrInet4 = uint16(1) + const ipAddrInet6 = uint16(2) + + if ia.Family == ipAddrInet4 { + m := uint32(ia.IPAddrHigh >> (32)) + var ip [4]uint8 + for i := 0; i < 4; i++ { + ip[i] = uint8(m % 0x100) + m = m / 0x100 + } + return net.IPv4(ip[3], ip[2], ip[1], ip[0]) + } + + if ia.Family == ipAddrInet6 { + addr := make(net.IP, net.IPv6len) + h := ia.IPAddrHigh + for i := 7; i >= 0; i-- { + addr[i] = byte(h % 0x100) + h = h / 0x100 + } + l := ia.IPAddrLow + for i := 7; i >= 0; i-- { + addr[i+8] = byte(l % 0x100) + l = l / 0x100 + } + return addr + } + + return net.IPv4zero +} diff --git a/modules/chrony/collect.go b/modules/chrony/collect.go new file mode 100644 index 000000000..b349d4fd8 --- /dev/null +++ b/modules/chrony/collect.go @@ -0,0 +1,95 @@ +package chrony + +import ( + "fmt" + "time" +) + +func (c *Chrony) collect() (map[string]int64, error) { + if c.client == nil { + client, err := c.newClient(c) + if err != nil { + return nil, err + } + c.client = client + } + + mx := make(map[string]int64) + + if err := c.collectTracking(mx); err != nil { + return nil, err + } + if err := c.collectActivity(mx); err != nil { + return mx, err + } + + return mx, nil +} + +const ( + // https://github.com/mlichvar/chrony/blob/7daf34675a5a2487895c74d1578241ca91a4eb70/ntp.h#L70-L75 + leapStatusNormal = 0 + leapStatusInsertSecond = 1 + leapStatusDeleteSecond = 2 + leapStatusUnsynchronised = 3 +) + +func (c *Chrony) collectTracking(mx map[string]int64) error { + // https://github.com/mlichvar/chrony/blob/5b04f3ca902e5d10aa5948fb7587d30b43941049/client.c#L2129 + tp, err := c.client.Tracking() + if err != nil { + return fmt.Errorf("error on collecting tracking: %v", err) + } + + mx["stratum"] = int64(tp.Stratum) + mx["leap_status_normal"] = boolToInt(tp.LeapStatus == leapStatusNormal) + mx["leap_status_insert_second"] = boolToInt(tp.LeapStatus == leapStatusInsertSecond) + mx["leap_status_delete_second"] = boolToInt(tp.LeapStatus == leapStatusDeleteSecond) + mx["leap_status_unsynchronised"] = boolToInt(tp.LeapStatus == leapStatusUnsynchronised) + mx["root_delay"] = tp.RootDelay.Int64() + mx["root_dispersion"] = tp.RootDispersion.Int64() + mx["skew"] = tp.SkewPpm.Int64() + mx["last_offset"] = tp.LastOffset.Int64() + mx["rms_offset"] = tp.RmsOffset.Int64() + mx["update_interval"] = tp.LastUpdateInterval.Int64() + // handle chrony restarts + if tp.RefTime.Time().Year() != 1970 { + mx["ref_measurement_time"] = time.Now().Unix() - tp.RefTime.Time().Unix() + } + mx["residual_frequency"] = tp.ResidFreqPpm.Int64() + // https://github.com/mlichvar/chrony/blob/5b04f3ca902e5d10aa5948fb7587d30b43941049/client.c#L1706 + mx["current_correction"] = abs(tp.CurrentCorrection.Int64()) + mx["frequency"] = abs(tp.FreqPpm.Int64()) + + return nil +} + +func (c *Chrony) collectActivity(mx map[string]int64) error { + // https://github.com/mlichvar/chrony/blob/5b04f3ca902e5d10aa5948fb7587d30b43941049/client.c#L2791 + ap, err := c.client.Activity() + if err != nil { + return fmt.Errorf("error on collecting activity: %v", err) + } + + mx["online_sources"] = int64(ap.Online) + mx["offline_sources"] = int64(ap.Offline) + mx["burst_online_sources"] = int64(ap.BurstOnline) + mx["burst_offline_sources"] = int64(ap.BurstOffline) + mx["unresolved_sources"] = int64(ap.Unresolved) + + return nil +} + +func boolToInt(v bool) int64 { + if v { + return 1 + } + return 0 +} + +func abs(v int64) int64 { + if v < 0 { + return -v + } + return v +} diff --git a/modules/chrony/init.go b/modules/chrony/init.go new file mode 100644 index 000000000..f80fa1319 --- /dev/null +++ b/modules/chrony/init.go @@ -0,0 +1,12 @@ +package chrony + +import ( + "errors" +) + +func (c Chrony) validateConfig() error { + if c.Address == "" { + return errors.New("empty 'address'") + } + return nil +} diff --git a/modules/chrony/types.go b/modules/chrony/types.go deleted file mode 100644 index cd40b891f..000000000 --- a/modules/chrony/types.go +++ /dev/null @@ -1,210 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -package chrony - -import ( - "fmt" - "math" - "net" - "time" -) - -const ( - // protoVersionNumber is the protocol version for this client - protoVersionNumber = protoVersionNumber6 - protoVersionNumber6 = uint8(6) - protoVersionNumber5 = uint8(5) - - // pktTypeCMDRequest is the request packet type - pktTypeCMDRequest = uint8(1) - // pktTypeCMDReply is the reply packet type - pktTypeCMDReply = uint8(2) - - // reqTracking identifies a tracking request - reqTracking = uint16(33) - // reqActivity identifies an activity check request - reqActivity = uint16(44) - - // floatExpBits represent 32-bit floating-point format consisting of 7-bit signed exponent - floatExpBits = 7 - // floatCoefBits represent chronyFloat 25-bit signed coefficient without hidden bit - floatCoefBits = 25 - - scaleFactor = 1000000000 -) - -// RequestPacket holds a chrony request -type requestPacket struct { - Version uint8 /* Protocol version */ - PktType uint8 /* What sort of packet this is */ - Res1 uint8 - Res2 uint8 - Command uint16 /* Which command is being issued */ - Attempt uint16 /* How many resends the client has done (count up from zero for same sequence number) */ - SeqNumber uint32 /* Client's sequence number */ - Pad [396]byte -} - -// TrackingPayload is the payload for tracking replies (`RPY_Tracking`) -type trackingPayload struct { - RefID uint32 - Ip ipAddr - Stratum uint16 - LeapStatus uint16 - RefTime chronyTimespec - CurrentCorrection chronyFloat - LastOffset chronyFloat - RmsOffset chronyFloat - FreqPpm chronyFloat - ResidFreqPpm chronyFloat - SkewPpm chronyFloat - RootDelay chronyFloat - RootDispersion chronyFloat - LastUpdateInterval chronyFloat -} - -const ( - IpaddrInet4 = uint16(1) - IpaddrInet6 = uint16(2) -) - -type ipAddr struct { - IPAddrHigh uint64 - IPAddrLow uint64 - Family uint16 - Pad uint16 -} - -func (tracking *trackingPayload) String() string { - return fmt.Sprintf( - "RefID: %d, ActivictServer: %s, Stratum: %d, RefTime: %s, CurrentCorrection: %f, "+ - "FreqPpm: %f, SkewPpm: %f, RootDelay: %f, "+ - "RootDispersion: %f, LeapStatus: %d, LastUpdateInterval: %f, "+ - "LastOffset: %f, CurrentCorrection: %f", - tracking.RefID, tracking.Ip.String(), tracking.Stratum, tracking.RefTime.Time().Format(time.RFC3339), - tracking.CurrentCorrection.Float64(), tracking.FreqPpm.Float64(), tracking.SkewPpm.Float64(), - tracking.RootDelay.Float64(), tracking.RootDispersion.Float64(), tracking.LeapStatus, - tracking.LastUpdateInterval.Float64(), tracking.LastOffset.Float64(), tracking.CurrentCorrection.Float64(), - ) -} - -func (ia ipAddr) Ip() net.IP { - if ia.Family == IpaddrInet4 { - m := uint32(ia.IPAddrHigh >> (32)) - var ip [4]uint8 - for i := 0; i < 4; i++ { - ip[i] = uint8(m % 0x100) - m = m / 0x100 - } - return net.IPv4(ip[3], ip[2], ip[1], ip[0]) - } - - if ia.Family == IpaddrInet6 { - res := make(net.IP, net.IPv6len) - h := ia.IPAddrHigh - for i := 7; i >= 0; i-- { - res[i] = byte(h % 0x100) - h = h / 0x100 - } - l := ia.IPAddrLow - for i := 7; i >= 0; i-- { - res[i+8] = byte(l % 0x100) - l = l / 0x100 - } - - return res - } - - return net.IPv4zero -} - -func (ia ipAddr) String() string { - return ia.Ip().String() -} - -// ActivityPayload is the payload for activity replies (`RPY_Activity`) -type activityPayload struct { - Online int32 - Offline int32 - BurstOnline int32 - BurstOffline int32 - Unresolved int32 -} - -func (activity *activityPayload) String() string { - return fmt.Sprintf("Online: %d, Offline: %d, BurstOnline: %d, BurstOffline: %d, Unresolved: %d", - activity.Online, activity.Offline, activity.BurstOnline, activity.BurstOffline, activity.Unresolved, - ) -} - -// replyPacket is the common header for all replies -// chrony version 4.1 -type replyPacket struct { - Version uint8 - PktType uint8 - Res1 uint8 - Res2 uint8 - Command uint16 - Reply uint16 - Status uint16 - Pad1 uint16 - Pad2 uint16 - Pad3 uint16 - SeqNum uint32 - Pad4 uint32 - Pad5 uint32 -} - -// chronyTimespec is the custom chrony timespec type (`Timespec`) -type chronyTimespec struct { - TvSecHigh uint32 - TvSecLow uint32 - TvNSec uint32 -} - -func (ct chronyTimespec) Time() time.Time { - var nsec = uint32(999999999) - if ct.TvNSec < nsec { - nsec = ct.TvNSec - } - - return time.Unix(int64(uint64(ct.TvSecHigh)<<32+uint64(ct.TvSecLow)), int64(nsec)) -} - -// EpochSeconds returns the number of seconds since epoch -func (ct chronyTimespec) EpochSeconds() float64 { - ts := uint64(ct.TvSecHigh) << 32 - ts += uint64(ct.TvSecLow) - return float64(ts) -} - -/* 32-bit floating-point format consisting of 7-bit signed exponent - and 25-bit signed coefficient without hidden bit. - The result is calculated as: 2^(exp - 25) * coef */ -type chronyFloat int32 - -// Float64 does magic to decode float from int32. -// Code is copied and translated to Go from original C sources. -func (cf chronyFloat) Float64() float64 { - var exp, coef int32 - - x := uint32(cf) - - exp = int32(x >> floatCoefBits) - if exp >= 1<<(floatExpBits-1) { - exp -= 1 << floatExpBits - } - exp -= floatCoefBits - - coef = int32(x % (1 << floatCoefBits)) - if coef >= 1<<(floatCoefBits-1) { - coef -= 1 << floatCoefBits - } - - return float64(coef) * math.Pow(2.0, float64(exp)) -} - -// Int64 returns the 64bits float value -func (cf chronyFloat) Int64() int64 { - return int64(cf.Float64() * scaleFactor) -}