Skip to content

Commit

Permalink
Merge 9ff8cfe into 25c1372
Browse files Browse the repository at this point in the history
  • Loading branch information
susisu committed Mar 12, 2019
2 parents 25c1372 + 9ff8cfe commit a715621
Show file tree
Hide file tree
Showing 3 changed files with 202 additions and 50 deletions.
11 changes: 6 additions & 5 deletions check-ntpoffset/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ command = ["check-ntpoffset", "-w", "50", "-c", "100"]
### Options

```
-c, --critical= Critical threshold of ntp offset(ms) (default: 100)
-w, --warning= Warning threshold of ntp offset(ms) (default: 50)
-s, --ntp-servers= Use specified NTP Servers(plural servers can be set separated by ,). When set plural servers, use first
response. If not set, use local command just like ntpd/chronyd.
-t, --ntp-timeout= Timeout of NTP Server Querying(in seconds). (default: 15)
-c, --critical= Critical threshold of ntp offset(ms) (default: 100)
-w, --warning= Warning threshold of ntp offset(ms) (default: 50)
-s, --ntp-servers= Use specified NTP Servers(plural servers can be set separated by ,). When set plural servers, use first
response. If not set, use local command just like ntpd/chronyd.
-t, --ntp-timeout= Timeout of NTP Server Querying(in seconds). (default: 15)
-S, --check-stratum Check stratum and fail if the machine is not synchronized.
```


Expand Down
90 changes: 68 additions & 22 deletions check-ntpoffset/lib/check-ntpoffset.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import (
)

var opts struct {
Crit float64 `short:"c" long:"critical" default:"100" description:"Critical threshold of ntp offset(ms)"`
Warn float64 `short:"w" long:"warning" default:"50" description:"Warning threshold of ntp offset(ms)"`
NTPServers string `short:"s" long:"ntp-servers" default:"" description:"Use specified NTP Servers(plural servers can be set separated by ,). When set plural servers, use first response. If not set, use local command just like ntpd/chronyd."`
NTPTimeout int `short:"t" long:"ntp-timeout" default:"15" description:"Timeout of NTP Server Querying(in seconds)."`
Crit float64 `short:"c" long:"critical" default:"100" description:"Critical threshold of ntp offset(ms)"`
Warn float64 `short:"w" long:"warning" default:"50" description:"Warning threshold of ntp offset(ms)"`
NTPServers string `short:"s" long:"ntp-servers" default:"" description:"Use specified NTP Servers(plural servers can be set separated by ,). When set plural servers, use first response. If not set, use local command just like ntpd/chronyd."`
NTPTimeout int `short:"t" long:"ntp-timeout" default:"15" description:"Timeout of NTP Server Querying(in seconds)."`
CheckStratum bool `short:"S" long:"check-stratum" description:"Check stratum and fail if the machine is not synchronized."`
}

var ntpTimeout int
Expand All @@ -44,7 +45,7 @@ func run(args []string) *checkers.Checker {
}
ntpTimeout = opts.NTPTimeout

offset, err := getNTPOffset(opts.NTPServers)
offset, err := getNTPOffset(opts.NTPServers, opts.CheckStratum)
if err != nil {
return checkers.Unknown(err.Error())
}
Expand Down Expand Up @@ -114,7 +115,7 @@ func detectNTPDname() (ntpdName string, err error) {
return ntpdName, err
}

func getNTPOffset(ntpServers string) (float64, error) {
func getNTPOffset(ntpServers string, checkStratum bool) (float64, error) {
if ntpServers != "" {
return getNTPOffsetFromNTPServers(ntpServers)
}
Expand All @@ -125,9 +126,9 @@ func getNTPOffset(ntpServers string) (float64, error) {
}
switch ntpdName {
case ntpNTPD:
return getNTPOffsetFromNTPD()
return getNTPOffsetFromNTPD(checkStratum)
case ntpChronyd:
return getNTPOffsetFromChrony()
return getNTPOffsetFromChrony(checkStratum)
}
return 0.0, fmt.Errorf("unsupported ntp daemon %q", ntpdName)
}
Expand Down Expand Up @@ -160,49 +161,94 @@ func getNTPOffsetFromNTPServers(ntpServers string) (offset float64, err error) {
}
}

