cmd/ntpcheck/cmd/utils.go (284 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 cmd
import (
"crypto/md5"
"encoding/binary"
"fmt"
"io/ioutil"
"math"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/facebook/time/leaphash"
"github.com/facebook/time/leapsectz"
ntp "github.com/facebook/time/ntp/protocol"
"github.com/facebook/time/timestamp"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/sys/unix"
)
// refID converts ip into ReFID format and prints it on stdout
func refID(ipStr string) error {
if ipStr == "" {
return fmt.Errorf("Error: no IP provided")
}
ip := net.ParseIP(ipStr)
if ip == nil {
return fmt.Errorf("%q is not a valid IP address", ipStr)
}
// output IPv4 as-is
ipv4 := ip.To4()
if ipv4 != nil {
fmt.Println(ipv4)
return nil
}
hashed := md5.Sum(ip)
fmt.Println(net.IPv4(hashed[0], hashed[1], hashed[2], hashed[3]).String())
return nil
}
// fakeSeconds displays N fake leap seconds on stdout
func fakeSeconds(secondsCount int) {
ntpEpoch := time.Date(1900, time.January, 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
firstOfThisMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, time.UTC)
// the timestamp in this format is number of seconds from NTP Epoch
fmt.Printf("# Generating %d fake leap seconds:\n", secondsCount)
for i := 0; i < secondsCount; i++ {
firstOfFakeMonth := firstOfThisMonth.AddDate(0, i+1, 0)
delta := int((firstOfFakeMonth.Sub(ntpEpoch)).Seconds())
fmt.Printf("%d XX # %s\n", delta, firstOfFakeMonth.Format(time.RFC3339))
}
}
// signFile generates hash for leap Second hashfile and prints is on stdout
func signFile(fileName string) {
data, err := ioutil.ReadFile(fileName)
if err != nil {
fmt.Printf("Error opening %q: %s\n", fileName, err)
}
fmt.Printf("#h %s\n", leaphash.Compute(string(data)))
}
func stripZeroes(num float64) string {
s := fmt.Sprintf("%.2f", num)
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
}
// ntpDate prints data similar to 'ntptime' command output
func ntpDate(remoteServerAddr string, remoteServerPort string, requests int) error {
timeout := 5 * time.Second
addr := net.JoinHostPort(remoteServerAddr, remoteServerPort)
conn, err := net.DialTimeout("udp", addr, timeout)
if err != nil {
return fmt.Errorf("failed to connect to %s: %w", addr, err)
}
defer conn.Close()
// get connection file descriptor
connFd, err := timestamp.ConnFd(conn.(*net.UDPConn))
if err != nil {
return err
}
// Allow reading of kernel timestamps via socket
if err := timestamp.EnableSWTimestampsRx(connFd); err != nil {
return err
}
err = unix.SetNonblock(connFd, false)
if err != nil {
return err
}
var sumDelay int64
var sumOffset int64
for i := 0; i < requests; i++ {
clientTransmitTime := time.Now()
sec, frac := ntp.Time(clientTransmitTime)
clientWireTransmitTime := ntp.Unix(sec, frac)
log.Debugf("Client TX timestamp (ntp): %v\n", clientWireTransmitTime)
request := &ntp.Packet{
Settings: 0x1B,
TxTimeSec: sec,
TxTimeFrac: frac,
}
if err := binary.Write(conn, binary.BigEndian, request); err != nil {
return fmt.Errorf("failed to send request, %w", err)
}
var response *ntp.Packet
var clientReceiveTime time.Time
var buf []byte
blockingRead := make(chan bool, 1)
go func() {
// This calls unix.Recvmsg which has no timeout
buf, _, clientReceiveTime, err = timestamp.ReadPacketWithRXTimestamp(connFd)
if err != nil {
blockingRead <- true
return
}
response, err = ntp.BytesToPacket(buf)
blockingRead <- true
}()
select {
case <-blockingRead:
if err != nil {
return err
}
case <-time.After(timeout):
return fmt.Errorf("timeout waiting for reply from server for %v", timeout)
}
serverReceiveTime := ntp.Unix(response.RxTimeSec, response.RxTimeFrac)
serverTransmitTime := ntp.Unix(response.TxTimeSec, response.TxTimeFrac)
originTime := ntp.Unix(response.OrigTimeSec, response.OrigTimeFrac)
log.Debugf("Origin TX timestamp (T1): %v", originTime)
log.Debugf("Server RX timestamp (T2): %v", serverReceiveTime)
log.Debugf("Server TX timestamp (T3): %v", serverTransmitTime)
log.Debugf("Client RX timestamp (T4): %v", clientReceiveTime)
// sanity check: origin time must be same as client transmit time
if response.OrigTimeSec != sec || response.OrigTimeFrac != frac {
log.Errorf("Client TX timestamp %v not equal to Origin TX timestamp %v", clientTransmitTime, originTime)
}
delay := ntp.RoundTripDelay(originTime, serverReceiveTime, serverTransmitTime, clientReceiveTime)
offset := ntp.Offset(originTime, serverReceiveTime, serverTransmitTime, clientReceiveTime)
correctTime := ntp.CorrectTime(clientReceiveTime, offset)
sumDelay += delay
sumOffset += offset
if i == requests-1 {
fmt.Printf("\nServer: %s, Stratum: %d, Requests %d\n", addr, response.Stratum, requests)
fmt.Printf("Last Request:\n")
fmt.Printf("Offset: %fs (%sus) | Delay: %fs (%sus)\n",
float64(offset)/float64(time.Second.Nanoseconds()),
stripZeroes(math.Round(float64(offset)/float64(time.Microsecond.Nanoseconds()))),
float64(delay)/float64(time.Second.Nanoseconds()),
stripZeroes(math.Round(float64(delay)/float64(time.Microsecond.Nanoseconds()))))
fmt.Printf("Correct Time is %s\n\n", correctTime)
}
}
avgDelay := float64(sumDelay) / float64(requests)
avgOffset := float64(sumOffset) / float64(requests)
fmt.Printf("Average (%d requests):\n", requests)
fmt.Printf("Offset: %fs (%sus) | Delay: %fs (%sus)\n",
avgOffset/float64(time.Second.Nanoseconds()),
stripZeroes(math.Round(avgOffset/float64(time.Microsecond.Nanoseconds()))),
avgDelay/float64(time.Second.Nanoseconds()),
stripZeroes(math.Round(avgDelay/float64(time.Microsecond.Nanoseconds()))))
return nil
}
// printLeap prints leap second information from the system timezone database
func printLeap(srcfile string) error {
ls, err := leapsectz.Parse(srcfile)
if err != nil {
return err
}
for _, l := range ls {
fmt.Println(l.Time().UTC())
}
return nil
}
// addFakeSecondZoneInfo reads current zoneinfo db leap seconds and add one in the end of month or year
func addFakeSecondZoneInfo(srcfile, dstfile string, offsetMonth int) error {
ls, err := leapsectz.Parse(srcfile)
if err != nil {
return err
}
ntpEpoch := time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
now := time.Now()
fakeLeap := time.Date(now.Year(), now.Month()+time.Month(offsetMonth), 1, 0, 0, 0, 0, time.UTC)
fmt.Printf("Fake second added on %s\n", fakeLeap.Format(time.RFC3339))
ls = append(ls, leapsectz.LeapSecond{
Tleap: uint64(fakeLeap.Sub(ntpEpoch).Seconds()) + uint64(len(ls)),
Nleap: int32(len(ls) + 1),
})
o, err := os.Create(dstfile)
if err != nil {
return err
}
defer o.Close()
if err := leapsectz.Write(o, '2', ls, ""); err != nil {
return err
}
return nil
}
// cli vars
var refidIP string
var fsCount int
var signFileName string
var remoteServerAddr string
var remoteServerPort int
var ntpdateRequests int
var sourceLeapSeconds string
var destLeapSeconds string
var offsetMonth int
func init() {
RootCmd.AddCommand(utilsCmd)
// refid
utilsCmd.AddCommand(refidCmd)
refidCmd.Flags().StringVarP(&refidIP, "ip", "i", "", "IP address")
// fakeseconds
utilsCmd.AddCommand(fakeSecondsCmd)
fakeSecondsCmd.Flags().IntVarP(&fsCount, "count", "c", 5, "Number of entities to generate")
// signfile
utilsCmd.AddCommand(signFileCmd)
signFileCmd.Flags().StringVarP(&signFileName, "file", "f", "leap-seconds.list", "File name")
// ntpdate
utilsCmd.AddCommand(ntpdateCmd)
ntpdateCmd.Flags().StringVarP(&remoteServerAddr, "server", "s", "", "Server to query")
ntpdateCmd.Flags().IntVarP(&remoteServerPort, "port", "p", 123, "Port of the remote server")
ntpdateCmd.Flags().IntVarP(&ntpdateRequests, "requests", "r", 3, "How many requests to send")
// printleap
utilsCmd.AddCommand(printLeapCmd)
printLeapCmd.Flags().StringVarP(&sourceLeapSeconds, "srcfile", "s", "/usr/share/zoneinfo/right/UTC", "Source file of leap seconds")
// addFakeSecondZoneInfo
utilsCmd.AddCommand(addFakeSecondZoneInfoCmd)
addFakeSecondZoneInfoCmd.Flags().IntVarP(&offsetMonth, "month", "m", 1, "How many monthes to add to current to insert leap second")
addFakeSecondZoneInfoCmd.Flags().StringVarP(&sourceLeapSeconds, "srcfile", "s", "/usr/share/zoneinfo/right/UTC", "Source file of leap seconds")
addFakeSecondZoneInfoCmd.Flags().StringVarP(&destLeapSeconds, "dstfile", "d", "/usr/share/zoneinfo/right/Fake", "Destination file for fake leap seconds")
}
var utilsCmd = &cobra.Command{
Use: "utils",
Short: "Collection of NTP-related utils",
}
var refidCmd = &cobra.Command{
Use: "refid",
Short: "Converts IP address to refid",
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
err := refID(refidIP)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
var fakeSecondsCmd = &cobra.Command{
Use: "fakeseconds",
Short: "Prints some fake seconds.",
Long: `Prints some fake seconds (potential slots when leap seconds might happen in leap-seconds format. For reference: https://www.ietf.org/timezones/data/leap-seconds.list`,
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
fakeSeconds(fsCount)
},
}
var signFileCmd = &cobra.Command{
Use: "signfile",
Short: "Generate hash signature for leap-seconds.list",
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
signFile(signFileName)
},
}
var ntpdateCmd = &cobra.Command{
Use: "ntpdate",
Short: "Sends NTP request(s) to a remote NTP server. Similar to ntpdate -q",
Long: "'ntpdate' will poll remote NTP server and will report metrics including local clock offset and roundtrip delay based on response from server",
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
if remoteServerAddr == "" {
fmt.Println("server must be specified")
os.Exit(1)
}
if err := ntpDate(remoteServerAddr, strconv.Itoa(remoteServerPort), ntpdateRequests); err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
var printLeapCmd = &cobra.Command{
Use: "printleap",
Short: "Prints leap second information from the system timezone database",
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
if err := printLeap(sourceLeapSeconds); err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}
var addFakeSecondZoneInfoCmd = &cobra.Command{
Use: "addfakesecond",
Short: "Adds fake second to zoneinfo database",
Long: `'addfakesecond' will read current zoneinfo leap second file from srcfile, add fake second
in the end of the month (plus offset provided by -m) and write new file to dstfile`,
Run: func(cmd *cobra.Command, args []string) {
ConfigureVerbosity()
if err := addFakeSecondZoneInfo(sourceLeapSeconds, destLeapSeconds, offsetMonth); err != nil {
fmt.Println(err)
os.Exit(1)
}
},
}