ntpclients/ntpviz.py: Whoops no datetime.UTC befor 3.11.
[ntpsec.git] / ntpclients / ntpsnmpd.py
blobffde3f940b142a37c047239caecf5fe700b708fc
1 #! @PYSHEBANG@
2 # -*- coding: utf-8 -*-
4 # Copyright the NTPsec project contributors
6 # SPDX-License-Identifier: BSD-2-Clause
8 from __future__ import print_function, division
10 import sys
11 import os
12 import getopt
13 import time
14 import socket
16 try:
17 import ntp.packet
18 import ntp.util
19 import ntp.agentx_packet
20 ax = ntp.agentx_packet
21 from ntp.agentx import PacketControl
22 except ImportError as e:
23 sys.stderr.write(
24 "ntpsnmpd: can't find Python NTP library.\n")
25 sys.stderr.write("%s\n" % e)
26 sys.exit(1)
29 # TODO This is either necessary, or a different workaround is.
30 ntp.util.deunicode_units()
32 logfp = sys.stderr
33 nofork = False
34 debug = 0
35 defaultTimeout = 30
37 log = (lambda msg, msgdbg: ntp.util.dolog(logfp, msg, debug, msgdbg))
39 ntpRootOID = (1, 3, 6, 1, 2, 1, 197) # mib-2 . 197, aka: NTPv4-MIB
41 snmpTrapOID = (1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
42 snmpSysUptime = (1, 3, 6, 1, 2, 1, 1, 3, 0)
44 DEFHOST = "localhost"
45 DEFLOG = "ntpsnmpd.log"
48 class DataSource(ntp.agentx.MIBControl):
49 def __init__(self, hostname=DEFHOST, settingsFile=None, notifySpin=0.1):
50 # This is defined as a dict tree because it is simpler, and avoids
51 # certain edge cases
52 # OIDs are relative from ntp root
53 ntp.agentx.MIBControl.__init__(self, mibRoot=ntpRootOID)
54 # MIB node init
55 # block 0
56 self.addNode((0,)) # ntpEntNotifications
57 self.addNode((0, 1)) # ntpEntNotifModeChange
58 self.addNode((0, 2)) # ntpEntNotifStratumChange
59 self.addNode((0, 3)) # ntpEntNotifSyspeerChange
60 self.addNode((0, 4)) # ntpEntNotifAddAssociation
61 self.addNode((0, 5)) # ntpEntNotifRemoveAsociation
62 self.addNode((0, 6)) # ntpEntNotifConfigChanged
63 self.addNode((0, 7)) # ntpEntNotifLeapSecondAnnounced
64 self.addNode((0, 8)) # ntpEntNotifHeartbeat
65 # block 1
66 # block 1, 1
67 self.addNode((1, 1, 1, 0), # ntpNetSoftwareName utf8str
68 (lambda oid: self.cbr_systemInfo(oid, "name")))
69 self.addNode((1, 1, 2, 0), # ntpEntSoftwareVersion utf8str
70 (lambda oid: self.cbr_systemInfo(oid, "version")))
71 self.addNode((1, 1, 3, 0), # ntpEntSoftwareVendor utf8str
72 (lambda oid: self.cbr_systemInfo(oid, "vendor")))
73 self.addNode((1, 1, 4, 0), # ntpEntSystemType utf8str
74 (lambda oid: self.cbr_systemInfo(oid, "system")))
75 self.addNode((1, 1, 5, 0), # ntpEntTimeResolution uint32
76 self.cbr_timeResolution)
77 self.addNode((1, 1, 6, 0), # ntpEntTimePrecision int32
78 self.cbr_timePrecision)
79 self.addNode((1, 1, 7, 0), # ntpEntTimeDistance DisplayString
80 self.cbr_timeDistance)
81 # block 1, 2
82 self.addNode((1, 2, 1, 0), # ntpEntStatusCurrentMode INTEGER {...}
83 self.cbr_statusCurrentMode)
84 self.addNode((1, 2, 2, 0), # ntpEntStatusStratum NtpStratum
85 self.cbr_statusStratum)
86 self.addNode((1, 2, 3, 0), # ntpEntStatusActiveRefSourceId uint32
87 self.cbr_statusActiveRefSourceID)
88 self.addNode((1, 2, 4, 0), # ntpEntStatusActiveRefSourceName utf8str
89 self.cbr_statusActiveRefSourceName)
90 self.addNode((1, 2, 5, 0), # ntpEntStatusActiveOffset DisplayString
91 self.cbr_statusActiveOffset)
92 self.addNode((1, 2, 6, 0), # ntpEntStatusNumberOfRefSources unit32
93 self.cbr_statusNumRefSources)
94 self.addNode((1, 2, 7, 0), # ntpEntStatusDispersion DisplayString
95 self.cbr_statusDispersion)
96 self.addNode((1, 2, 8, 0), # ntpEntStatusEntityUptime TimeTicks
97 self.cbr_statusEntityUptime)
98 self.addNode((1, 2, 9, 0), # ntpEntStatusDateTime NtpDateTime
99 self.cbr_statusDateTime)
100 self.addNode((1, 2, 10, 0), # ntpEntStatusLeapSecond NtpDateTime
101 self.cbr_statusLeapSecond)
102 self.addNode((1, 2, 11, 0), # ntpEntStatusLeapSecondDirection int32
103 self.cbr_statusLeapSecDirection)
104 self.addNode((1, 2, 12, 0), # ntpEntStatusInPkts Counter32
105 self.cbr_statusInPkts)
106 self.addNode((1, 2, 13, 0), # ntpEntStatusOutPkts Counter32
107 self.cbr_statusOutPkts)
108 self.addNode((1, 2, 14, 0), # ntpEntStatusBadVersion Counter32
109 self.cbr_statusBadVersion)
110 self.addNode((1, 2, 15, 0), # ntpEntStatusProtocolError Counter32
111 self.cbr_statusProtocolError)
112 self.addNode((1, 2, 16, 0), # ntpEntStatusNotifications Counter32
113 self.cbr_statusNotifications)
114 self.addNode((1, 2, 17, 1, 1)) # ntpEntStatPktMode INTEGER {...}
115 self.addNode((1, 2, 17, 1, 2)) # ntpEntStatPktSent Counter32
116 self.addNode((1, 2, 17, 1, 3)) # ntpEntStatPktRecived Counter32
117 # block 1, 3
118 self.addNode((1, 3, 1, 1, 1), # ntpAssocId uint32 (1..99999)
119 dynamic=self.sub_assocID)
120 self.addNode((1, 3, 1, 1, 2), # ntpAssocName utf8str
121 dynamic=self.sub_assocName)
122 self.addNode((1, 3, 1, 1, 3), # ntpAssocRefId DisplayString
123 dynamic=self.sub_assocRefID)
124 self.addNode((1, 3, 1, 1, 4), # ntpAssocAddressType InetAddressType
125 dynamic=self.sub_assocAddrType)
126 self.addNode((1, 3, 1, 1, 5), # ntpAssocAddress InetAddress SIZE
127 dynamic=self.sub_assocAddr)
128 self.addNode((1, 3, 1, 1, 6), # ntpAssocOffset DisplayString
129 dynamic=self.sub_assocOffset)
130 self.addNode((1, 3, 1, 1, 7), # ntpAssocStratum NtpStratum
131 dynamic=self.sub_assocStratum)
132 self.addNode((1, 3, 1, 1, 8), # ntpAssocStatusJitter DisplayString
133 dynamic=self.sub_assocJitter)
134 self.addNode((1, 3, 1, 1, 9), # ntpAssocStatusDelay DisplayString
135 dynamic=self.sub_assocDelay)
136 self.addNode((1, 3, 1, 1, 10), # ntpAssocStatusDispersion DisplayStr
137 dynamic=self.sub_assocDispersion)
138 self.addNode((1, 3, 2, 1, 1), # ntpAssocStatInPkts Counter32
139 dynamic=self.sub_assocStatInPkts)
140 self.addNode((1, 3, 2, 1, 2), # ntpAssocStatOutPkts Counter32
141 dynamic=self.sub_assocStatOutPkts)
142 self.addNode((1, 3, 2, 1, 3), # ntpAssocStatProtocolError Counter32
143 dynamic=self.sub_assocStatProtoErr)
144 # block 1, 4
145 self.addNode((1, 4, 1, 0), # ntpEntHeartbeatInterval unit32
146 self.cbr_entHeartbeatInterval,
147 self.cbw_entHeartbeatInterval)
148 self.addNode((1, 4, 2, 0), # ntpEntNotifBits BITS {...}
149 self.cbr_entNotifBits,
150 self.cbw_entNotifBits)
151 # block 1, 5
152 self.addNode((1, 5, 1, 0), # ntpEntNotifMessage utf8str
153 self.cbr_entNotifMessage)
154 # block 2 # all compliance statements
155 # print(repr(self.oidTree))
156 # print(self.oidTree[1]["subids"][1][1][0])
157 self.session = ntp.packet.ControlSession()
158 self.hostname = hostname if hostname else DEFHOST
159 self.session.openhost(self.hostname)
160 self.settingsFilename = settingsFile
161 # Cache so we don't hammer ntpd, default 1 second timeout
162 # Timeout default pulled from a hat: we don't want it to last for
163 # long, just not flood ntpd with duplicatte requests during a walk.
164 self.cache = ntp.util.Cache(1)
165 self.oldValues = {} # Used by notifications to detect changes
166 # spinGap so we don't spam ntpd with requests during notify checks
167 self.notifySpinTime = notifySpin
168 self.lastNotifyCheck = 0
169 self.lastHeartbeat = 0 # Timestamp used for heartbeat notifications
170 self.heartbeatInterval = 0 # should save to disk
171 self.sentNotifications = 0
172 # Notify bits, they control whether the daemon sends notifications.
173 # these are saved to disk
174 self.notifyModeChange = False # 1
175 self.notifyStratumChange = False # 2
176 self.notifySyspeerChange = False # 3
177 self.notifyAddAssociation = False # 4
178 self.notifyRMAssociation = False # 5
179 self.notifyConfigChange = False # 6 [This is not implemented]
180 self.notifyLeapSecondAnnounced = False # 7
181 self.notifyHeartbeat = False # 8
182 self.misc_loadDynamicSettings()
184 # =============================================================
185 # Data read callbacks start here
186 # comment divider lines represent not yet implemented callbacks
187 # =============================================================
189 # Blank: notification OIDs
191 def cbr_systemInfo(self, oid, category=None):
192 if category == "name": # The product name of the running NTP
193 data = "NTPsec"
194 elif category == "version": # version string
195 data = ntp.util.stdversion()
196 elif category == "vendor": # vendor/author name
197 data = "Internet Civil Engineering Institute"
198 elif category == "system": # system / hardware info
199 # Extract sysname, release, machine from os.uname() tuple
200 uname = os.uname()
201 data = " ".join([uname[0], uname[2], uname[4]])
202 vb = ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
203 return vb
205 def cbr_timeResolution(self, oid):
206 # Uinteger32
207 # Arrives in fractional milliseconds
208 fuzz = self.safeReadvar(0, ["fuzz"])
209 if fuzz is None:
210 return None
211 fuzz = fuzz["fuzz"]
212 # We want to emit fractions of seconds
213 # Yes we are flooring instead of rounding: don't want to emit a
214 # resolution value higher than ntpd actually produces.
215 if fuzz != 0:
216 fuzz = int(1 / fuzz)
217 else:
218 fuzz = 0
219 return ax.Varbind(ax.VALUE_GAUGE32, oid, fuzz)
221 def cbr_timePrecision(self, oid):
222 return self.readCallbackSkeletonSimple(oid, "precision",
223 ax.VALUE_INTEGER)
225 def cbr_timeDistance(self, oid):
226 # Displaystring
227 data = self.safeReadvar(0, ["rootdist"], raw=True)
228 if data is None:
229 return None
230 data = ntp.util.unitifyvar(data["rootdist"][1], "rootdist",
231 width=None, unitSpace=True)
232 return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
234 # Blank: ntpEntStatus
236 def cbr_statusCurrentMode(self, oid):
237 mode = self.misc_getMode()
238 return ax.Varbind(ax.VALUE_INTEGER, oid, mode)
240 def cbr_statusStratum(self, oid):
241 # NTPstratum
242 return self.readCallbackSkeletonSimple(oid, "stratum",
243 ax.VALUE_GAUGE32)
245 def cbr_statusActiveRefSourceID(self, oid):
246 # range of uint32
247 syspeer = self.misc_getSyspeerID()
248 return ax.Varbind(ax.VALUE_GAUGE32, oid, syspeer)
250 def cbr_statusActiveRefSourceName(self, oid):
251 # utf8
252 data = self.safeReadvar(0, ["peeradr"])
253 if data is None:
254 return None
255 data = ntp.util.canonicalize_dns(data["peeradr"])
256 return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
258 def cbr_statusActiveOffset(self, oid):
259 # DisplayString
260 data = self.safeReadvar(0, ["koffset"], raw=True)
261 if data is None:
262 return None
263 data = ntp.util.unitifyvar(data["koffset"][1], "koffset",
264 width=None, unitSpace=True)
265 return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
267 def cbr_statusNumRefSources(self, oid):
268 # range of uint32
269 try:
270 data = self.session.readstat()
271 return ax.Varbind(ax.VALUE_GAUGE32, oid, len(data))
272 except ntp.packet.ControlException:
273 return None
275 def cbr_statusDispersion(self, oid):
276 # DisplayString
277 data = self.safeReadvar(0, ["rootdisp"], raw=True)
278 if data is None:
279 return None
280 return ax.Varbind(ax.VALUE_OCTET_STR, oid, data["rootdisp"][1])
282 def cbr_statusEntityUptime(self, oid):
283 # TimeTicks
284 # What the spec claims:
285 # The uptime of the NTP entity, (i.e., the time since ntpd was
286 # (re-)initialized not sysUptime!). The time is represented in
287 # hundreds of seconds since Jan 1, 1970 (00:00:00.000) UTC.
289 # First problem: TimeTicks represents hundred*ths* of seconds, could
290 # easily be a typo.
291 # Second problem: snmpwalk will happily give you a display of
292 # how long a period of time a value is, such as uptime since start.
293 # That is the opposite of what the spec claims.
295 # I am abandoning the spec, and going with what makes a lick of sense
296 uptime = self.safeReadvar(0, ["ss_reset"])
297 if uptime is None:
298 return None
299 uptime = uptime["ss_reset"] * 100
300 return ax.Varbind(ax.VALUE_TIME_TICKS, oid, uptime)
302 def cbr_statusDateTime(self, oid):
303 # NtpDateTime
304 data = self.safeReadvar(0, ["reftime"])
305 if data is None:
306 return None
307 txt = data["reftime"]
308 value = ntp.util.deformatNTPTime(txt)
309 return ax.Varbind(ax.VALUE_OCTET_STR, oid, value)
311 def cbr_statusLeapSecond(self, oid): # I am not confident in this yet
312 # NtpDateTime
313 DAY = 86400
314 fmt = "%.8x%.8x"
315 data = self.safeReadvar(0, ["reftime"])
316 hasleap = self.safeReadvar(0, ["leap"])
317 if (data is None) or (hasleap is None):
318 return None
319 data = data["reftime"]
320 hasleap = hasleap["leap"]
321 if hasleap in (1, 2):
322 seconds = int(data.split(".")[0], 0)
323 days = seconds // DAY
324 scheduled = (days * DAY) + (DAY - 1) # 23:59:59 of $CURRENT_DAY
325 formatted = fmt % (scheduled, 0)
326 else:
327 formatted = fmt % (0, 0)
328 value = ntp.util.hexstr2octets(formatted)
329 return ax.Varbind(ax.VALUE_OCTET_STR, oid, value)
331 def cbr_statusLeapSecDirection(self, oid):
332 # range of int32
333 leap = self.safeReadvar(0, ["leap"])
334 if leap is None:
335 return None
336 leap = leap["leap"]
337 if leap == 1:
338 pass # leap 1 == forward
339 elif leap == 2:
340 leap = -1 # leap 2 == backward
341 else:
342 leap = 0 # leap 0 or 3 == no change
343 return ax.Varbind(ax.VALUE_INTEGER, oid, leap)
345 def cbr_statusInPkts(self, oid):
346 return self.readCallbackSkeletonSimple(oid, "io_received",
347 ax.VALUE_COUNTER32)
349 def cbr_statusOutPkts(self, oid):
350 return self.readCallbackSkeletonSimple(oid, "io_sent",
351 ax.VALUE_COUNTER32)
353 def cbr_statusBadVersion(self, oid):
354 return self.readCallbackSkeletonSimple(oid, "ss_oldver",
355 ax.VALUE_COUNTER32)
357 def cbr_statusProtocolError(self, oid):
358 data = self.safeReadvar(0, ["ss_badformat", "ss_badauth"])
359 if data is None:
360 return None
361 protoerr = 0
362 for key in data.keys():
363 protoerr += data[key]
364 return ax.Varbind(ax.VALUE_COUNTER32, oid, protoerr)
366 def cbr_statusNotifications(self, oid):
367 return ax.Varbind(ax.VALUE_COUNTER32, oid, self.sentNotifications)
369 ##############################
371 # == Dynamics ==
372 # assocID
373 # assocName
374 # assocRefID
375 # assocAddrType
376 # assocAddr
377 # assocOffset
378 # assocStratum
379 # assocJitter
380 # assocDelay
381 # assocDispersion
382 # assocInPackets
383 # assocOutPackets
384 # assocProtocolErrors
386 #########################
388 def cbr_entHeartbeatInterval(self, oid):
389 # uint32
390 return ax.Varbind(ax.VALUE_GAUGE32, oid, self.heartbeatInterval)
392 def cbr_entNotifBits(self, oid):
393 # BITS
394 data = ax.bools2Bits((False, # notUsed(0)
395 self.notifyModeChange,
396 self.notifyStratumChange,
397 self.notifySyspeerChange,
398 self.notifyAddAssociation,
399 self.notifyRMAssociation,
400 self.notifyConfigChange,
401 self.notifyLeapSecondAnnounced,
402 self.notifyHeartbeat))
403 return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
405 ##########################
407 def cbr_entNotifMessage(self, oid):
408 # utf8str
409 return ax.Varbind(ax.VALUE_OCTET_STR, oid, "no event")
411 #########################
413 # =====================================
414 # Data write callbacks
415 # Returns an error value (or noError)
416 # Must check that the value is correct for the bind, this does not mean
417 # the type: the master agent handles that
418 # Actions: test, undo, commit, cleanup
419 # =====================================
421 def cbw_entHeartbeatInterval(self, action, varbind, oldData=None):
422 if action == "test":
423 return ax.ERR_NOERROR
424 elif action == "commit":
425 self.heartbeatInterval = varbind.payload
426 self.misc_storeDynamicSettings()
427 return ax.ERR_NOERROR
428 elif action == "undo":
429 self.heartbeatInterval = oldData
430 self.misc_storeDynamicSettings()
431 return ax.ERR_NOERROR
432 elif action == "cleanup":
433 pass
435 def cbw_entNotifBits(self, action, varbind, oldData=None):
436 if action == "test":
437 return ax.ERR_NOERROR
438 elif action == "commit":
439 (self.notifyModeChange,
440 self.notifyStratumChange,
441 self.notifySyspeerChange,
442 self.notifyAddAssociation,
443 self.notifyRMAssociation,
444 self.notifyConfigChange,
445 self.notifyLeapSecondAnnounced,
446 self.notifyHeartbeat) = ax.bits2Bools(varbind.payload, 8)
447 self.misc_storeDynamicSettings()
448 return ax.ERR_NOERROR
449 elif action == "undo":
450 (self.notifyModeChange,
451 self.notifyStratumChange,
452 self.notifySyspeerChange,
453 self.notifyAddAssociation,
454 self.notifyRMAssociation,
455 self.notifyConfigChange,
456 self.notifyLeapSecondAnnounced,
457 self.notifyHeartbeat) = ax.bits2Bools(oldData, 8)
458 self.misc_storeDynamicSettings()
459 return ax.ERR_NOERROR
460 elif action == "cleanup":
461 pass
463 # ========================================================================
464 # Dynamic tree generator callbacks
466 # The structure of these callbacks is somewhat complicated because they
467 # share code that is potentially finicky.
469 # The dynamicCallbackSkeleton() method handles the construction of the
470 # MIB tree, and the placement of the handler() within it. It also provides
471 # some useful data to the handler() via the readCallback() layer.
472 # ========================================================================
474 # Packet Mode Table
475 # These are left as stubs for now. Information is lacking on where the
476 # data should come from.
478 def sub_statPktMode(self):
479 pass
481 def sub_statPktSent(self):
482 pass
484 def sub_statPktRecv(self):
485 pass
487 # Association Table
489 def sub_assocID(self):
490 def handler(oid, associd):
491 return ax.Varbind(ax.VALUE_GAUGE32, oid, associd)
492 return self.dynamicCallbackSkeleton(handler)
494 def sub_assocName(self):
495 return self.dynamicCallbackPeerdata("srcadr", True,
496 ax.VALUE_OCTET_STR)
498 def sub_assocRefID(self):
499 def handler(oid, associd):
500 pdata = self.misc_getPeerData()
501 if pdata is None:
502 return None
503 # elaborate code in util.py indicates this may not be stable
504 try:
505 refid = pdata[associd]["refid"][1]
506 except IndexError:
507 refid = ""
508 return ax.Varbind(ax.VALUE_OCTET_STR, oid, refid)
509 return self.dynamicCallbackSkeleton(handler)
511 def sub_assocAddrType(self):
512 def handler(oid, associd):
513 pdata = self.misc_getPeerData()
514 if pdata is None:
515 return None
516 srcadr = pdata[associd]["srcadr"][1]
517 try:
518 socklen = len(socket.getaddrinfo(srcadr, None)[0][-1])
519 except socket.gaierror:
520 socklen = None
521 if socklen == 2: # ipv4
522 addrtype = 1
523 elif socklen == 4: # ipv6
524 addrtype = 2
525 else:
526 # there is also ipv4z and ipv6z..... don't know how to
527 # detect those yet. Or if I even need to.
528 addrtype = 0 # is this ok? or should it return a NULL?
529 return ax.Varbind(ax.VALUE_INTEGER, oid, addrtype)
530 return self.dynamicCallbackSkeleton(handler)
532 def sub_assocAddr(self):
533 def handler(oid, associd):
534 pdata = self.misc_getPeerData()
535 if pdata is None:
536 return None
537 srcadr = pdata[associd]["srcadr"][1]
538 # WARNING: I am only guessing that this is correct
539 # Discover what type of address we have
540 try:
541 sockinfo = socket.getaddrinfo(srcadr, None)[0][-1]
542 addr = sockinfo[0]
543 ipv6 = True if len(sockinfo) == 4 else False
544 except socket.gaierror:
545 addr = None # how to handle?
546 ipv6 = None
547 # Convert address string to octets
548 srcadr = []
549 if not ipv6:
550 pieces = addr.split(".")
551 for piece in pieces:
552 try:
553 srcadr.append(int(piece)) # feed it a list of ints
554 except ValueError:
555 # Have gotten piece == "" before. Skip over that.
556 # Still try to return data because it is potential
557 # debugging information.
558 continue
559 elif ipv6:
560 pieces = addr.split(":")
561 for piece in pieces:
562 srcadr.append(ntp.util.hexstr2octets(piece))
563 srcadr = "".join(srcadr) # feed it an octet string
564 # The octet string encoder can handle either chars or 0-255
565 # ints. We use both of those options.
566 return ax.Varbind(ax.VALUE_OCTET_STR, oid, srcadr)
567 return self.dynamicCallbackSkeleton(handler)
569 def sub_assocOffset(self):
570 def handler(oid, associd):
571 pdata = self.misc_getPeerData()
572 if pdata is None:
573 return None
574 offset = pdata[associd]["offset"][1]
575 offset = ntp.util.unitifyvar(offset, "offset", width=None,
576 unitSpace=True)
577 return ax.Varbind(ax.VALUE_OCTET_STR, oid, offset)
578 return self.dynamicCallbackSkeleton(handler)
580 def sub_assocStratum(self):
581 return self.dynamicCallbackPeerdata("stratum", False,
582 ax.VALUE_GAUGE32)
584 def sub_assocJitter(self):
585 return self.dynamicCallbackPeerdata("jitter", True,
586 ax.VALUE_OCTET_STR)
588 def sub_assocDelay(self):
589 return self.dynamicCallbackPeerdata("delay", True,
590 ax.VALUE_OCTET_STR)
592 def sub_assocDispersion(self):
593 return self.dynamicCallbackPeerdata("rootdisp", True,
594 ax.VALUE_OCTET_STR)
596 def sub_assocStatInPkts(self):
597 def handler(oid, associd):
598 inpkts = self.safeReadvar(associd, ["received"])
599 if inpkts is None:
600 return None
601 inpkts = inpkts["received"]
602 return ax.Varbind(ax.VALUE_COUNTER32, oid, inpkts)
603 return self.dynamicCallbackSkeleton(handler)
605 def sub_assocStatOutPkts(self):
606 def handler(oid, associd):
607 outpkts = self.safeReadvar(associd, ["sent"])
608 if outpkts is None:
609 return None
610 outpkts = outpkts["sent"]
611 return ax.Varbind(ax.VALUE_COUNTER32, oid, outpkts)
612 return self.dynamicCallbackSkeleton(handler)
614 def sub_assocStatProtoErr(self):
615 def handler(oid, associd):
616 pvars = self.safeReadvar(associd, ["badauth", "bogusorg",
617 "seldisp", "selbroken"])
618 if pvars is None:
619 return None
620 protoerr = 0
621 for key in pvars.keys():
622 protoerr += pvars[key]
623 return ax.Varbind(ax.VALUE_COUNTER32, oid, protoerr)
624 return self.dynamicCallbackSkeleton(handler)
626 # =====================================
627 # Notification handlers
628 # =====================================
630 def checkNotifications(self, control):
631 currentTime = time.time()
632 if (currentTime - self.lastNotifyCheck) < self.notifySpinTime:
633 return
634 self.lastNotifyCheck = currentTime
636 if self.notifyModeChange:
637 self.doNotifyModeChange(control)
639 if self.notifyStratumChange:
640 self.doNotifyStratumChange(control)
642 if self.notifySyspeerChange:
643 self.doNotifySyspeerChange(control)
645 # Both add and remove have to look at the same data, don't want them
646 # stepping on each other. Therefore the functions are combined.
647 if self.notifyAddAssociation and self.notifyRMAssociation:
648 self.doNotifyChangeAssociation(control, "both")
649 elif self.notifyAddAssociation:
650 self.doNotifyChangeAssociation(control, "add")
651 elif self.notifyRMAssociation:
652 self.doNotifyChangeAssociation(control, "rm")
654 if self.notifyConfigChange:
655 self.doNotifyConfigChange(control)
657 if self.notifyLeapSecondAnnounced:
658 self.doNotifyLeapSecondAnnounced(control)
660 if self.notifyHeartbeat:
661 self.doNotifyHeartbeat(control)
663 def doNotifyModeChange(self, control):
664 oldMode = self.oldValues.get("mode")
665 newMode = self.misc_getMode() # connection failure handled by method
666 if oldMode is None:
667 self.oldValues["mode"] = newMode
668 return
669 elif oldMode != newMode:
670 self.oldValues["mode"] = newMode
671 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
672 ax.OID(ntpRootOID + (0, 1))),
673 ax.Varbind(ax.VALUE_INTEGER, ntpRootOID + (1, 2, 1),
674 newMode)]
675 control.sendNotify(vl)
676 self.sentNotifications += 1
678 def doNotifyStratumChange(self, control):
679 oldStratum = self.oldValues.get("stratum")
680 newStratum = self.safeReadvar(0, ["stratum"])
681 if newStratum is None:
682 return # couldn't read
683 newStratum = newStratum["stratum"]
684 if oldStratum is None:
685 self.oldValues["stratum"] = newStratum
686 return
687 elif oldStratum != newStratum:
688 self.oldValues["stratum"] = newStratum
689 datetime = self.safeReadvar(0, ["reftime"])
690 if datetime is None:
691 datetime = ""
692 else:
693 datetime = ntp.util.deformatNTPTime(datetime["reftime"])
694 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
695 ax.OID(ntpRootOID + (0, 2))),
696 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
697 datetime),
698 ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (1, 2, 2),
699 newStratum),
700 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
701 "Stratum changed")] # Uh... what goes here?
702 control.sendNotify(vl)
703 self.sentNotifications += 1
705 def doNotifySyspeerChange(self, control):
706 oldSyspeer = self.oldValues.get("syspeer")
707 newSyspeer = self.safeReadvar(0, ["peeradr"])
708 if newSyspeer is None:
709 return # couldn't read
710 newSyspeer = newSyspeer["peeradr"]
711 if oldSyspeer is None:
712 self.oldValues["syspeer"] = newSyspeer
713 return
714 elif oldSyspeer != newSyspeer:
715 self.oldValues["syspeer"] = newSyspeer
716 datetime = self.safeReadvar(0, ["reftime"])
717 if datetime is None:
718 datetime = ""
719 else:
720 datetime = ntp.util.deformatNTPTime(datetime["reftime"])
721 syspeer = self.misc_getSyspeerID()
722 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
723 ax.OID(ntpRootOID + (0, 3))),
724 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
725 datetime),
726 ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (1, 2, 3),
727 syspeer),
728 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
729 "SysPeer changed")] # Uh... what goes here?
730 control.sendNotify(vl)
731 self.sentNotifications += 1
733 def doNotifyChangeAssociation(self, control, which):
734 # Add and remove are combined because they use the same data source
735 # and it would be easy to have them stepping on each other.
736 changes = self.misc_getAssocListChanges()
737 if changes is None:
738 return
739 datetime = self.safeReadvar(0, ["reftime"])
740 if datetime is None:
741 datetime = ""
742 else:
743 datetime = ntp.util.deformatNTPTime(datetime["reftime"])
744 adds, rms = changes
745 if which in ("add", "both"):
746 for name in adds:
747 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
748 ax.OID(ntpRootOID + (0, 4))), # Add
749 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
750 datetime),
751 ax.Varbind(ax.VALUE_OCTET_STR,
752 ntpRootOID + (1, 3, 1, 1, 2),
753 name),
754 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
755 "Association added")]
756 control.sendNotify(vl)
757 self.sentNotifications += 1
758 if which in ("rm", "both"):
759 for name in rms:
760 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
761 ax.OID(ntpRootOID + (0, 5))), # Remove
762 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
763 datetime),
764 ax.Varbind(ax.VALUE_OCTET_STR,
765 ntpRootOID + (1, 3, 1, 1, 2),
766 name),
767 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
768 "Association removed")]
769 control.sendNotify(vl)
770 self.sentNotifications += 1
772 def doNotifyConfigChange(self, control):
773 # This left unimplemented because the MIB wants something we can't
774 # and/or shouldn't provide
775 pass
777 def doNotifyLeapSecondAnnounced(self, control):
778 oldLeap = self.oldValues.get("leap")
779 newLeap = self.safeReadvar(0, ["leap"])
780 if newLeap is None:
781 return
782 newLeap = newLeap["leap"]
783 if oldLeap is None:
784 self.oldValues["leap"] = newLeap
785 return
786 if oldLeap != newLeap:
787 self.oldValues["leap"] = newLeap
788 if (oldLeap in (0, 3)) and (newLeap in (1, 2)):
789 # changed noleap or unsync to a leap announcement
790 datetime = self.safeReadvar(0, ["reftime"])
791 if datetime is None:
792 datetime = ""
793 else:
794 datetime = ntp.util.deformatNTPTime(datetime["reftime"])
795 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
796 ax.OID(ntpRootOID + (0, 7))),
797 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
798 datetime),
799 ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
800 "Leap second announced")]
801 control.sendNotify(vl)
802 self.sentNotifications += 1
804 def doNotifyHeartbeat(self, control): # TODO: check if ntpd running?
805 vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
806 ax.OID(ntpRootOID + (0, 8))),
807 ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (0, 1, 4, 1),
808 self.heartbeatInterval)]
809 if self.heartbeatInterval == 0: # interval == 0 means send once
810 self.notifyHeartbeat = False
811 control.sendNotify(vl)
812 self.sentNotifications += 1
813 else:
814 current = ntp.util.monoclock()
815 if (current - self.lastHeartbeat) > self.heartbeatInterval:
816 self.lastHeartbeat = current
817 control.sendNotify(vl)
818 self.sentNotifications += 1
820 # =====================================
821 # Misc data helpers (not part of the MIB proper)
822 # =====================================
824 def misc_loadDynamicSettings(self):
825 if self.settingsFilename is None:
826 return
828 def boolify(d, k):
829 return True if d[k][0][1] == "True" else False
830 optionList = ("notify-mode-change", "notify-stratum-change",
831 "notify-syspeer-change", "notify-add-association",
832 "notify-rm-association", "notify-leap-announced",
833 "notify-heartbeat", "heartbeat-interval")
834 settings = loadSettings(self.settingsFilename, optionList)
835 if settings is None:
836 return
837 for key in settings.keys():
838 if key == "notify-mode-change":
839 self.notifyModeChange = boolify(settings, key)
840 elif key == "notify-stratum-change":
841 self.notifyStratumChange = boolify(settings, key)
842 elif key == "notify-syspeer-change":
843 self.notifySyspeerChange = boolify(settings, key)
844 elif key == "notify-add-association":
845 self.notifyAddAssociation = boolify(settings, key)
846 elif key == "notify-rm-association":
847 self.notifyRMAssociation = boolify(settings, key)
848 elif key == "notify-leap-announced":
849 self.notifyLeapSecondAnnounced = boolify(settings, key)
850 elif key == "notify-heartbeat":
851 self.notifyHeartbeat = boolify(settings, key)
852 elif key == "heartbeat-interval":
853 self.heartbeatInterval = settings[key][0][1]
855 def misc_storeDynamicSettings(self):
856 if self.settingsFilename is None:
857 return
858 settings = {}
859 settings["notify-mode-change"] = str(self.notifyModeChange)
860 settings["notify-stratum-change"] = str(self.notifyStratumChange)
861 settings["notify-syspeer-change"] = str(self.notifySyspeerChange)
862 settings["notify-add-association"] = str(self.notifyAddAssociation)
863 settings["notify-rm-association"] = str(self.notifyRMAssociation)
864 settings["notify-leap-announced"] = str(self.notifyLeapSecondAnnounced)
865 settings["notify-heartbeat"] = str(self.notifyHeartbeat)
866 settings["heartbeat-interval"] = str(self.heartbeatInterval)
867 storeSettings(self.settingsFilename, settings)
869 def misc_getAssocListChanges(self):
870 # We need to keep the names, because those won't be available
871 # after they have been removed.
872 oldAssoc = self.oldValues.get("assoc")
873 newAssoc = {}
874 # Yes, these are cached, for a very short time
875 pdata = self.misc_getPeerData()
876 if pdata is None:
877 return
878 ids = self.misc_getPeerIDs()
879 if ids is None:
880 return
881 for associd in ids:
882 addr = pdata[associd]["srcadr"][1]
883 name = ntp.util.canonicalize_dns(addr)
884 newAssoc[associd] = name
885 if oldAssoc is None:
886 self.oldValues["assoc"] = newAssoc
887 return
888 elif oldAssoc != newAssoc:
889 oldIDs = oldAssoc.keys()
890 newIDs = newAssoc.keys()
891 adds = []
892 rms = []
893 for associd in oldIDs + newIDs:
894 if associd not in newIDs: # removed
895 rms.append(oldAssoc[associd])
896 if associd not in oldIDs: # added
897 adds.append(newAssoc[associd])
898 return (adds, rms)
899 return
901 def misc_getMode(self): # FIXME: not fully implemented
902 try:
903 # Don't care about the data, this is a ploy to get the rstatus
904 self.session.readvar(0, ["stratum"])
905 except ntp.packet.ControlException as e:
906 if e.message == ntp.packet.SERR_SOCKET:
907 # Can't connect, ntpd probably not running
908 return 1
909 else:
910 raise e
911 rstatus = self.session.rstatus # a ploy to get the system status
912 source = ntp.control.CTL_SYS_SOURCE(rstatus)
913 if source == ntp.control.CTL_SST_TS_UNSPEC:
914 mode = 2 # Not yet synced
915 elif False:
916 mode = 3 # No reference configured
917 elif source == ntp.control.CTL_SST_TS_LOCAL:
918 mode = 4 # Distributing local clock (low accuracy)
919 elif source in (ntp.control.CTL_SST_TS_ATOM,
920 ntp.control.CTL_SST_TS_LF,
921 ntp.control.CTL_SST_TS_HF,
922 ntp.control.CTL_SST_TS_UHF):
923 # I am not sure if I should be including the radios in this
924 mode = 5 # Synced to local refclock
925 elif source == ntp.control.CTL_SST_TS_NTP:
926 # Should this include "other"? That covers things like chrony...
927 mode = 6 # Sync to remote NTP
928 else:
929 mode = 99 # Unknown
930 return mode
932 def misc_getSyspeerID(self):
933 peers = self.misc_getPeerData()
934 syspeer = 0
935 for associd in peers.keys():
936 rstatus = peers[associd]["peerstatus"]
937 if (ntp.control.CTL_PEER_STATVAL(rstatus) & 0x7) == \
938 ntp.control.CTL_PST_SEL_SYSPEER:
939 syspeer = associd
940 break
941 return syspeer
943 def safeReadvar(self, associd, variables=None, raw=False):
944 # Use this when we want to catch packet errors, but don't care
945 # about what they are
946 try:
947 return self.session.readvar(associd, varlist=variables, raw=raw)
948 except ntp.packet.ControlException:
949 return None
951 def dynamicCallbackPeerdata(self, variable, raw, valueType):
952 rawindex = 1 if raw else 0
954 def handler(oid, associd):
955 pdata = self.misc_getPeerData()
956 if pdata is None:
957 return None
958 value = pdata[associd][variable][rawindex]
959 return ax.Varbind(valueType, oid, value)
960 return self.dynamicCallbackSkeleton(handler)
962 def dynamicCallbackSkeleton(self, handler):
963 # Build a dynamic MIB tree, installing the provided handler in it
964 def readCallback(oid):
965 # This function assumes that it is a leaf node and that the
966 # last number in the OID is the index.
967 index = oid.subids[-1] # if called properly this works (Ha!)
968 index -= 1 # SNMP reserves index 0, effectively 1-based lists
969 associd = self.misc_getPeerIDs()[index]
970 return handler(oid, associd)
971 subs = {}
972 associds = self.misc_getPeerIDs() # need the peer count
973 for i in range(len(associds)):
974 subs[i+1] = {"reader": readCallback}
975 return subs
977 def readCallbackSkeletonSimple(self, oid, varname, dataType):
978 # Used for entries that just need a simple variable retrevial
979 # but do not need any processing.
980 data = self.safeReadvar(0, [varname])
981 if data is None:
982 return None
983 else:
984 return ax.Varbind(dataType, oid, data[varname])
986 def misc_getPeerIDs(self):
987 peerids = self.cache.get("peerids")
988 if peerids is None:
989 try:
990 peerids = [x.associd for x in self.session.readstat()]
991 except ntp.packet.ControlException:
992 peerids = []
993 peerids.sort()
994 self.cache.set("peerids", peerids)
995 return peerids
997 def misc_getPeerData(self):
998 peerdata = self.cache.get("peerdata")
999 if peerdata is None:
1000 associds = self.misc_getPeerIDs()
1001 peerdata = {}
1002 for aid in associds:
1003 try:
1004 pdata = self.safeReadvar(aid, raw=True)
1005 pdata["peerstatus"] = self.session.rstatus
1006 except IOError:
1007 continue
1008 peerdata[aid] = pdata
1009 self.cache.set("peerdata", peerdata)
1010 return peerdata
1013 def connect(address):
1014 try:
1015 if type(address) is str:
1016 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
1017 sock.connect(address)
1018 else:
1019 host, port = address[0], address[1]
1020 af, _, _, _, _ = socket.getaddrinfo(host, port)[0]
1021 sock = socket.socket(af, socket.SOCK_STREAM)
1022 sock.connect((host, port))
1023 except socket.error as msg:
1024 log("Connection to %s failure: %s" % (repr(address), repr(msg)), 1)
1025 sys.exit(1)
1026 log("connected to master agent at " + repr(address), 3)
1027 return sock
1030 def mainloop(snmpSocket, reconnectionAddr, host=None):
1031 log("initing loop", 3)
1032 dbase = DataSource(host, "/var/ntpsntpd/notify.conf")
1033 while True: # Loop reconnection attempts
1034 control = PacketControl(snmpSocket, dbase, logfp=logfp, debug=debug)
1035 control.loopCallback = dbase.checkNotifications
1036 control.initNewSession()
1037 if not control.mainloop(True): # disconnected
1038 snmpSocket.close()
1039 snmpSocket = connect(reconnectionAddr)
1040 log("disconnected from master, attempting reconnect", 2)
1041 else: # Something else happened
1042 break
1045 def daemonize(runfunc, *runArgs):
1046 pid = os.fork()
1047 if pid < 0:
1048 log("Forking error " + str(pid), 1)
1049 sys.exit(pid)
1050 elif pid > 0: # We are the parent
1051 log("Daemonization success, child pid: " + str(pid), 3)
1052 sys.exit(0)
1054 # We must be the child
1056 os.umask(0)
1058 sid = os.setsid()
1060 # chdir should be here, change to what? root?
1062 global logfp
1063 if logfp == sys.stderr:
1064 logfp = None
1066 sys.stdin.close()
1067 sys.stdin = None
1068 sys.stdout.close()
1069 sys.stdout = None
1070 sys.stderr.close()
1071 sys.stderr = None
1073 runfunc(*runArgs)
1076 def loadSettings(filename, optionList):
1077 log("Loading config file: %s" % filename, 3)
1078 if not os.path.isfile(filename):
1079 return None
1080 options = {}
1081 with open(filename) as f:
1082 data = f.read()
1083 lines = ntp.util.parseConf(data)
1084 for line in lines:
1085 isQuote, token = line[0]
1086 if token in optionList:
1087 options[token] = line[1:]
1088 return options
1091 def storeSettings(filename, settings):
1092 dirname = os.path.dirname(filename)
1093 if not os.path.exists(dirname):
1094 os.makedirs(dirname)
1095 data = []
1096 for key in settings.keys():
1097 data.append("%s %s\n" % (key, settings[key]))
1098 data = "".join(data)
1099 with open(filename, "w") as f:
1100 f.write(data)
1103 usage = """
1104 USAGE: ntpsnmpd [-n] [ntp host]
1105 Flg Arg Option-Name Description
1106 -n no no-fork Do not fork and daemonize.
1107 -x Adr master-addr Specify address for connecting to the master agent
1108 - default /var/agentx/master
1109 -d no debug-level Increase output debug message level
1110 - may appear multiple times
1111 -l Str logfile Logs debug messages to the provided filename
1112 -D Int set-debug-level Set the output debug message level
1113 - may appear multiple times
1114 -h no help Print a usage message.
1115 -V no version Output version information and exit
1119 if __name__ == "__main__":
1120 bin_ver = "ntpsec-@NTPSEC_VERSION_EXTENDED@"
1121 ntp.util.stdversioncheck(bin_ver)
1122 try:
1123 (options, arguments) = getopt.getopt(
1124 sys.argv[1:],
1125 "nx:dD:Vhl:c:",
1126 ["no-fork", "master-address=", "debug-level", "set-debug-level=",
1127 "version", "help", "logfile=", "configfile="])
1128 except getopt.GetoptError as e:
1129 sys.stderr.write("%s\n" % e)
1130 sys.stderr.write(usage)
1131 raise SystemExit(1)
1133 masterAddr = "/var/agentx/master"
1134 logfile = DEFLOG
1135 hostname = DEFHOST
1137 # Check for non-default config-file
1138 conffile = "/etc/ntpsnmpd.conf"
1139 for (switch, val) in options:
1140 if switch in ("-c", "--configfile"):
1141 conffile = val
1142 break
1144 # Load configuration file
1145 conf = loadSettings(conffile,
1146 ("master-addr", "logfile", "loglevel", "ntp-addr"))
1147 if conf is not None:
1148 for key in conf.keys():
1149 if key == "master-addr": # Address of the SNMP master daemon
1150 val = conf[key][0][1]
1151 if ":" in val:
1152 host, port = val.split(":")
1153 port = int(port)
1154 masterAddr = (host, port)
1155 else:
1156 masterAddr = val
1157 elif key == "logfile":
1158 logfile = conf[key][0][1]
1159 elif key == "ntp-addr": # Address of the NTP daemon
1160 hostname = conf[key][0][1]
1161 elif key == "loglevel":
1162 errmsg = "Error: loglevel parameter '%s' not a number\n"
1163 debug = conf[key][0][1]
1165 fileLogging = False
1166 for (switch, val) in options:
1167 if switch in ("-n", "--no-fork"):
1168 nofork = True
1169 elif switch in ("-x", "--master-addr"):
1170 if ":" in val:
1171 host, port = val.split(":")
1172 port = int(port)
1173 masterAddr = (host, port)
1174 else:
1175 masterAddr = val
1176 elif switch in ("-d", "--debug-level"):
1177 debug += 1
1178 elif switch in ("-D", "--set-debug-level"):
1179 errmsg = "Error: -D parameter '%s' not a number\n"
1180 debug = ntp.util.safeargcast(val, int, errmsg, usage)
1181 elif switch in ("-V", "--version"):
1182 print("ntpsnmpd %s" % ntp.util.stdversion())
1183 raise SystemExit(0)
1184 elif switch in ("-h", "--help"):
1185 print(usage)
1186 raise SystemExit(0)
1187 elif switch in ("-l", "--logfile"):
1188 logfile = val
1189 fileLogging = True
1191 if not nofork:
1192 fileLogging = True
1194 if fileLogging:
1195 if logfp != sys.stderr:
1196 logfp.close()
1197 logfp = open(logfile, "a", 1) # 1 => line buffered
1199 hostname = arguments[0] if arguments else DEFHOST
1201 # Connect here so it can always report a connection error
1202 sock = connect(masterAddr)
1204 if nofork:
1205 mainloop(sock, hostname)
1206 else:
1207 daemonize(mainloop, sock, hostname)