func getNTPOffsetFromNTPD() (offset float64, err error) {
err = withCmd(exec.Command(cmdNTPq, "-c", "rv 0 offset"), func(out io.Reader) error {
offset, err = parseNTPOffsetFromNTPD(out)
func getNTPOffsetFromNTPD(checkStratum bool) (offset float64, err error) {
err = withCmd(exec.Command(cmdNTPq, "-c", "rv 0 stratum,offset"), func(out io.Reader) error {
offset, err = parseNTPOffsetFromNTPD(out, checkStratum)
return err
})
return offset, err
}

func parseNTPOffsetFromNTPD(out io.Reader) (float64, error) {
func parseNTPOffsetFromNTPD(out io.Reader, checkStratum bool) (float64, error) {
scr := bufio.NewScanner(out)
const stratumPrefix = "stratum="
const offsetPrefix = "offset="
var offset *float64
for scr.Scan() {
line := scr.Text()
if strings.HasPrefix(line, offsetPrefix) {
return strconv.ParseFloat(strings.TrimPrefix(line, offsetPrefix), 64)
for _, column := range strings.Split(line, ",") {
column = strings.TrimPrefix(column, " ")
if checkStratum && strings.HasPrefix(column, stratumPrefix) {
stratum, err := strconv.ParseInt(strings.TrimPrefix(column, stratumPrefix), 10, 64)
if err != nil {
return 0.0, err
}
// stratum == 16 means that the machine is unsynchronized.
// ref. https://support.ntp.org/bin/view/Support/TroubleshootingNTP
if stratum == 16 {
return 0.0, fmt.Errorf("not synchronized to stratum")
}
}
if offset == nil && strings.HasPrefix(column, offsetPrefix) {
offsetMillis, err := strconv.ParseFloat(strings.TrimPrefix(column, offsetPrefix), 64)
if err != nil {
return 0.0, err
}
offset = &offsetMillis
}
}
}
return 0.0, fmt.Errorf("couldn't get ntp offset. ntpd process may be down")
if offset == nil {
return 0.0, fmt.Errorf("failed to get ntp offset")
}
return *offset, nil
}

func getNTPOffsetFromChrony() (offset float64, err error) {
func getNTPOffsetFromChrony(checkStratum bool) (offset float64, err error) {
err = withCmd(exec.Command(cmdChronyc, "tracking"), func(out io.Reader) error {
offset, err = parseNTPOffsetFromChrony(out)
offset, err = parseNTPOffsetFromChrony(out, checkStratum)
return err
})
return offset, err
}

func parseNTPOffsetFromChrony(out io.Reader) (offset float64, err error) {
func parseNTPOffsetFromChrony(out io.Reader, checkStratum bool) (float64, error) {
scr := bufio.NewScanner(out)
const stratumPrefix = "Stratum"
const offsetPrefix = "Last offset"
var offset *float64
for scr.Scan() {
line := scr.Text()
if strings.HasPrefix(line, "Last offset") {
if checkStratum && strings.HasPrefix(line, stratumPrefix) {
flds := strings.Fields(line)
if len(flds) != 3 {
return 0.0, fmt.Errorf("failed to get ntp stratum")
}
stratum, err := strconv.ParseInt(flds[2], 10, 64)
if err != nil {
return 0.0, err
}
// stratum == 0 means that the machine is unsynchronized.
// Actually this can be the best case, but that would be rare...
if stratum == 0 {
return 0.0, fmt.Errorf("not synchronized to stratum")
}
}
if offset == nil && strings.HasPrefix(line, offsetPrefix) {
flds := strings.Fields(line)
if len(flds) != 5 {
return 0.0, fmt.Errorf("failed to get ntp offset")
}
offset, err = strconv.ParseFloat(flds[3], 64)
offsetSeconds, err := strconv.ParseFloat(flds[3], 64)
if err != nil {
return 0.0, err
}
return offset * 1000, nil
offsetMillis := offsetSeconds * 1000
offset = &offsetMillis
}
}
return 0.0, fmt.Errorf("failed to get ntp offset")
if offset == nil {
return 0.0, fmt.Errorf("failed to get ntp offset")
}
return *offset, nil
}
151 changes: 128 additions & 23 deletions check-ntpoffset/lib/check-ntpoffset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ import (
)

func TestParseNTPOffsetFromChrony(t *testing.T) {
offset, err := parseNTPOffsetFromChrony(strings.NewReader(
`Reference ID : 160.16.75.242 (sv01.azsx.net)
testCases := []struct {
name string
input string
checkStratum bool
expect float64
expectError string
}{
{
name: "normal (checkStratum = false)",
input: `Reference ID : 160.16.75.242 (sv01.azsx.net)
Stratum : 3
Ref time (UTC) : Thu May 4 11:51:30 2017
System time : 0.000033190 seconds slow of NTP time
Expand All @@ -20,44 +28,141 @@ Root delay : 0.003541 seconds
Root dispersion : 0.000849 seconds
Update interval : 1030.4 seconds
Leap status : Normal
`))
if err != nil {
t.Fatalf("error should be nil but got: %v", err)
`,
checkStratum: false,
expect: 0.003614,
},
{
name: "normal (checkStratum = true, synchronized)",
input: `Reference ID : 160.16.75.242 (sv01.azsx.net)
Stratum : 3
Ref time (UTC) : Thu May 4 11:51:30 2017
System time : 0.000033190 seconds slow of NTP time
Last offset : +0.000003614 seconds
RMS offset : 0.000017540 seconds
Frequency : 10.880 ppm fast
Residual freq : -0.000 ppm
Skew : 0.003 ppm
Root delay : 0.003541 seconds
Root dispersion : 0.000849 seconds
Update interval : 1030.4 seconds
Leap status : Normal
`,
checkStratum: true,
expect: 0.003614,
},
{
name: "normal (checkStratum = true, unsynchronized)",
input: `Reference ID : 00000000 ()
Stratum : 0
Ref time (UTC) : Thu Jan 01 00:00:00 1970
System time : 0.000000000 seconds fast of NTP time
Last offset : +0.000000000 seconds
RMS offset : 0.000000000 seconds
Frequency : 281.118 ppm slow
Residual freq : +0.000 ppm
Skew : 0.000 ppm
Root delay : 1.000000 seconds
Root dispersion : 1.000000 seconds
Update interval : 0.0 seconds
Leap status : Not synchronised
`,
checkStratum: true,
expectError: "not synchronized to stratum",
},
}
expect := 0.003614
if offset != expect {
t.Errorf("invalid offset: %f (expected: %f)", offset, expect)

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
offset, err := parseNTPOffsetFromChrony(strings.NewReader(tc.input), tc.checkStratum)
if tc.expectError != "" {
if err == nil {
t.Error("error should not be nil")
}
if err.Error() != tc.expectError {
t.Errorf("unexpected error: %s (expected: %s)", err.Error(), tc.expectError)
}
} else {
if err != nil {
t.Fatalf("error should be nil but got: %v", err)
}
if offset != tc.expect {
t.Errorf("invalid offset: %f (expected: %f)", offset, tc.expect)
}
}
})
}
}

func TestParseNTPOffsetFromNTPD(t *testing.T) {
testCases := []struct {
name string
input string
expect float64
name string
input string
checkStratum bool
expect float64
expectError string
}{
{
name: "normal",
input: "offset=0.504\n",
expect: 0.504,
name: "normal (checkStratum = false)",
input: "stratum=3, offset=0.504\n",
checkStratum: false,
expect: 0.504,
},
{
name: "normal (checkStratum = true, synchronized)",
input: "stratum=3, offset=0.504\n",
checkStratum: true,
expect: 0.504,
},
{
name: "normal (checkStratum = true, unsynchronized)",
input: "stratum=16, offset=0.000000\n",
checkStratum: true,
expectError: "not synchronized to stratum",
},
{
name: "ntp on 4.2.2p1-18.el5.centos (checkStratum = false)",
input: `assID=0 status=06f4 leap_none, sync_ntp, 15 events, event_peer/strat_chg,
stratum=3, offset=0.180
`,
checkStratum: false,
expect: 0.18,
},
{
name: "ntp on 4.2.2p1-18.el5.centos (checkStratum = true, synchronized)",
input: `assID=0 status=06f4 leap_none, sync_ntp, 15 events, event_peer/strat_chg,
stratum=3, offset=0.180
`,
checkStratum: true,
expect: 0.18,
},
{
name: "ntp on 4.2.2p1-18.el5.centos",
name: "ntp on 4.2.2p1-18.el5.centos (checkStratum = true, unsynchronized)",
input: `assID=0 status=06f4 leap_none, sync_ntp, 15 events, event_peer/strat_chg,
offset=0.180
stratum=16, offset=0.000
`,
expect: 0.18,
checkStratum: true,
expectError: "not synchronized to stratum",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
offset, err := parseNTPOffsetFromNTPD(strings.NewReader(tc.input))
if err != nil {
t.Fatalf("error should be nil but got: %v", err)
}
if offset != tc.expect {
t.Errorf("%s: invalid offset: %f (expected: %f)", tc.name, offset, tc.expect)
offset, err := parseNTPOffsetFromNTPD(strings.NewReader(tc.input), tc.checkStratum)
if tc.expectError != "" {
if err == nil {
t.Error("error should not be nil")
}
if err.Error() != tc.expectError {
t.Errorf("unexpected error: %s (expected: %s)", err.Error(), tc.expectError)
}
} else {
if err != nil {
t.Fatalf("error should be nil but got: %v", err)
}
if offset != tc.expect {
t.Errorf("invalid offset: %f (expected: %f)", offset, tc.expect)
}
}
})
}
Expand Down

0 comments on commit a715621

Please sign in to comment.