cmd/ntpcheck/checker/chrony.go (148 lines of code) (raw):

/* Copyright (c) Facebook, Inc. and its affiliates. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package checker import ( "fmt" "io" "net" "os" "path" "github.com/facebook/time/ntp/chrony" "github.com/facebook/time/ntp/control" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) type chronyConn struct { net.Conn local string } // dialUnix opens a unixgram connection with chrony func dialUnix(address string) (*chronyConn, error) { base, _ := path.Split(address) local := path.Join(base, fmt.Sprintf("chronyc.%d.sock", os.Getpid())) conn, err := net.DialUnix("unixgram", &net.UnixAddr{Name: local, Net: "unixgram"}, &net.UnixAddr{Name: address, Net: "unixgram"}, ) if err != nil { return nil, err } if err := os.Chmod(local, 0666); err != nil { return nil, err } return &chronyConn{Conn: conn, local: local}, nil } // Close closes the unixgram connection with chrony func (c *chronyConn) Close() error { if err := os.RemoveAll(c.local); err != nil { return err } if err := c.Conn.Close(); err != nil { return err } return nil } type chronyClient interface { Communicate(packet chrony.RequestPacket) (chrony.ResponsePacket, error) } // ChronyCheck gathers NTP stats using chronyc/chronyd protocol client type ChronyCheck struct { Client chronyClient } // chrony reports all float measures in seconds, while NTP and this tool operate ms func secToMS(seconds float64) float64 { return 1000 * seconds } // Run is the main method of ChronyCheck and it fetches all information to return NTPCheckResult. // Essentially we request tracking info, num of peers, and then request source_data and ntp_data // for each peer individually. func (n *ChronyCheck) Run() (*NTPCheckResult, error) { var packet chrony.ResponsePacket var err error result := NewNTPCheckResult() // tracking data trackReq := chrony.NewTrackingPacket() packet, err = n.Client.Communicate(trackReq) if err != nil { return nil, errors.Wrap(err, "failed to get 'tracking' response") } log.Debugf("Got 'tracking' response:") log.Debugf("Status: %v", packet.GetStatus()) tracking, ok := packet.(*chrony.ReplyTracking) if !ok { return nil, errors.Errorf("Got wrong 'tracking' response %+v", packet) } result.Correction = tracking.CurrentCorrection result.LIDesc = control.LeapDesc[uint8(tracking.LeapStatus)] result.LI = uint8(tracking.LeapStatus) result.Event = "clock_sync" // no real events for chrony result.SysVars = NewSystemVariablesFromChrony(tracking) // sources list sourcesReq := chrony.NewSourcesPacket() packet, err = n.Client.Communicate(sourcesReq) if err != nil { return nil, errors.Wrap(err, "failed to get 'sources' response") } sources, ok := packet.(*chrony.ReplySources) if !ok { return nil, errors.Errorf("Got wrong 'sources' response %+v", packet) } log.Debugf("Got %d sources", sources.NSources) // per-source data for i := 0; i < int(sources.NSources); i++ { log.Debugf("Fetching source #%d info", i) sourceDataReq := chrony.NewSourceDataPacket(int32(i)) packet, err = n.Client.Communicate(sourceDataReq) if err != nil { return nil, errors.Wrapf(err, "failed to get 'sourcedata' response for source #%d", i) } sourceData, ok := packet.(*chrony.ReplySourceData) if !ok { return nil, errors.Errorf("Got wrong 'sourcedata' response %+v", packet) } // get ntpdata when using a unix socket var ntpData *chrony.ReplyNTPData if sourceData.Mode != chrony.SourceModeRef && n.Unix() { ntpDataReq := chrony.NewNTPDataPacket(sourceData.IPAddr) packet, err = n.Client.Communicate(ntpDataReq) if err != nil { return nil, errors.Wrapf(err, "failed to get 'ntpdata' response for source #%d", i) } ntpData, ok = packet.(*chrony.ReplyNTPData) if !ok { return nil, errors.Errorf("Got wrong 'ntpdata' response %+v", packet) } } peer, err := NewPeerFromChrony(sourceData, ntpData) if err != nil { return nil, errors.Wrapf(err, "failed to create Peer structure from response packet for peer=%s", sourceData.IPAddr) } result.Peers[uint16(i)] = peer // if main sync source, update ClockSource info if sourceData.State == chrony.SourceStateSync { if sourceData.Mode == chrony.SourceModeRef { result.ClockSource = "local" } else { result.ClockSource = "ntp" } } } return result, nil } // ServerStats return server stats func (n *ChronyCheck) ServerStats() (*ServerStats, error) { statsReq := chrony.NewServerStatsPacket() packet, err := n.Client.Communicate(statsReq) if err != nil { return nil, errors.Wrap(err, "failed to get 'serverstats' response") } var serverStats *ServerStats switch stats := packet.(type) { case *chrony.ReplyServerStats: serverStats = NewServerStatsFromChrony(stats) case *chrony.ReplyServerStats2: serverStats = NewServerStatsFromChrony2(stats) default: return nil, errors.Errorf("Got wrong 'serverstats' response %+v", packet) } log.Debugf("ServerStats: %v", serverStats) return serverStats, nil } // Unix returns true if connected via a unix socket func (n *ChronyCheck) Unix() bool { // it could be a mock, so verify type assertion if client, ok := n.Client.(*chrony.Client); ok { if _, ok := client.Connection.(*chronyConn); ok { return true } } return false } // NewChronyCheck is a constructor for ChronyCheck func NewChronyCheck(conn io.ReadWriter) *ChronyCheck { return &ChronyCheck{ Client: &chrony.Client{Sequence: 1, Connection: conn}, } }