feat(inputs.chrony): Remove chronyc dependency (#14629)

This commit is contained in:
Sven Rebhan 2024-02-01 20:15:28 +01:00 committed by GitHub
parent 43fd26a7a0
commit 120167501b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 489 additions and 233 deletions

View File

@ -128,6 +128,7 @@ following works:
- github.com/eclipse/paho.golang [Eclipse Public License - v 2.0](https://github.com/eclipse/paho.golang/blob/master/LICENSE)
- github.com/eclipse/paho.mqtt.golang [Eclipse Public License - v 2.0](https://github.com/eclipse/paho.mqtt.golang/blob/master/LICENSE)
- github.com/emicklei/go-restful [MIT License](https://github.com/emicklei/go-restful/blob/v3/LICENSE)
- github.com/facebook/time [Apache License 2.0](https://github.com/facebook/time/blob/main/LICENSE)
- github.com/fatih/color [MIT License](https://github.com/fatih/color/blob/master/LICENSE.md)
- github.com/felixge/httpsnoop [MIT License](https://github.com/felixge/httpsnoop/blob/master/LICENSE.txt)
- github.com/form3tech-oss/jwt-go [MIT License](https://github.com/form3tech-oss/jwt-go/blob/master/LICENSE)

3
go.mod
View File

@ -62,7 +62,7 @@ require (
github.com/compose-spec/compose-go v1.20.2
github.com/coocood/freecache v1.2.3
github.com/coreos/go-semver v0.3.1
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
github.com/coreos/go-systemd/v22 v22.5.0
github.com/couchbase/go-couchbase v0.1.1
github.com/datadope-io/go-zabbix/v2 v2.0.1
@ -74,6 +74,7 @@ require (
github.com/dynatrace-oss/dynatrace-metric-utils-go v0.5.0
github.com/eclipse/paho.golang v0.11.0
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e
github.com/fatih/color v1.16.0
github.com/go-ldap/ldap/v3 v3.4.6
github.com/go-logfmt/logfmt v0.6.0

5
go.sum
View File

@ -1010,8 +1010,9 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
@ -1108,6 +1109,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e h1:PaVm1gMon1pkJdbzoyw2VUCn07cA6RDbGdTbsppjcY8=
github.com/facebook/time v0.0.0-20240125155343-557f84f4ad3e/go.mod h1:NBW7VY75BPCsOPrSsARAengTlgOICicHey4Bv70lCUI=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/stackerr v0.0.0-20150612192056-c2fcf88613f4 h1:fP04zlkPjAGpsduG7xN3rRkxjAqkJaIQnnkNYYw/pAk=

View File

@ -1,63 +1,9 @@
# chrony Input Plugin
Get standard chrony metrics, requires chronyc executable.
This plugin queries metrics from a chrony NTP server. For details on the
meaning of the gathered fields please check the [chronyc manual][]
Below is the documentation of the various headers returned by `chronyc
tracking`.
- Reference ID - This is the refid and name (or IP address) if available, of the
server to which the computer is currently synchronised. If this is 127.127.1.1
it means the computer is not synchronised to any external source and that you
have the local mode operating (via the local command in chronyc (see section
local), or the local directive in the /etc/chrony.conf file (see section
local)).
- Stratum - The stratum indicates how many hops away from a computer with an
attached reference clock we are. Such a computer is a stratum-1 computer, so
the computer in the example is two hops away (i.e. a.b.c is a stratum-2 and is
synchronised from a stratum-1).
- Ref time - This is the time (UTC) at which the last measurement from the
reference source was processed.
- System time - In normal operation, chronyd never steps the system clock,
because any jump in the timescale can have adverse consequences for certain
application programs. Instead, any error in the system clock is corrected by
slightly speeding up or slowing down the system clock until the error has been
removed, and then returning to the system clocks normal speed. A consequence
of this is that there will be a period when the system clock (as read by other
programs using the gettimeofday() system call, or by the date command in the
shell) will be different from chronyd's estimate of the current true time
(which it reports to NTP clients when it is operating in server mode). The
value reported on this line is the difference due to this effect.
- Last offset - This is the estimated local offset on the last clock update.
- RMS offset - This is a long-term average of the offset value.
- Frequency - The frequency is the rate by which the systems clock would be
wrong if chronyd was not correcting it. It is expressed in ppm (parts per
million). For example, a value of 1ppm would mean that when the systems
clock thinks it has advanced 1 second, it has actually advanced by 1.000001
seconds relative to true time.
- Residual freq - This shows the residual frequency for the currently selected
reference source. This reflects any difference between what the measurements
from the reference source indicate the frequency should be and the frequency
currently being used. The reason this is not always zero is that a smoothing
procedure is applied to the frequency. Each time a measurement from the
reference source is obtained and a new residual frequency computed, the
estimated accuracy of this residual is compared with the estimated accuracy
(see skew next) of the existing frequency value. A weighted average is
computed for the new frequency, with weights depending on these accuracies. If
the measurements from the reference source follow a consistent trend, the
residual will be driven to zero over time.
- Skew - This is the estimated error bound on the frequency.
- Root delay - This is the total of the network path delays to the stratum-1
computer from which the computer is ultimately synchronised. In certain
extreme situations, this value can be negative. (This can arise in a symmetric
peer arrangement where the computers frequencies are not tracking each other
and the network delay is very short relative to the turn-around time at each
computer.)
- Root dispersion - This is the total dispersion accumulated through all the
computers back to the stratum-1 computer from which the computer is ultimately
synchronised. Dispersion is due to system clock resolution, statistical
measurement variations etc.
- Leap status - This is the leap status, which can be Normal, Insert second,
Delete second or Not synchronised.
[chronyc manual]: https://chrony-project.org/doc/4.4/chronyc.html
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
@ -73,7 +19,17 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
```toml @sample.conf
# Get standard chrony metrics, requires chronyc executable.
[[inputs.chrony]]
## If true, chronyc tries to perform a DNS lookup for the time server.
## Server address of chronyd with address scheme
## If empty or not set, the plugin will mimic the behavior of chronyc and
## check "unix:///run/chrony/chronyd.sock", "udp://127.0.0.1:323"
## and "udp://[::1]:323".
# server = ""
## Timeout for establishing the connection
# timeout = "5s"
## Try to resolve received addresses to host-names via DNS lookups
## Disabled by default to avoid DNS queries especially for slow DNS servers.
# dns_lookup = false
```
@ -100,5 +56,5 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Example Output
```text
chrony,leap_status=normal,reference_id=192.168.1.1,stratum=3 frequency=-35.657,system_time=0.000027073,last_offset=-0.000013616,residual_freq=-0,rms_offset=0.000027073,root_delay=0.000644,root_dispersion=0.003444,skew=0.001,update_interval=1031.2 1463750789687639161
chrony,leap_status=not\ synchronized,reference_id=A29FC87B,stratum=3 frequency=-16.000999450683594,last_offset=0.000012651000361074694,residual_freq=0,rms_offset=0.000025576999178156257,root_delay=0.0016550000291317701,root_dispersion=0.00330700003542006,skew=0.006000000052154064,system_time=0.000020389999917824753,update_interval=507.1999816894531 1706271167571675297
```

View File

@ -5,26 +5,30 @@ import (
_ "embed"
"errors"
"fmt"
"os/exec"
"net"
"net/url"
"strconv"
"strings"
"time"
fbchrony "github.com/facebook/time/ntp/chrony"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample.conf
var sampleConfig string
var (
execCommand = exec.Command // execCommand is used to mock commands in tests.
)
type Chrony struct {
DNSLookup bool `toml:"dns_lookup"`
path string
Server string `toml:"server"`
Timeout config.Duration `toml:"timeout"`
DNSLookup bool `toml:"dns_lookup"`
Log telegraf.Logger `toml:"-"`
conn net.Conn
client *fbchrony.Client
}
func (*Chrony) SampleConfig() string {
@ -32,98 +36,134 @@ func (*Chrony) SampleConfig() string {
}
func (c *Chrony) Init() error {
var err error
c.path, err = exec.LookPath("chronyc")
if err != nil {
return errors.New("chronyc not found: verify that chrony is installed and that chronyc is in your PATH")
if c.Server != "" {
// Check the specified server address
u, err := url.Parse(c.Server)
if err != nil {
return fmt.Errorf("parsing server address failed: %w", err)
}
switch u.Scheme {
case "unix":
// Keep the server unmodified
case "udp":
// Check if we do have a port and add the default port if we don't
if u.Port() == "" {
u.Host += ":323"
}
// We cannot have path elements in an UDP address
if u.Path != "" {
return fmt.Errorf("path detected in UDP address %q", c.Server)
}
u = &url.URL{Scheme: "udp", Host: u.Host}
default:
return errors.New("unknown or missing address scheme")
}
c.Server = u.String()
}
return nil
}
func (c *Chrony) Start(_ telegraf.Accumulator) error {
if c.Server != "" {
// Create a connection
u, err := url.Parse(c.Server)
if err != nil {
return fmt.Errorf("parsing server address failed: %w", err)
}
switch u.Scheme {
case "unix":
conn, err := net.DialTimeout("unix", u.Path, time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing %q failed: %w", c.Server, err)
}
c.conn = conn
case "udp":
conn, err := net.DialTimeout("udp", u.Host, time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing %q failed: %w", c.Server, err)
}
c.conn = conn
}
} else {
// If no server is given, reproduce chronyc's behavior
if conn, err := net.DialTimeout("unix", "/run/chrony/chronyd.sock", time.Duration(c.Timeout)); err == nil {
c.Server = "unix:///run/chrony/chronyd.sock"
c.conn = conn
} else if conn, err := net.DialTimeout("udp", "127.0.0.1:323", time.Duration(c.Timeout)); err == nil {
c.Server = "udp://127.0.0.1:323"
c.conn = conn
} else {
conn, err := net.DialTimeout("udp", "[::1]:323", time.Duration(c.Timeout))
if err != nil {
return fmt.Errorf("dialing server failed: %w", err)
}
c.Server = "udp://[::1]:323"
c.conn = conn
}
}
c.Log.Debugf("Connected to %q...", c.Server)
// Initialize the client
c.client = &fbchrony.Client{Connection: c.conn}
return nil
}
func (c *Chrony) Stop() {
if c.conn != nil {
if err := c.conn.Close(); err != nil {
c.Log.Errorf("Closing connection to %q failed: %v", c.Server, err)
}
}
}
func (c *Chrony) Gather(acc telegraf.Accumulator) error {
flags := []string{}
if !c.DNSLookup {
flags = append(flags, "-n")
req := fbchrony.NewTrackingPacket()
resp, err := c.client.Communicate(req)
if err != nil {
return fmt.Errorf("querying tracking data failed: %w", err)
}
tracking, ok := resp.(*fbchrony.ReplyTracking)
if !ok {
return fmt.Errorf("got unexpected response type %T while waiting for tracking data", resp)
}
flags = append(flags, "tracking")
cmd := execCommand(c.path, flags...)
out, err := internal.CombinedOutputTimeout(cmd, time.Second*5)
if err != nil {
return fmt.Errorf("failed to run command %q: %w - %s", strings.Join(cmd.Args, " "), err, string(out))
// according to https://github.com/mlichvar/chrony/blob/e11b518a1ffa704986fb1f1835c425844ba248ef/ntp.h#L70
var leapStatus string
switch tracking.LeapStatus {
case 0:
leapStatus = "normal"
case 1:
leapStatus = "insert second"
case 2:
leapStatus = "delete second"
case 3:
leapStatus = "not synchronized"
}
fields, tags, err := processChronycOutput(string(out))
if err != nil {
return err
tags := map[string]string{
"leap_status": leapStatus,
"reference_id": strings.ToUpper(strconv.FormatUint(uint64(tracking.RefID), 16)),
"stratum": strconv.FormatUint(uint64(tracking.Stratum), 10),
}
fields := map[string]interface{}{
"frequency": tracking.FreqPPM,
"system_time": tracking.CurrentCorrection,
"last_offset": tracking.LastOffset,
"residual_freq": tracking.ResidFreqPPM,
"rms_offset": tracking.RMSOffset,
"root_delay": tracking.RootDelay,
"root_dispersion": tracking.RootDispersion,
"skew": tracking.SkewPPM,
"update_interval": tracking.LastUpdateInterval,
}
acc.AddFields("chrony", fields, tags)
return nil
}
// processChronycOutput takes in a string output from the chronyc command, like:
//
// Reference ID : 192.168.1.22 (ntp.example.com)
// Stratum : 3
// Ref time (UTC) : Thu May 12 14:27:07 2016
// System time : 0.000020390 seconds fast of NTP time
// Last offset : +0.000012651 seconds
// RMS offset : 0.000025577 seconds
// Frequency : 16.001 ppm slow
// Residual freq : -0.000 ppm
// Skew : 0.006 ppm
// Root delay : 0.001655 seconds
// Root dispersion : 0.003307 seconds
// Update interval : 507.2 seconds
// Leap status : Normal
//
// The value on the left side of the colon is used as field name, if the first field on
// the right side is a float. If it cannot be parsed as float, it is a tag name.
//
// Ref time is ignored and all names are converted to snake case.
//
// It returns (<fields>, <tags>)
func processChronycOutput(out string) (map[string]interface{}, map[string]string, error) {
tags := map[string]string{}
fields := map[string]interface{}{}
lines := strings.Split(strings.TrimSpace(out), "\n")
for _, line := range lines {
stats := strings.Split(line, ":")
if len(stats) < 2 {
return nil, nil, fmt.Errorf("unexpected output from chronyc, expected ':' in %s", out)
}
name := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(stats[0]), " ", "_"))
// ignore reference time
if strings.Contains(name, "ref_time") {
continue
}
valueFields := strings.Fields(stats[1])
if len(valueFields) == 0 {
return nil, nil, fmt.Errorf("unexpected output from chronyc: %s", out)
}
if strings.Contains(strings.ToLower(name), "stratum") {
tags["stratum"] = valueFields[0]
continue
}
if strings.Contains(strings.ToLower(name), "reference_id") {
tags["reference_id"] = valueFields[0]
continue
}
value, err := strconv.ParseFloat(valueFields[0], 64)
if err != nil {
tags[name] = strings.ToLower(strings.Join(valueFields, " "))
continue
}
if strings.Contains(stats[1], "slow") {
value = -value
}
fields[name] = value
}
return fields, tags, nil
}
func init() {
inputs.Add("chrony", func() telegraf.Input {
return &Chrony{}
return &Chrony{Timeout: config.Duration(3 * time.Second)}
})
}

View File

@ -1,112 +1,329 @@
package chrony
import (
"bytes"
"encoding/binary"
"fmt"
"os"
"os/exec"
"math"
"net"
"testing"
"time"
fbchrony "github.com/facebook/time/ntp/chrony"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/wait"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
func TestGather(t *testing.T) {
c := Chrony{
path: "chronyc",
// Setup a mock server
server := Server{
TrackingInfo: &fbchrony.Tracking{
RefID: 0xA29FC87B,
IPAddr: net.ParseIP("192.168.1.22"),
Stratum: 3,
LeapStatus: 3,
RefTime: time.Now(),
CurrentCorrection: 0.000020390,
LastOffset: 0.000012651,
RMSOffset: 0.000025577,
FreqPPM: -16.001,
ResidFreqPPM: 0.0,
SkewPPM: 0.006,
RootDelay: 0.001655,
RootDispersion: 0.003307,
LastUpdateInterval: 507.2,
},
}
// overwriting exec commands with mock commands
execCommand = fakeExecCommand
defer func() { execCommand = exec.Command }()
addr, err := server.Listen(t)
require.NoError(t, err)
defer server.Shutdown()
// Setup the plugin
plugin := &Chrony{
Server: "udp://" + addr,
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Start the plugin, do a gather and stop everything
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
require.NoError(t, plugin.Gather(&acc))
plugin.Stop()
server.Shutdown()
err := c.Gather(&acc)
if err != nil {
t.Fatal(err)
// Do the comparison
expected := []telegraf.Metric{
metric.New(
"chrony",
map[string]string{
"reference_id": "A29FC87B",
"leap_status": "not synchronized",
"stratum": "3",
},
map[string]interface{}{
"system_time": 0.000020390,
"last_offset": 0.000012651,
"rms_offset": 0.000025577,
"frequency": -16.001,
"residual_freq": 0.0,
"skew": 0.006,
"root_delay": 0.001655,
"root_dispersion": 0.003307,
"update_interval": 507.2,
},
time.Unix(0, 0),
),
}
tags := map[string]string{
"reference_id": "192.168.1.22",
"leap_status": "not synchronized",
"stratum": "3",
}
fields := map[string]interface{}{
"system_time": 0.000020390,
"last_offset": 0.000012651,
"rms_offset": 0.000025577,
"frequency": -16.001,
"residual_freq": 0.0,
"skew": 0.006,
"root_delay": 0.001655,
"root_dispersion": 0.003307,
"update_interval": 507.2,
options := []cmp.Option{
// tests on linux with go1.20 will add a warning about code coverage, ignore that tag
testutil.IgnoreTags("warning"),
testutil.IgnoreTime(),
cmpopts.EquateApprox(0.001, 0),
}
// tests on linux with go1.20 will add a warning about code coverage
// due to the code coverage dir not being set
delete(acc.Metrics[0].Tags, "warning")
acc.AssertContainsTaggedFields(t, "chrony", fields, tags)
// test with dns lookup
c.DNSLookup = true
err = c.Gather(&acc)
if err != nil {
t.Fatal(err)
}
acc.AssertContainsTaggedFields(t, "chrony", fields, tags)
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, options...)
}
// fakeExecCommand is a helper function that mock
// the exec.Command call (and call the test binary)
func fakeExecCommand(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
func TestIntegration(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Start the docker container
container := testutil.Container{
Image: "dockurr/chrony",
ExposedPorts: []string{"323/udp"},
Files: map[string]string{
"/etc/telegraf-chrony.conf": "testdata/chrony.conf",
"/start.sh": "testdata/start.sh",
},
Entrypoint: []string{"/start.sh"},
WaitingFor: wait.ForLog("Selected source"),
}
require.NoError(t, container.Start(), "failed to start container")
defer container.Terminate()
// Setup the plugin
plugin := &Chrony{
Server: "udp://" + container.Address + ":" + container.Ports["323"],
Log: testutil.Logger{},
}
require.NoError(t, plugin.Init())
// Collect the metrics and compare
var acc testutil.Accumulator
require.NoError(t, plugin.Start(&acc))
defer plugin.Stop()
require.NoError(t, plugin.Gather(&acc))
// Setup the expectations
expected := []telegraf.Metric{
metric.New(
"chrony",
map[string]string{
"leap_status": "normal",
"reference_id": "A29FC87B",
"stratum": "4",
},
map[string]interface{}{
"frequency": float64(0),
"last_offset": float64(0),
"residual_freq": float64(0),
"rms_offset": float64(0),
"root_delay": float64(0),
"root_dispersion": float64(0),
"skew": float64(0),
"system_time": float64(0),
"update_interval": float64(0),
},
time.Unix(0, 0),
),
}
options := []cmp.Option{
testutil.IgnoreTags("leap_status", "reference_id", "stratum"),
testutil.IgnoreTime(),
}
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsStructureEqual(t, expected, actual, options...)
}
// TestHelperProcess isn't a real test. It's used to mock exec.Command
// For example, if you run:
// GO_WANT_HELPER_PROCESS=1 go test -test.run=TestHelperProcess -- chrony tracking
// it returns below mockData.
func TestHelperProcess(_ *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
type Server struct {
TrackingInfo *fbchrony.Tracking
conn net.PacketConn
}
func (s *Server) Shutdown() {
if s.conn != nil {
s.conn.Close()
}
}
lookup := "Reference ID : 192.168.1.22 (ntp.example.com)\n"
noLookup := "Reference ID : 192.168.1.22 (192.168.1.22)\n"
mockData := `Stratum : 3
Ref time (UTC) : Thu May 12 14:27:07 2016
System time : 0.000020390 seconds fast of NTP time
Last offset : +0.000012651 seconds
RMS offset : 0.000025577 seconds
Frequency : 16.001 ppm slow
Residual freq : -0.000 ppm
Skew : 0.006 ppm
Root delay : 0.001655 seconds
Root dispersion : 0.003307 seconds
Update interval : 507.2 seconds
Leap status : Not synchronized
`
args := os.Args
// Previous arguments are tests stuff, that looks like :
// /tmp/go-build970079519/…/_test/integration.test -test.run=TestHelperProcess --
cmd, args := args[3], args[4:]
if cmd != "chronyc" {
fmt.Fprint(os.Stdout, "command not found")
//nolint:revive // error code is important for this "test"
os.Exit(1)
func (s *Server) Listen(t *testing.T) (string, error) {
conn, err := net.ListenPacket("udp", "127.0.0.1:0")
if err != nil {
return "", err
}
if args[0] == "tracking" {
fmt.Fprint(os.Stdout, lookup+mockData)
s.conn = conn
addr := s.conn.LocalAddr().String()
go s.serve(t)
return addr, nil
}
func (s *Server) serve(t *testing.T) {
defer s.conn.Close()
for {
buf := make([]byte, 4096)
n, addr, err := s.conn.ReadFrom(buf)
if err != nil {
return
}
t.Logf("mock server: received %d bytes from %q\n", n, addr.String())
var header fbchrony.RequestHead
data := bytes.NewBuffer(buf)
if err := binary.Read(data, binary.BigEndian, &header); err != nil {
t.Logf("mock server: reading request header failed: %v", err)
return
}
seqno := header.Sequence + 1
switch header.Command {
case 33: // tracking
_, err := s.conn.WriteTo(s.encodeTrackingReply(seqno), addr)
if err != nil {
t.Logf("mock server [tracking]: writing reply failed: %v", err)
} else {
t.Log("mock server [tracking]: successfully wrote reply")
}
default:
t.Logf("mock server: unhandled command %v", header.Command)
}
}
}
func (s *Server) encodeTrackingReply(sequence uint32) []byte {
t := s.TrackingInfo
// Encode the header
buf := []byte{
0x06, // version 6
0x02, // packet type 2: tracking
0x00, // res1
0x00, // res2
0x00, 0x21, // command 33: tracking request
0x00, 0x05, // reply 5: tracking reply
0x00, 0x00, // status 0: success
0x00, 0x00, // pad1
0x00, 0x00, // pad2
0x00, 0x00, // pad3
}
buf = binary.BigEndian.AppendUint32(buf, sequence) // sequence number
buf = append(buf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) // pad 4 & 5
// Encode data
buf = binary.BigEndian.AppendUint32(buf, t.RefID)
buf = append(buf, t.IPAddr.To16()...)
if len(t.IPAddr) == 4 {
buf = append(buf, 0x00, 0x01) // IPv4 address family
} else {
fmt.Fprint(os.Stdout, noLookup+mockData)
buf = append(buf, 0x00, 0x02) // IPv6 address family
}
buf = append(buf, 0x00, 0x00) // padding
buf = binary.BigEndian.AppendUint16(buf, t.Stratum)
buf = binary.BigEndian.AppendUint16(buf, t.LeapStatus)
sec := uint64(t.RefTime.Unix())
nsec := uint32(t.RefTime.UnixNano() % t.RefTime.Unix() * int64(time.Second))
buf = binary.BigEndian.AppendUint32(buf, uint32(sec>>32)) // seconds high part
buf = binary.BigEndian.AppendUint32(buf, uint32(sec&0xffffffff)) // seconds low part
buf = binary.BigEndian.AppendUint32(buf, nsec) // nanoseconds
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.CurrentCorrection))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.LastOffset))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RMSOffset))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.FreqPPM))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.ResidFreqPPM))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.SkewPPM))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RootDelay))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.RootDispersion))
buf = binary.BigEndian.AppendUint32(buf, encodeFloat(t.LastUpdateInterval))
return buf
}
// Modified based on https://github.com/mlichvar/chrony/blob/master/util.c
const (
floatExpBits = int32(7)
floatCoeffBits = int32(25) // 32 - floatExpBits
floatExpMin = int32(-(1 << (floatExpBits - 1)))
floatExpMax = -floatExpMin - 1
floatCoefMin = int32(-(1 << (floatCoeffBits - 1)))
floatCoefMax = -floatCoefMin - 1
)
func encodeFloat(x float64) uint32 {
var neg int32
if math.IsNaN(x) {
/* Save NaN as zero */
x = 0.0
} else if x < 0.0 {
x = -x
neg = 1
}
//nolint:revive // error code is important for this "test"
os.Exit(0)
var exp, coef int32
if x > 1.0e100 {
exp = floatExpMax
coef = floatCoefMax + neg
} else if x > 1.0e-100 {
exp = int32(math.Log2(x)) + 1
coef = int32(x*math.Pow(2.0, float64(-exp+floatCoeffBits)) + 0.5)
if coef <= 0 {
panic(fmt.Errorf("invalid coefficient %v for value %f", coef, x))
}
/* we may need to shift up to two bits down */
for coef > floatCoefMax+neg {
coef >>= 1
exp++
}
if exp > floatExpMax {
/* overflow */
exp = floatExpMax
coef = floatCoefMax + neg
} else if exp < floatExpMin {
/* underflow */
if exp+floatCoeffBits >= floatExpMin {
coef >>= floatExpMin - exp
exp = floatExpMin
} else {
exp = 0
coef = 0
}
}
}
/* negate back */
if neg != 0 {
coef = int32(uint32(-coef) % (1 << floatCoeffBits))
}
return uint32(exp<<floatCoeffBits) | uint32(coef)
}

View File

@ -1,4 +1,14 @@
# Get standard chrony metrics, requires chronyc executable.
[[inputs.chrony]]
## If true, chronyc tries to perform a DNS lookup for the time server.
## Server address of chronyd with address scheme
## If empty or not set, the plugin will mimic the behavior of chronyc and
## check "unix:///run/chrony/chronyd.sock", "udp://127.0.0.1:323"
## and "udp://[::1]:323".
# server = ""
## Timeout for establishing the connection
# timeout = "5s"
## Try to resolve received addresses to host-names via DNS lookups
## Disabled by default to avoid DNS queries especially for slow DNS servers.
# dns_lookup = false

View File

@ -0,0 +1,11 @@
server 0.pool.ntp.org iburst
server 1.pool.ntp.org iburst
server 2.pool.ntp.org iburst
server 3.pool.ntp.org iburst
driftfile /var/lib/chrony/chrony.drift
makestep 0.1 3
bindcmdaddress 0.0.0.0
cmdallow all
allow all

17
plugins/inputs/chrony/testdata/start.sh vendored Normal file
View File

@ -0,0 +1,17 @@
#!/bin/sh
# confirm correct permissions on chrony run directory
if [ -d /run/chrony ]; then
chown -R chrony:chrony /run/chrony
chmod o-rx /run/chrony
# remove previous pid file if it exist
rm -f /var/run/chrony/chronyd.pid
fi
# confirm correct permissions on chrony variable state directory
if [ -d /var/lib/chrony ]; then
chown -R chrony:chrony /var/lib/chrony
fi
## startup chronyd in the foreground
exec /usr/sbin/chronyd -u chrony -d -x -f /etc/telegraf-chrony.conf