Merge lp:~paulgear/charms/trusty/ntp/fix-divide-by-zero into lp:charms/trusty/ntp

Proposed by Paul Gear
Status: Merged
Merged at revision: 20
Proposed branch: lp:~paulgear/charms/trusty/ntp/fix-divide-by-zero
Merge into: lp:charms/trusty/ntp
Diff against target: 1092 lines (+778/-102)
4 files modified
files/nagios/check_ntpmon.py (+282/-99)
hooks/ntp_hooks.py (+2/-1)
tests/00-setup (+2/-2)
unit_tests/test_check_ntpmon.py (+492/-0)
To merge this branch: bzr merge lp:~paulgear/charms/trusty/ntp/fix-divide-by-zero
Reviewer Review Type Date Requested Status
Tim Van Steenburgh (community) Approve
Adam Israel (community) Approve
Whit Morriss (community) Approve
Review via email: mp+255660@code.launchpad.net

Description of the change

I've fixed the upstream version of check_ntpmon.py; this commit merges the current head version from https://github.com/paulgear/check_ntpmon

To post a comment you must log in.
Revision history for this message
Whit Morriss (whitmo) wrote :

Thanks Paul!

LGTM +1. Merges cleanly, tests pass (demonstrating successful deployment).

My only gripe is that there are no unittests to cover this bug fix (or the python code in general). While this fix *looks* fine, having some test to prove it would be really nice!

-w

review: Approve
19. By Charles Butler

[r=lazypower] Charles Butler 2015-04-16 [lazypower] Ammended unit test to specify private-address
Brad Marshall 2015-04-01 [merge] [bradm] Fixed unit name on the test
Whit Morriss 2015-03-31 specify unit
Brad Marshall 2015-03-31 [bradm] Merge in nrpe fixes
Brad Marshall 2015-03-27 [bradm] Remove the total_source line
Brad Marshall 2015-03-27 [bradm] Tidied up extra unneeded lines.
Brad Marshall 2015-03-27 [bradm] Deploy ntp subordinate before adding relation to ntpmaster, c...
Brad Marshall 2015-03-27 [bradm] Apply the timeout to the sentry
Brad Marshall 2015-03-27 [bradm] Add missing type to nagios_servicegroups
Brad Marshall 2015-03-27 [bradm] Fixed nagios_servicegroup setting
Brad Marshall 2015-03-27 [bradm] Added default empty string to nagios_servicegroup, unit tests...
Brad Marshall 2015-03-25 [merge] [bradm] Fixed conflicts with upstream
Brad Marshall 2015-03-17 [bradm] Add docs on auto peers and peers config options.
Brad Marshall 2015-03-16 [bradm] Fixed use_iburst variable to work properly
Brad Marshall 2015-03-16 [bradm] Added use_iburst config variable, tidy up warning message
Brad Marshall 2015-03-16 [bradm] Fixed formatting on warning
Brad Marshall 2015-03-16 [bradm] Not using brackets inside strings
Brad Marshall 2015-03-16 [bradm] Add backslash for continued line
Brad Marshall 2015-03-16 [bradm] Added restrict line for new ntp sources to ntp.conf. Add peer...
Brad Marshall 2015-03-16 [bradm] Gave source a default value, made auto_peers a boolean
Brad Marshall 2015-03-16 [bradm] Add auto_peers config option
Brad Marshall 2015-03-16 [bradm] First cut of ntp peers relationship

Revision history for this message
Paul Gear (paulgear) wrote :

Hi Whit,

I have a reasonably comprehensive test suite for it upstream: https://github.com/paulgear/ntpmon/blob/master/test_check_ntpmon.py; where would be the most appropriate point to run it?

Revision history for this message
Whit Morriss (whitmo) wrote :

I think in this case what would be best is unit tests that exercise the
hook code (which seem to be generally missing, not your fault). The ntpmon
tests do look handy for exercising a deployment in general though.

-w

On Thu, Apr 16, 2015 at 6:14 PM, Paul Gear <email address hidden> wrote:

> Hi Whit,
>
> I have a reasonably comprehensive test suite for it upstream:
> https://github.com/paulgear/ntpmon/blob/master/test_check_ntpmon.py;
> where would be the most appropriate point to run it?
> --
>
> https://code.launchpad.net/~paulgear/charms/trusty/ntp/fix-divide-by-zero/+merge/255660
> You are reviewing the proposed merge of
> lp:~paulgear/charms/trusty/ntp/fix-divide-by-zero into lp:charms/trusty/ntp.
>

--
---------------
D. Whit Morriss
Developer, Juju Ecosystem
Canonical USA

Revision history for this message
Paul Gear (paulgear) wrote :

I've done a quick rebase of this code to use the latest version of trunk. Is there anything else needed before this can be merged? Should I be asking for it to be merged another branch instead of trunk?

20. By Paul Gear

Merge merge upstream check_ntpmon.py

21. By Paul Gear

Update to upstream check_ntpmon

Notable changes:
- detects the case when ntpd is not running correctly
- always returns OK until ntpd has been running for long enough to get 8 successful polls

22. By Paul Gear

Install new prerequisite for upstream check_ntpmon

Revision history for this message
Adam Israel (aisrael) wrote :

Hi Paul,

I had a chance to review your merge proposal today. The current tests pass cleanly. As far as your new test upstream, that could be integrated into the charm's tests/ directory; something like 20-test_check_ntpmon. Still, this looks good to me, especially as it fixes a bug. You have my +1.

Revision history for this message
Adam Israel (aisrael) :
review: Approve
Revision history for this message
Paul Gear (paulgear) wrote :

> Hi Paul,
>
> I had a chance to review your merge proposal today. The current tests pass
> cleanly. As far as your new test upstream, that could be integrated into the
> charm's tests/ directory; something like 20-test_check_ntpmon. Still, this
> looks good to me, especially as it fixes a bug. You have my +1.

Thanks Adam. My impression of the charm tests/ directory was that it was intended for deployment tests rather than unit tests. Is that not the case? If not, I can easily add the test there.

Regards,
Paul

23. By Paul Gear

Add check_ntpmon unit tests from https://github.com/paulgear/check_ntpmon/blob/master/test_check_ntpmon.py

24. By Paul Gear

Move unit tests to where they belong, add path to tested module; install software dependency

25. By Paul Gear

Fix comment

Revision history for this message
Paul Gear (paulgear) wrote :

I've added the unit tests for check_ntpmon to the unit_tests directory now.

26. By Paul Gear

Fix setup script

Revision history for this message
Tim Van Steenburgh (tvansteenburgh) wrote :

