Merge lp:~paulgear/charms/trusty/ntp/fix-divide-by-zero into lp:charms/trusty/ntp
- Trusty Tahr (14.04)
- fix-divide-by-zero
- Merge into trunk
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 |
Related bugs: |
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 |
Commit message
Description of the change
I've fixed the upstream version of check_ntpmon.py; this commit merges the current head version from https:/
- 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
Paul Gear (paulgear) wrote : | # |
Hi Whit,
I have a reasonably comprehensive test suite for it upstream: https:/
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:/
> where would be the most appropriate point to run it?
> --
>
> https:/
> 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
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
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_
Adam Israel (aisrael) : | # |
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_
> 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
Paul Gear (paulgear) wrote : | # |
I've added the unit tests for check_ntpmon to the unit_tests directory now.
Tim Van Steenburgh (tvansteenburgh) wrote : | # |
LGTM, thanks Paul!
Preview Diff
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 | + |
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