LGTM, thanks Paul!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'files/nagios/check_ntpmon.py'
2--- files/nagios/check_ntpmon.py 2015-03-23 00:39:09 +0000
3+++ files/nagios/check_ntpmon.py 2015-05-14 10:36:53 +0000
4@@ -20,28 +20,30 @@
5 #
6
7 import argparse
8+import psutil
9 import re
10 import subprocess
11 import sys
12+import time
13 import traceback
14 import warnings
15
16
17 def ishostnamey(name):
18 """Return true if the passed name is roughly hostnamey. NTP is rather casual about how it
19- reports hostnames and IP addresses, so we can't be too strict. This function simply tests
20+ reports hostnames and IP addresses, so we can't be too strict. This method simply tests
21 that all of the characters in the string are letters, digits, dash, or period."""
22 return re.search(r'^[\w.-]*$', name) is not None and name.find('_') == -1
23
24
25 def isipaddressy(name):
26 """Return true if the passed name is roughly IP addressy. NTP is rather casual about how it
27- reports hostnames and IP addresses, so we can't be too strict. This function simply tests
28+ reports hostnames and IP addresses, so we can't be too strict. This method simply tests
29 that all of the characters in the string are hexadecimal digits, period, or colon."""
30 return re.search(r'^[0-9a-f.:]*$', name) is not None
31
32
33-class CheckNTPMon(object):
34+class CheckNTPMonSilent(object):
35
36 def __init__(self,
37 warnpeers=2,
38@@ -50,7 +52,6 @@
39 critoffset=50,
40 warnreach=75,
41 critreach=50):
42-
43 self.warnpeers = warnpeers
44 self.okpeers = okpeers
45 self.warnoffset = warnoffset
46@@ -59,65 +60,130 @@
47 self.critreach = critreach
48
49 def peers(self, n):
50+ if n >= self.okpeers:
51+ return (0, "OK: %d usable peers" % (n))
52+ elif n < self.warnpeers:
53+ return (2, "CRITICAL: Too few peers (%d) - must be at least %d" %
54+ (n, self.warnpeers))
55+ else:
56+ return (1, "WARNING: Too few peers (%d) - should be at least %d" %
57+ (n, self.okpeers))
58+
59+ def offset(self, offset):
60+ if abs(offset) > self.critoffset:
61+ return (2,
62+ "CRITICAL: Offset too high (%g) - must be less than %g" %
63+ (offset, self.critoffset))
64+ if abs(offset) > self.warnoffset:
65+ return (1,
66+ "WARNING: Offset too high (%g) - should be less than %g" %
67+ (offset, self.warnoffset))
68+ else:
69+ return (0, "OK: Offset normal (%g)" % (offset))
70+
71+ def reachability(self, percent):
72+ if percent < 0 or percent > 100:
73+ raise ValueError('Value must be a percentage')
74+ if percent <= self.critreach:
75+ return (
76+ 2,
77+ "CRITICAL: Reachability too low (%g%%) - must be more than %g%%" %
78+ (percent, self.critreach))
79+ elif percent <= self.warnreach:
80+ return (
81+ 1,
82+ "WARNING: Reachability too low (%g%%) - should be more than %g%%" %
83+ (percent, self.warnreach))
84+ else:
85+ return (0, "OK: Reachability normal (%g%%)" % (percent))
86+
87+ def sync(self, synchost):
88+ synced = len(synchost) > 0 and (ishostnamey(synchost) or
89+ isipaddressy(synchost))
90+ if synced:
91+ return (0, "OK: time is in sync with %s" % (synchost))
92+ else:
93+ return (2, "CRITICAL: no sync host selected")
94+
95+ def is_silent(self):
96+ return True
97+
98+ def dump(self):
99+ print "warnpeers = %d" % (self.warnpeers)
100+ print "okpeers = %d" % (self.okpeers)
101+ print "warnoffset = %g" % (self.warnoffset)
102+ print "critoffset = %g" % (self.critoffset)
103+ print "warnreach = %g" % (self.warnreach)
104+ print "critreach = %g" % (self.critreach)
105+
106+ @classmethod
107+ def clone(cls, obj):
108+ # must actually be a CheckNTPMonSilent object
109+ assert obj.warnpeers is not None
110+ if obj.is_silent():
111+ return obj
112+ else:
113+ return CheckNTPMonSilent(warnpeers=obj.warnpeers,
114+ okpeers=obj.okpeers,
115+ warnoffset=obj.warnoffset,
116+ critoffset=obj.critoffset,
117+ warnreach=obj.warnreach,
118+ critreach=obj.critreach)
119+
120+
121+class CheckNTPMon(CheckNTPMonSilent):
122+
123+ """Version of CheckNTPMonSilent which prints out the diagnostic message and returns
124+ integer return code instead of a list"""
125+
126+ def __init__(self,
127+ warnpeers=2,
128+ okpeers=4,
129+ warnoffset=10,
130+ critoffset=50,
131+ warnreach=75,
132+ critreach=50):
133+ CheckNTPMonSilent.__init__(self, warnpeers, okpeers, warnoffset,
134+ critoffset, warnreach, critreach)
135+
136+ def peers(self, n):
137 """Return 0 if the number of peers is OK
138 Return 1 if the number of peers is WARNING
139 Return 2 if the number of peers is CRITICAL"""
140- if n >= self.okpeers:
141- print "OK: %d usable peers" % n
142- return 0
143- elif n < self.warnpeers:
144- print "CRITICAL: Too few peers (%d) - must be at least %d" % (n, self.warnpeers)
145- return 2
146- else:
147- print "WARNING: Too few peers (%d) - should be at least %d" % (n, self.okpeers)
148- return 1
149+ code, msg = CheckNTPMonSilent.peers(self, n)
150+ print msg
151+ return code
152
153 def offset(self, offset):
154 """Return 0 if the offset is OK
155 Return 1 if the offset is WARNING
156 Return 2 if the offset is CRITICAL"""
157- if abs(offset) > self.critoffset:
158- print "CRITICAL: Offset too high (%g) - must be less than %g" % \
159- (offset, self.critoffset)
160- return 2
161- if abs(offset) > self.warnoffset:
162- print "WARNING: Offset too high (%g) - should be less than %g" % \
163- (offset, self.warnoffset)
164- return 1
165- else:
166- print "OK: Offset normal (%g)" % (offset)
167- return 0
168+ code, msg = CheckNTPMonSilent.offset(self, offset)
169+ print msg
170+ return code
171
172 def reachability(self, percent):
173 """Return 0 if the reachability percentage is OK
174 Return 1 if the reachability percentage is warning
175 Return 2 if the reachability percentage is critical
176 Raise a ValueError if reachability is not a percentage"""
177- if percent < 0 or percent > 100:
178- raise ValueError('Value must be a percentage')
179- if percent <= self.critreach:
180- print "CRITICAL: Reachability too low (%g%%) - must be more than %g%%" % \
181- (percent, self.critreach)
182- return 2
183- elif percent <= self.warnreach:
184- print "WARNING: Reachability too low (%g%%) - should be more than %g%%" % \
185- (percent, self.warnreach)
186- return 1
187- else:
188- print "OK: Reachability normal (%g%%)" % (percent)
189- return 0
190+ code, msg = CheckNTPMonSilent.reachability(self, percent)
191+ print msg
192+ return code
193
194 def sync(self, synchost):
195- """Return 0 if the synchost is non-zero in length and is a roughly valid host identifier, return 2 otherwise."""
196- synced = len(synchost) > 0 and (ishostnamey(synchost) or isipaddressy(synchost))
197- if synced:
198- print "OK: time is in sync with %s" % (synchost)
199- else:
200- print "CRITICAL: no sync host selected"
201- return 0 if synced else 2
202+ """Return 0 if the synchost is non-zero in length and is a roughly valid host identifier
203+ Return 2 otherwise"""
204+ code, msg = CheckNTPMonSilent.sync(self, synchost)
205+ print msg
206+ return code
207+
208+ def is_silent(self):
209+ return False
210
211
212 class NTPPeers(object):
213+
214 """Turn the peer lines returned by 'ntpq -pn' into a data structure usable for checks."""
215
216 noiselines = [
217@@ -135,7 +201,8 @@
218
219 def shouldignore(self, fields, l):
220 if len(fields) != 10:
221- warnings.warn('Invalid ntpq peer line - there are %d fields: %s' % (len(fields), l))
222+ warnings.warn('Invalid ntpq peer line - there are %d fields: %s' %
223+ (len(fields), l))
224 return True
225 if fields[1] in self.ignorepeers:
226 return True
227@@ -186,20 +253,23 @@
228 if self.isnoiseline(l):
229 continue
230
231- # first column is the tally field, the rest are whitespace-separated fields
232+ # first column is the tally field, the rest are
233+ # whitespace-separated fields
234 tally = l[0]
235 fields = l[1:-1].split()
236
237 if self.shouldignore(fields, l):
238 continue
239
240- fieldnames = ['peer', 'refid', 'stratum', 'type', 'lastpoll', 'interval', 'reach',
241- 'delay', 'offset', 'jitter']
242+ fieldnames = ['peer', 'refid', 'stratum', 'type', 'lastpoll',
243+ 'interval', 'reach', 'delay', 'offset', 'jitter']
244 peerdata = dict(zip(fieldnames, fields))
245
246 offset = abs(float(peerdata['offset']))
247 if not self.parsetally(tally, peerdata, offset):
248- warnings.warn('Unknown tally code detected - please report a bug: %s' % (l))
249+ warnings.warn(
250+ 'Unknown tally code detected - please report a bug: %s' %
251+ (l))
252 continue
253
254 self.ntpdata['peers'] += 1
255@@ -208,35 +278,43 @@
256 # reachability - this counts the number of bits set in the reachability field
257 # (which is displayed in octal in the ntpq output)
258 # http://stackoverflow.com/questions/9829578/fast-way-of-counting-bits-in-python
259- self.ntpdata['totalreach'] += bin(int(peerdata['reach'], 8)).count("1")
260-
261- # reachability as a percentage of the last 8 polls, across all peers
262- self.ntpdata['reachability'] = float(self.ntpdata['totalreach']) * 100 / self.ntpdata['peers'] / 8
263+ self.ntpdata['totalreach'] += bin(int(peerdata['reach'],
264+ 8)).count("1")
265
266 # average offsets
267 if self.ntpdata['survivors'] > 0:
268- self.ntpdata['averageoffsetsurvivors'] = \
269- self.ntpdata['offsetsurvivors'] / self.ntpdata['survivors']
270+ self.ntpdata['averageoffsetsurvivors'] = self.ntpdata['offsetsurvivors'] / self.ntpdata['survivors']
271 if self.ntpdata['discards'] > 0:
272- self.ntpdata['averageoffsetdiscards'] = \
273- self.ntpdata['offsetdiscards'] / self.ntpdata['discards']
274- self.ntpdata['averageoffset'] = self.ntpdata['offsetall'] / self.ntpdata['peers']
275+ self.ntpdata['averageoffsetdiscards'] = self.ntpdata['offsetdiscards'] / self.ntpdata['discards']
276+
277+ if self.ntpdata['peers'] > 0:
278+ # precent average reachability of all peers over the last 8 polls
279+ reach = float(self.ntpdata['totalreach']) * 100 / self.ntpdata['peers']
280+ self.ntpdata['reachability'] = reach / 8
281+
282+ # average offset of all peers
283+ self.ntpdata['averageoffset'] = self.ntpdata['offsetall'] / self.ntpdata['peers']
284+ else:
285+ # if there are no peers, reachability is zero and average offset is invalid
286+ self.ntpdata['reachability'] = 0.0
287+ self.ntpdata['averageoffset'] = float('nan')
288
289 def dump(self):
290 if self.ntpdata.get('syncpeer'):
291- print "Synced to: %s, offset %g ms" % \
292- (self.ntpdata['syncpeer'], self.ntpdata['offsetsyncpeer'])
293+ print "Synced to: %s, offset %g ms" % (
294+ self.ntpdata['syncpeer'], self.ntpdata['offsetsyncpeer'])
295 else:
296 print "No remote sync peer"
297- print "%d total peers, average offset %g ms" % \
298- (self.ntpdata['peers'], self.ntpdata['averageoffset'])
299+ print "%d total peers, average offset %g ms" % (
300+ self.ntpdata['peers'], self.ntpdata['averageoffset'])
301 if self.ntpdata['survivors'] > 0:
302- print "%d good peers, average offset %g ms" % \
303- (self.ntpdata['survivors'], self.ntpdata['averageoffsetsurvivors'])
304+ print "%d good peers, average offset %g ms" % (
305+ self.ntpdata['survivors'], self.ntpdata['averageoffsetsurvivors'])
306 if self.ntpdata['discards'] > 0:
307- print "%d discarded peers, average offset %g ms" % \
308- (self.ntpdata['discards'], self.ntpdata['averageoffsetdiscards'])
309- print "Average reachability of all peers: %d%%" % (self.ntpdata['reachability'])
310+ print "%d discarded peers, average offset %g ms" % (
311+ self.ntpdata['discards'], self.ntpdata['averageoffsetdiscards'])
312+ print "Average reachability of all peers: %d%%" % (
313+ self.ntpdata['reachability'])
314
315 def check_peers(self, check=None):
316 """Check the number of usable peers"""
317@@ -246,11 +324,13 @@
318
319 def check_offset(self, check=None):
320 """Check the offset from the sync peer, returning critical, warning,
321- or OK based on the CheckNTPMon results.
322+ or OK based on the CheckNTPMon results.
323 If there is no sync peer, use the average offset of survivors instead.
324- If there are no survivors, use the average offset of discards instead, and return warning as a minimum.
325+ If there are no survivors, use the average offset of discards instead,
326+ and return warning as a minimum.
327 If there are no discards, return critical.
328 """
329+ result = 0
330 if check is None:
331 check = self.check if self.check else CheckNTPMon()
332 if 'offsetsyncpeer' in self.ntpdata:
333@@ -259,10 +339,20 @@
334 return check.offset(self.ntpdata['averageoffsetsurvivors'])
335 if 'averageoffsetdiscards' in self.ntpdata:
336 result = check.offset(self.ntpdata['averageoffsetdiscards'])
337- return 1 if result < 1 else result
338+ msg = "WARNING: No sync peer or survivors - used discard offsets"
339+ if check.is_silent():
340+ return [1 if result[0] < 1 else result[0],
341+ msg + " (" + result[1] + ")"]
342+ else:
343+ print msg
344+ return 1 if result < 1 else result
345 else:
346- print "CRITICAL: No peers for which to check offset"
347- return 2
348+ ret = [2, "CRITICAL: No peers for which to check offset"]
349+ if check.is_silent():
350+ return ret
351+ else:
352+ print ret[1]
353+ return ret[0]
354
355 def check_reachability(self, check=None):
356 """Check reachability of all peers"""
357@@ -275,74 +365,166 @@
358 if check is None:
359 check = self.check if self.check else CheckNTPMon()
360 if self.ntpdata.get('syncpeer') is None:
361- print "CRITICAL: No sync peer"
362- return 2
363+ ret = [2, "CRITICAL: No sync peer"]
364+ if check.is_silent():
365+ return ret
366+ else:
367+ print ret[1]
368+ return ret[0]
369 return check.sync(self.ntpdata['syncpeer'])
370
371 def checks(self, methods=None, check=None):
372- ret = 0
373+ """Run the specified list of checks (or all of them if none is supplied)
374+ and return the worst result. Output only the diagnostic message for that
375+ result."""
376+
377+ # ensure check exists and is silent
378+ if check is None:
379+ check = self.check if self.check else CheckNTPMonSilent()
380+ if not check.is_silent():
381+ check = CheckNTPMonSilent.clone(check)
382+ assert check.is_silent()
383+
384 if not methods:
385- methods = [self.check_offset, self.check_peers, self.check_reachability, self.check_sync]
386+ methods = [self.check_offset, self.check_peers,
387+ self.check_reachability, self.check_sync]
388+
389+ ret = -1
390+ msg = None
391 for method in methods:
392- check = method()
393- if ret < check:
394- ret = check
395+ result = method(check=check)
396+ if ret < result[0]:
397+ ret = result[0]
398+ msg = result[1]
399+
400+ if msg is None:
401+ print "%s returned no results - please report a bug" % (method)
402+ return 3
403+ print msg
404 return ret
405
406 @staticmethod
407 def query():
408 lines = None
409 try:
410- output = subprocess.check_output(["ntpq", "-pn"])
411- lines = output.split("\n")
412+ null = open("/dev/null", "a")
413+ output = subprocess.check_output(["ntpq", "-pn"], stderr=null)
414+ if len(output) > 0:
415+ lines = output.split("\n")
416 except:
417 traceback.print_exc(file=sys.stdout)
418 return lines
419
420
421+class NTPProcess(object):
422+
423+ def __init__(self, names=None):
424+ """Look for ntpd or xntpd in the process table and save its process object."""
425+ if names is None:
426+ names = ["ntpd", "xntpd"]
427+ self.proc = None
428+ for proc in psutil.process_iter():
429+ try:
430+ if proc.name() in names:
431+ self.proc = proc
432+ break
433+ except psutil.Error:
434+ pass
435+
436+ def runtime(self):
437+ """Return the length of time in seconds that the process has been running.
438+ If ntpd is not running or any error occurs, return -1."""
439+ if self.proc is None:
440+ return -1
441+ try:
442+ now = time.time()
443+ start = int(self.proc.create_time())
444+ return now - start
445+ except psutil.Error:
446+ return -1
447+
448+
449 def main():
450 methodnames = ['offset', 'peers', 'reachability', 'sync']
451 options = {
452- 'warnpeers': [ 2, int, 'Minimum number of peers to be considered non-critical'],
453- 'okpeers': [ 4, int, 'Minimum number of peers to be considered OK'],
454- 'warnoffset': [ 10, float, 'Minimum offset to be considered warning'],
455- 'critoffset': [ 50, float, 'Minimum offset to be considered critical'],
456- 'warnreach': [ 75, float, 'Minimum peer reachability percentage to be considered OK'],
457- 'critreach': [ 50, float, 'Minimum peer reachability percentage to be considered non-crtical'],
458+ 'warnpeers': [
459+ 2, int, 'Minimum number of peers to be considered non-critical',
460+ ],
461+ 'okpeers': [
462+ 4, int, 'Minimum number of peers to be considered OK',
463+ ],
464+ 'warnoffset': [
465+ 10, float, 'Minimum offset to be considered warning',
466+ ],
467+ 'critoffset': [
468+ 50, float, 'Minimum offset to be considered critical',
469+ ],
470+ 'warnreach': [
471+ 75, float, 'Minimum peer reachability percentage to be considered OK',
472+ ],
473+ 'critreach': [
474+ 50, float, 'Minimum peer reachability percentage to be considered non-crtical',
475+ ],
476 }
477
478 # Create check ranges; will be used by parse_args to store options
479 checkntpmon = CheckNTPMon()
480
481 # parse command line
482- parser = argparse.ArgumentParser(description='Nagios NTP check incorporating the logic of NTPmon')
483- parser.add_argument('--check', choices=methodnames,
484- help='Select check to run; if omitted, run all checks and return the worst result.')
485- parser.add_argument('--debug', action='store_true',
486- help='Include "ntpq -pn" output and internal state dump along with check results.')
487+ parser = argparse.ArgumentParser(
488+ description='Nagios NTP check incorporating the logic of NTPmon')
489+ parser.add_argument(
490+ '--check',
491+ choices=methodnames,
492+ help='Select check to run; if omitted, run all checks and return the worst result.')
493+ parser.add_argument(
494+ '--debug',
495+ action='store_true',
496+ help='Include "ntpq -pn" output and internal state dump along with check results.')
497+ parser.add_argument(
498+ '--run-time',
499+ default=512,
500+ type=int,
501+ help='Time in seconds (default: 512) for which to always return OK after ntpd startup.')
502+ parser.add_argument(
503+ '--test',
504+ action='store_true',
505+ help='Accept "ntpq -pn" output on standard input instead of running it.')
506 for o in options.keys():
507 helptext = options[o][2] + ' (default: %d)' % (options[o][0])
508- parser.add_argument('--' + o, default=options[o][0], help=helptext, type=options[o][1])
509+ parser.add_argument('--' + o,
510+ default=options[o][0],
511+ help=helptext,
512+ type=options[o][1])
513 args = parser.parse_args(namespace=checkntpmon)
514
515 # run ntpq
516- lines = NTPPeers.query()
517+ lines = NTPPeers.query() if not args.test else [x.rstrip() for x in sys.stdin.readlines()]
518 if lines is None:
519 # Unknown result
520- print "Cannot get peers from ntpq."
521- print "Please check that an NTP server is installed and functional."
522+ print "UNKNOWN: Cannot get peers from ntpq. Please check that an NTP server is installed and running."
523 sys.exit(3)
524
525- # initialise our object with the results of ntpq and our preferred check thresholds
526+ # Don't report anything other than OK until ntpd has been running for at
527+ # least enough time for 8 polling intervals of 64 seconds each.
528+ age = NTPProcess().runtime()
529+ if age > 0 and age <= args.run_time:
530+ print "OK: ntpd still starting up (running %d seconds)" % age
531+ sys.exit(0)
532+
533+ # initialise our object with the results of ntpq and our preferred check
534+ # thresholds
535 ntp = NTPPeers(lines, checkntpmon)
536
537 if args.debug:
538 print "\n".join(lines)
539+ checkntpmon.dump()
540 ntp.dump()
541
542 # work out which method to run
543 # (methods must be in the same order as methodnames above)
544- methods = [ntp.check_offset, ntp.check_peers, ntp.check_reachability, ntp.check_sync]
545+ methods = [ntp.check_offset, ntp.check_peers, ntp.check_reachability,
546+ ntp.check_sync]
547 checkmethods = dict(zip(methodnames, methods))
548
549 # if check argument is specified, run just that check
550@@ -352,10 +534,11 @@
551 ret = method()
552 # else check all the methods
553 else:
554- ret = ntp.checks(methods)
555+ ret = ntp.checks()
556
557 sys.exit(ret)
558
559+
560 if __name__ == "__main__":
561 main()
562
563
564=== modified file 'hooks/ntp_hooks.py'
565--- hooks/ntp_hooks.py 2015-03-31 00:35:56 +0000
566+++ hooks/ntp_hooks.py 2015-05-14 10:36:53 +0000
567@@ -88,7 +88,8 @@
568 'nrpe-external-master-relation-changed')
569 def update_nrpe_config():
570 # python-dbus is used by check_upstart_job
571- fetch.apt_install('python-dbus')
572+ # python-psutil is used by check_ntpmon
573+ fetch.apt_install(['python-dbus', 'python-psutil'])
574 nagios_ntpmon_checks = hookenv.config('nagios_ntpmon_checks')
575 if os.path.isdir(NAGIOS_PLUGINS):
576 host.rsync(os.path.join(os.getenv('CHARM_DIR'), 'files', 'nagios',
577
578=== modified file 'tests/00-setup'
579--- tests/00-setup 2014-06-18 18:11:00 +0000
580+++ tests/00-setup 2015-05-14 10:36:53 +0000
581@@ -3,11 +3,11 @@
582 set -ex
583
584 # Check if amulet is installed before adding repository and updating apt-get.
585-dpkg -s amulet
586-if [ $? -ne 0 ]; then
587+if ! dpkg -s amulet; then
588 sudo add-apt-repository -y ppa:juju/stable
589 sudo apt-get update
590 sudo apt-get install -y amulet
591 fi
592
593 # Install any additional python packages or software here.
594+sudo apt-get install -y python-psutil
595
596=== added directory 'unit_tests'
597=== added file 'unit_tests/test_check_ntpmon.py'
598--- unit_tests/test_check_ntpmon.py 1970-01-01 00:00:00 +0000
599+++ unit_tests/test_check_ntpmon.py 2015-05-14 10:36:53 +0000
600@@ -0,0 +1,492 @@
601+#!/usr/bin/python
602+#
603+# Author: Paul Gear
604+# Copyright: (c) 2015 Gear Consulting Pty Ltd <http://libertysys.com.au/>
605+# License: GPLv3 <http://www.gnu.org/licenses/gpl.html>
606+# Description: Test CheckNTPMon class
607+#
608+# This program is free software: you can redistribute it and/or modify it under
609+# the terms of the GNU General Public License as published by the Free Software
610+# Foundation, either version 3 of the License, or (at your option) any later
611+# version.
612+#
613+# This program is distributed in the hope that it will be useful, but WITHOUT
614+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
615+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
616+# details.
617+#
618+# You should have received a copy of the GNU General Public License along with
619+# this program. If not, see <http://www.gnu.org/licenses/>.
620+#
621+
622+import argparse
623+import unittest
624+import sys
625+
626+from pkg_resources import resource_filename
627+
628+# allow importing check_ntpmon from the correct directory
629+sys.path.append(resource_filename(__name__, '../files/nagios'))
630+
631+from check_ntpmon import CheckNTPMon, CheckNTPMonSilent, NTPPeers
632+
633+testdata = [
634+"""
635+ remote refid st t when poll reach delay offset jitter
636+==============================================================================
637+ 137.189.4.10 .STEP. 16 u - 1024 0 0.000 0.000 0.000
638+ 128.199.253.156 .STEP. 16 u - 1024 0 0.000 0.000 0.000
639+ 103.233.241.1 .STEP. 16 u - 1024 0 0.000 0.000 0.000
640+ 128.199.169.185 .STEP. 16 u - 1024 0 0.000 0.000 0.000
641+*91.189.89.199 131.188.3.220 2 u 1019 1024 377 346.845 -0.598 1.105
642+ 192.189.54.33 .STEP. 16 u - 1024 0 0.000 0.000 0.000
643+ 129.250.35.250 .STEP. 16 u - 1024 0 0.000 0.000 0.000
644+ 27.54.95.11 .STEP. 16 u - 1024 0 0.000 0.000 0.000
645+ 54.252.129.186 .STEP. 16 u - 1024 0 0.000 0.000 0.000
646+""",
647+"""
648+ remote refid st t when poll reach delay offset jitter
649+==============================================================================
650+-203.19.252.1 210.9.192.50 2 u 456 1024 377 47.629 -1.526 35.084
651+*202.60.94.11 223.252.32.9 2 u 618 1024 377 18.785 0.259 0.371
652++202.60.94.15 223.252.32.9 2 u 635 1024 377 19.333 0.038 0.851
653++54.252.129.186 223.252.32.9 2 u 927 1024 377 36.072 0.337 1.043
654+-192.168.1.2 203.23.237.200 3 u 788 1024 377 0.627 -0.605 0.162
655+-192.168.1.1 203.23.237.200 3 u 123 1024 377 0.299 -0.941 0.329
656+ 192.168.1.21 .INIT. 16 u - 1024 0 0.000 0.000 0.000
657+""",
658+"""
659+ remote refid st t when poll reach delay offset jitter
660+==============================================================================
661+*91.189.94.4 131.188.3.220 2 u 338 1024 377 1.600 -194.54 171.548
662+""",
663+"""
664+ remote refid st t when poll reach delay offset jitter
665+==============================================================================
666+ 218.189.210.3 .INIT. 16 u - 64 0 0.000 0.000 0.000
667+ 103.224.117.98 .INIT. 16 u - 64 0 0.000 0.000 0.000
668+ 118.143.17.82 .INIT. 16 u - 64 0 0.000 0.000 0.000
669+ 91.189.89.199 192.93.2.20 2 u 21 64 7 336.631 0.913 0.223
670+ 175.45.85.97 .INIT. 16 u - 64 0 0.000 0.000 0.000
671+ 192.189.54.33 .INIT. 16 u - 64 0 0.000 0.000 0.000
672+ 129.250.35.250 .INIT. 16 u - 64 0 0.000 0.000 0.000
673+ 54.252.165.245 .INIT. 16 u - 64 0 0.000 0.000 0.000
674+""",
675+]
676+
677+demodata = [
678+"""
679+ remote refid st t when poll reach delay offset disp
680+=========================================================================
681+*WWVB_SPEC(1) .WWVB. 0 l 114 64 377 0.00 37.623 12.77
682+ relay.hp.com listo 2 u 225 512 377 6.93 34.052 10.79
683+ cosl4.cup.hp.co listo 2 u 226 512 377 4.18 29.385 13.21
684+ paloalto.cns.hp listo 2 u 235 512 377 9.80 33.487 11.61
685+ chelmsford.cns. listo 2 u 233 512 377 88.79 30.462 9.66
686+ atlanta.cns.hp. listo 2 u 231 512 377 67.44 32.909 12.86
687+ colorado.cns.hp listo 2 u 233 512 377 43.70 30.077 18.63
688+ boise.cns.hp.co listo 2 u 224 512 377 33.42 31.682 8.54
689+""",
690+"""
691+ remote refid st t when poll reach delay offset jitter
692+==============================================================================
693+ GENERIC(1) .GPS. 0 l 4 64 1 0.000 -0.719 0.001
694+ PPS(1) .PPS. 16 l - 64 0 0.000 0.000 4000.00
695+ ltgpsdemo .INIT. 16 u 3 64 0 0.000 0.000 4000.00
696+""",
697+"""
698+ remote refid st t when poll reach delay offset jitter
699+==============================================================================
700+ GENERIC(1) .GPS. 0 l 38 64 7 0.000 -1.193 0.528
701+ PPS(1) .PPS. 16 l - 64 0 0.000 0.000 4000.00
702+ ltgpsdemo .GPS. 1 u 33 64 1 0.624 -0.417 0.001
703+""",
704+"""
705+ remote refid st t when poll reach delay offset jitter
706+==============================================================================
707+*GENERIC(1) .GPS. 0 l 45 64 377 0.000 -0.437 0.203
708+ PPS(1) .PPS. 16 l - 64 0 0.000 0.000 4000.00
709++ltgpsdemo .PPS. 1 u 116 512 377 0.500 0.349 0.106
710+""",
711+# mangled from Cisco router demo output:
712+"""
713+ 27.54.95.11 .STEP. 16 u - 64 0 0.000 0.000 15937.
714++130.102.128.23 216.218.254.20 2 u 25 64 77 51.267 32.537 189.39
715+ 128.184.34.53 169.254.0.1 3 u 64 64 122 49.115 29.474 1939.5
716+*129.250.35.250 133.243.238.24 2 u 14 64 177 261.47 7.906 65.514
717++129.250.35.251 133.243.238.24 2 u 55 64 77 255.70 13.942 190.86
718+""",
719+"""
720+ remote refid st t when poll reach delay offset jitter
721+==============================================================================
722+ ff05::101 .MCST. 16 u - 64 0 0.000 0.000 4000.00
723+*example.site.co .PPS. 1 u 320 1024 377 1.955 -1.234 1.368
724+""",
725+"""
726+remote refid st t when poll reach delay offset jitter
727+==========================================================
728+-navobs1.oar.net .USNO. 1 u 958 1024 377 89.425 -6.073 0.695
729+*navobs1.gatech. .GPS. 1 u 183 1024 375 82.102 1.639 0.281
730+-NAVOBS1.MIT.EDU .PSC. 1 u 895 1024 377 90.912 -0.207 0.368
731++navobs1.wustl.e .GPS. 1 u 48 1024 377 76.890 1.093 0.525
732+-bigben.cac.wash .USNO. 1 u 924 1024 377 113.327 0.028 0.326
733++tick.ucla.edu .GPS. 1 u 107 1024 377 102.470 2.032 0.482
734+-ntp.alaska.edu .GPS. 1 u 881 1024 377 168.741 5.180 5.157
735+-tock.mhpcc.hpc. .GPS. 1 u 933 1024 377 174.518 -1.094 0.054
736+""",
737+"""
738+remote refid st t when poll reach delay offset disp
739+==========================================================
740++128.252.19.1 .GPS. 1 u 495 1024 377 30.90 -6.366 8.26
741+*139.78.133.139 .USNO. 1 u 936 1024 377 48.43 -2.906 5.20
742+""",
743+"""
744+remote refid st t when poll reach delay offset jitter
745+======================================================
746++navobs1.wustl.e .GPS. 1 u 241 256 377 77.626 1.744 0.195
747+-tick.ucla.edu .GPS. 1 u 136 256 377 102.069 2.019 2.281
748++ntp.alaska.edu .GPS. 1 u 207 256 377 168.971 0.528 6.612
749+*GPS_NMEA(1) .GPS. 0 l 62 64 377 0.000 0.000 0.001
750+LOCAL(0) .LOCL. 10 l 62 64 377 0.000 0.000 0.000
751+""",
752+"""
753+ remote refid st t when poll reach delay offset jitter
754+==============================================================================
755+*dione.cbane.org 204.123.2.5 2 u 509 1024 377 51.661 -3.343 0.279
756++ns1.your-site.c 132.236.56.252 3 u 899 1024 377 48.395 2.047 1.006
757++ntp.yoinks.net 129.7.1.66 2 u 930 1024 377 0.693 1.035 0.241
758+ LOCAL(0) .LOCL. 10 l 45 64 377 0.000 0.000 0.001
759+""",
760+"""
761+remote refid st t when poll reach delay offset jitter
762+======================================================================
763+ 6s-ntp .ACST. 16 u - 64 0 0.000 0.000 0.002
764+*ntp0.kostecke.n 192.168.19.2 3 u 225 1024 377 0.723 -3.463 1.889
765+""",
766+"""
767+ remote refid st t when poll reach delay offset jitter
768+==============================================================================
769+ 10.35.60.40 .INIT. 16 u 505 1024 0 0.000 0.000 0.000
770+ 10.35.60.41 .INIT. 16 u 471 1024 0 0.000 0.000 0.000
771+""",
772+]
773+
774+
775+class TestCheckNTPMon(unittest.TestCase):
776+
777+ def test_offset(self):
778+ check = CheckNTPMon()
779+
780+ for i in [50.01, 50.1, 51, 99, 100, 999]:
781+ self.assertEqual(check.offset(i), 2, 'High offset non-critical')
782+ self.assertEqual(check.offset(-i), 2, 'High offset non-critical')
783+
784+ for i in [10.01, 10.1, 11, 49, 49.99, 50]:
785+ self.assertEqual(check.offset(i), 1, 'Moderate offset non-warning')
786+ self.assertEqual(check.offset(-i), 1, 'Moderate offset non-warning')
787+
788+ for i in [0, 0.01, 1, 1.01, 9, 9.99, 10]:
789+ self.assertEqual(check.offset(i), 0, 'Low offset non-OK')
790+ self.assertEqual(check.offset(-i), 0, 'Low offset non-OK')
791+
792+ def test_peers(self):
793+ check = CheckNTPMon()
794+
795+ for i in [-100, -10, -1, 0, 1]:
796+ self.assertEqual(check.peers(i), 2, 'Low peers non-critical')
797+
798+ for i in [2, 3]:
799+ self.assertEqual(check.peers(i), 1, 'Few peers non-warning')
800+
801+ for i in [4, 5, 6, 10, 100]:
802+ self.assertEqual(check.peers(i), 0, 'High peers non-OK')
803+
804+ def test_reach(self):
805+ check = CheckNTPMon()
806+
807+ for i in [0, 0.01, 1, 25, 49, 49.99, 50]:
808+ self.assertEqual(check.reachability(i), 2, 'Low reachability non-critical')
809+ for i in [50.01, 50.1, 74.99, 75]:
810+ self.assertEqual(check.reachability(i), 1, 'Moderate reachability non-warning')
811+ for i in [75.01, 76, 99, 100]:
812+ self.assertEqual(check.reachability(i), 0, 'High reachability non-OK')
813+ # check that invalid percentage causes exception
814+ for i in [-100, -1, 100.01, 101, 1000]:
815+ self.assertRaises(ValueError, check.reachability, (i))
816+
817+ def test_sync(self):
818+ check = CheckNTPMon()
819+
820+ self.assertEqual(check.sync(''), 2, 'Invalid sync peer not detected')
821+ self.assertEqual(check.sync(' '), 2, 'Invalid sync peer not detected')
822+ self.assertEqual(check.sync('!@#$%^&*()'), 2, 'Invalid sync peer not detected')
823+ self.assertEqual(check.sync('blah.example.com'), 0, 'Sync peer not detected')
824+ self.assertEqual(check.sync('192.168.2.1'), 0, 'Sync peer not detected')
825+ self.assertEqual(check.sync('fe80::1'), 0, 'Sync peer not detected')
826+ self.assertEqual(check.sync('ds002.dedicated'), 0, 'Sync peer not detected')
827+ self.assertEqual(check.sync('node01.au.serve'), 0, 'Sync peer not detected')
828+
829+ def test_NTPPeer0(self):
830+ # check the parsing done by NTPPeers
831+ ntp = NTPPeers(testdata[0].split("\n"))
832+ self.assertEqual(ntp.ntpdata['syncpeer'], '91.189.89.199')
833+ self.assertEqual(ntp.ntpdata['offsetsyncpeer'], 0.598)
834+ self.assertEqual(ntp.ntpdata['survivors'], 1)
835+ self.assertEqual(ntp.ntpdata['averageoffsetsurvivors'], 0.598)
836+ self.assertEqual(ntp.ntpdata['discards'], 0)
837+ self.assertEqual(ntp.ntpdata.get('averageoffsetdiscards'), None)
838+ self.assertEqual(ntp.ntpdata['peers'], 1)
839+ self.assertEqual(ntp.ntpdata['averageoffset'], 0.598)
840+ self.assertEqual(ntp.ntpdata['reachability'], 100)
841+
842+ # run checks on the data
843+ check = CheckNTPMon()
844+ self.assertEqual(check.sync(ntp.ntpdata['syncpeer']), 0, 'Sync peer not detected')
845+ self.assertEqual(check.offset(ntp.ntpdata['offsetsyncpeer']), 0, 'Low offset non-OK')
846+ self.assertEqual(check.offset(ntp.ntpdata['averageoffsetsurvivors']), 0, 'Low offset non-OK')
847+ self.assertEqual(check.offset(ntp.ntpdata['averageoffset']), 0, 'Low offset non-OK')
848+ self.assertEqual(check.peers(ntp.ntpdata['peers']), 2, 'Low peers non-critical')
849+ self.assertEqual(check.reachability(ntp.ntpdata['reachability']), 0,
850+ 'High reachability non-OK')
851+
852+ # run overall health checks
853+ self.assertEqual(ntp.check_sync(), 0, 'Sync peer not detected')
854+ self.assertEqual(ntp.check_offset(), 0, 'Low offset non-OK')
855+ self.assertEqual(ntp.check_peers(), 2, 'Low peers non-critical')
856+ self.assertEqual(ntp.check_reachability(), 0, 'High reachability non-OK')
857+
858+ def test_NTPPeer1(self):
859+ # check the parsing done by NTPPeers
860+ ntp = NTPPeers(testdata[1].split("\n"))
861+ self.assertEqual(ntp.ntpdata['syncpeer'], '202.60.94.11')
862+ self.assertEqual(ntp.ntpdata['offsetsyncpeer'], 0.259)
863+ self.assertEqual(ntp.ntpdata['survivors'], 3)
864+ self.assertEqual(ntp.ntpdata['averageoffsetsurvivors'], 0.21133333333333335)
865+ self.assertEqual(ntp.ntpdata['discards'], 3)
866+ self.assertEqual(ntp.ntpdata['averageoffsetdiscards'], 1.024)
867+ self.assertEqual(ntp.ntpdata['peers'], 6)
868+ self.assertEqual(ntp.ntpdata['averageoffset'], 0.6176666666666667)
869+ self.assertEqual(ntp.ntpdata['reachability'], 100)
870+
871+ # run checks on the data
872+ check = CheckNTPMon()
873+ self.assertEqual(check.sync(ntp.ntpdata['syncpeer']), 0, 'Sync peer not detected')
874+ self.assertEqual(check.offset(ntp.ntpdata['offsetsyncpeer']), 0, 'Low offset non-OK')
875+ self.assertEqual(check.offset(ntp.ntpdata['averageoffsetsurvivors']), 0, 'Low offset non-OK')
876+ self.assertEqual(check.offset(ntp.ntpdata['averageoffsetdiscards']), 0, 'Low offset non-OK')
877+ self.assertEqual(check.offset(ntp.ntpdata['averageoffset']), 0, 'Low offset non-OK')
878+ self.assertEqual(check.peers(ntp.ntpdata['peers']), 0, 'High peers non-OK')
879+ self.assertEqual(check.reachability(ntp.ntpdata['reachability']), 0,
880+ 'High reachability non-OK')
881+
882+ # run overall health checks
883+ self.assertEqual(ntp.check_sync(), 0, 'Sync peer not detected')
884+ self.assertEqual(ntp.check_offset(), 0, 'Low offset non-OK')
885+ self.assertEqual(ntp.check_peers(), 0, 'High peers non-OK')
886+ self.assertEqual(ntp.check_reachability(), 0, 'High reachability non-OK')
887+
888+ def test_NTPPeer2(self):
889+ # check the parsing done by NTPPeers
890+ ntp = NTPPeers(testdata[2].split("\n"))
891+ self.assertEqual(ntp.ntpdata['syncpeer'], '91.189.94.4')
892+ self.assertEqual(ntp.ntpdata['offsetsyncpeer'], 194.54)
893+ self.assertEqual(ntp.ntpdata['survivors'], 1)
894+ self.assertEqual(ntp.ntpdata['averageoffsetsurvivors'], 194.54)
895+ self.assertEqual(ntp.ntpdata['discards'], 0)
896+ self.assertEqual(ntp.ntpdata.get('averageoffsetdiscards'), None)
897+ self.assertEqual(ntp.ntpdata['peers'], 1)
898+ self.assertEqual(ntp.ntpdata['averageoffset'], 194.54)
899+ self.assertEqual(ntp.ntpdata['reachability'], 100)
900+
901+ # run checks on the data
902+ check = CheckNTPMon()
903+ self.assertEqual(check.sync(ntp.ntpdata['syncpeer']), 0, 'Sync peer not detected')
904+ self.assertEqual(check.offset(ntp.ntpdata['offsetsyncpeer']), 2, 'High offset non-critical')
905+ self.assertEqual(check.offset(ntp.ntpdata['averageoffsetsurvivors']), 2, 'High offset non-critical')
906+ self.assertEqual(ntp.ntpdata.get('averageoffsetdiscards'), None)
907+ self.assertEqual(check.offset(ntp.ntpdata['averageoffset']), 2, 'High offset non-critical')
908+ self.assertEqual(check.peers(ntp.ntpdata['peers']), 2, 'Low peers non-critical')
909+ self.assertEqual(check.reachability(ntp.ntpdata['reachability']), 0,
910+ 'High reachability non-OK')
911+
912+ # run overall health checks
913+ self.assertEqual(ntp.check_sync(), 0, 'Sync peer not detected')
914+ self.assertEqual(ntp.check_offset(), 2, 'High offset non-critical')
915+ self.assertEqual(ntp.check_peers(), 2, 'Low peers non-critical')
916+ self.assertEqual(ntp.check_reachability(), 0, 'High reachability non-OK')
917+
918+ def test_NTPPeer3(self):
919+ # check the parsing done by NTPPeers
920+ ntp = NTPPeers(testdata[3].split("\n"))
921+ self.assertEqual(ntp.ntpdata.get('syncpeer'), None)
922+ self.assertEqual(ntp.ntpdata.get('offsetsyncpeer'), None)
923+ self.assertEqual(ntp.ntpdata['survivors'], 0)
924+ self.assertEqual(ntp.ntpdata.get('averageoffsetsurvivors'), None)
925+ self.assertEqual(ntp.ntpdata['discards'], 1)
926+ self.assertEqual(ntp.ntpdata['averageoffsetdiscards'], 0.913)
927+ self.assertEqual(ntp.ntpdata['peers'], 1)
928+ self.assertEqual(ntp.ntpdata['averageoffset'], 0.913)
929+ self.assertEqual(ntp.ntpdata['reachability'], 37.5)
930+
931+ # run checks on the data
932+ check = CheckNTPMon()
933+ self.assertEqual(check.offset(ntp.ntpdata['averageoffsetdiscards']), 0, 'Low offset non-OK')
934+ self.assertEqual(check.offset(ntp.ntpdata['averageoffset']), 0, 'Low offset non-OK')
935+ self.assertEqual(check.peers(ntp.ntpdata['peers']), 2, 'Low peers non-critical')
936+ self.assertEqual(check.reachability(ntp.ntpdata['reachability']), 2,
937+ 'Low reachability non-critical')
938+
939+ # run overall health checks
940+ self.assertEqual(ntp.check_sync(), 2, 'Missing sync peer not detected')
941+ self.assertEqual(ntp.check_offset(), 1, 'Missing sync peer/survivor offset non-warning')
942+ self.assertEqual(ntp.check_peers(), 2, 'Low peers non-critical')
943+ self.assertEqual(ntp.check_reachability(), 2, 'Low reachability non-critical')
944+
945+ def test_defaults(self):
946+ c = CheckNTPMon()
947+ self.assertEqual(c.warnpeers, 2)
948+ self.assertEqual(c.okpeers, 4)
949+ self.assertEqual(c.warnoffset, 10)
950+ self.assertEqual(c.critoffset, 50)
951+ self.assertEqual(c.warnreach, 75)
952+ self.assertEqual(c.critreach, 50)
953+
954+ def test_non_default(self):
955+ c = CheckNTPMon(1, 2, 9, 49, 80, 60)
956+ self.assertEqual(c.warnpeers, 1)
957+ self.assertEqual(c.okpeers, 2)
958+ self.assertEqual(c.warnoffset, 9)
959+ self.assertEqual(c.critoffset, 49)
960+ self.assertEqual(c.warnreach, 80)
961+ self.assertEqual(c.critreach, 60)
962+
963+ def test_clone(self):
964+ ch = CheckNTPMon()
965+ self.assertFalse(ch.is_silent())
966+ c = CheckNTPMonSilent.clone(ch)
967+ self.assertTrue(c.is_silent())
968+ self.assertEqual(c.warnpeers, 2)
969+ self.assertEqual(c.okpeers, 4)
970+ self.assertEqual(c.warnoffset, 10)
971+ self.assertEqual(c.critoffset, 50)
972+ self.assertEqual(c.warnreach, 75)
973+ self.assertEqual(c.critreach, 50)
974+
975+ def test_clone_non_default(self):
976+ ch = CheckNTPMon(1, 2, 9, 49, 80, 60)
977+ self.assertFalse(ch.is_silent())
978+ c = CheckNTPMonSilent.clone(ch)
979+ self.assertTrue(c.is_silent())
980+ self.assertEqual(c.warnpeers, 1)
981+ self.assertEqual(c.okpeers, 2)
982+ self.assertEqual(c.warnoffset, 9)
983+ self.assertEqual(c.critoffset, 49)
984+ self.assertEqual(c.warnreach, 80)
985+ self.assertEqual(c.critreach, 60)
986+
987+ def test_clone_silent(self):
988+ """Cloning CheckNTPMonSilent should return itself"""
989+ cs = CheckNTPMonSilent()
990+ c = CheckNTPMonSilent.clone(cs)
991+ self.assertEqual(c, cs)
992+
993+ def test_clone_inheritance(self):
994+ """Cloning a child class should work too"""
995+ class testobject(CheckNTPMonSilent):
996+ def dump(self):
997+ pass
998+ obj = testobject()
999+ self.assertEqual(obj.warnpeers, 2)
1000+ self.assertEqual(obj.okpeers, 4)
1001+ self.assertEqual(obj.warnoffset, 10)
1002+ self.assertEqual(obj.critoffset, 50)
1003+ self.assertEqual(obj.warnreach, 75)
1004+ self.assertEqual(obj.critreach, 50)
1005+ c = CheckNTPMonSilent.clone(obj)
1006+ self.assertEqual(c.warnpeers, 2)
1007+ self.assertEqual(c.okpeers, 4)
1008+ self.assertEqual(c.warnoffset, 10)
1009+ self.assertEqual(c.critoffset, 50)
1010+ self.assertEqual(c.warnreach, 75)
1011+ self.assertEqual(c.critreach, 50)
1012+
1013+ def test_clone_inheritance_non_silent(self):
1014+ """Cloning a non-silent child class should work too"""
1015+ class testobject(CheckNTPMon):
1016+ def dump(self):
1017+ pass
1018+ obj = testobject()
1019+ self.assertEqual(obj.warnpeers, 2)
1020+ self.assertEqual(obj.okpeers, 4)
1021+ self.assertEqual(obj.warnoffset, 10)
1022+ self.assertEqual(obj.critoffset, 50)
1023+ self.assertEqual(obj.warnreach, 75)
1024+ self.assertEqual(obj.critreach, 50)
1025+ c = CheckNTPMonSilent.clone(obj)
1026+ self.assertEqual(c.warnpeers, 2)
1027+ self.assertEqual(c.okpeers, 4)
1028+ self.assertEqual(c.warnoffset, 10)
1029+ self.assertEqual(c.critoffset, 50)
1030+ self.assertEqual(c.warnreach, 75)
1031+ self.assertEqual(c.critreach, 50)
1032+
1033+ def test_clone_inheritance_non_silent_made_silent(self):
1034+ """Don't try this at home, kids"""
1035+ class testobject(CheckNTPMon):
1036+ def is_silent(self):
1037+ return True
1038+ obj = testobject()
1039+ c = CheckNTPMonSilent.clone(obj)
1040+ self.assertEqual(c, obj)
1041+
1042+ def test_bad_clone(self):
1043+ """Cloning something that's not a CheckNTPMonSilent or CheckNTPMon should raise an AttributeError"""
1044+ class testobject(object):
1045+ pass
1046+ obj = testobject()
1047+ self.assertRaises(AttributeError, CheckNTPMonSilent.clone, obj)
1048+
1049+ def test_demos(self):
1050+ """Ensure that demo data is parsed successfully and doesn't produce exceptions or unknown results"""
1051+ for d in demodata:
1052+ ntp = NTPPeers(d.split("\n"))
1053+ ntp.dump()
1054+ methods = [ntp.check_offset, ntp.check_peers, ntp.check_reachability,
1055+ ntp.check_sync, ntp.checks]
1056+ for method in methods:
1057+ ret = method()
1058+ self.assertIn(ret, [0, 1, 2],
1059+ "Method %s returned invalid result parsing demo data:\n%s\nTry running with --show-demos." % (method, d))
1060+
1061+
1062+def demo():
1063+ """Duplicate of test_demos which shows full output"""
1064+ i = 0
1065+ for d in demodata:
1066+ print "Parsing demo data %d: %s" % (i, d)
1067+ ntp = NTPPeers(d.split("\n"))
1068+ i += 1
1069+ ntp.dump()
1070+ methods = [ntp.check_offset, ntp.check_peers, ntp.check_reachability,
1071+ ntp.check_sync, ntp.checks]
1072+ for method in methods:
1073+ ret = method()
1074+ if ret not in [0, 1, 2]:
1075+ print "Method %s returned invalid result parsing demo data:\n%s" % (method, d)
1076+ sys.exit(3)
1077+
1078+
1079+if __name__ == "__main__":
1080+ # object to store parsed arguments
1081+ test_checkntpmon = CheckNTPMon()
1082+
1083+ # parse command line
1084+ parser = argparse.ArgumentParser(description='NTPmon test class')
1085+ parser.add_argument('--show-demos', action='store_true',
1086+ help='Show demo output.')
1087+ args = parser.parse_args(namespace=test_checkntpmon)
1088+ if test_checkntpmon.show_demos:
1089+ demo()
1090+ else:
1091+ unittest.main()
1092+

Subscribers

People subscribed via source and target branches

to all changes: