2 # -*- coding: utf-8 -*-
4 # Copyright the NTPsec project contributors
6 # SPDX-License-Identifier: BSD-2-Clause
8 from __future__
import print_function
, division
19 import ntp
.agentx_packet
20 ax
= ntp
.agentx_packet
21 from ntp
.agentx
import PacketControl
22 except ImportError as e
:
24 "ntpsnmpd: can't find Python NTP library.\n")
25 sys
.stderr
.write("%s\n" % e
)
29 # TODO This is either necessary, or a different workaround is.
30 ntp
.util
.deunicode_units()
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)
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
52 # OIDs are relative from ntp root
53 ntp
.agentx
.MIBControl
.__init
__(self
, mibRoot
=ntpRootOID
)
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
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
)
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
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
)
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
)
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
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
201 data
= " ".join([uname
[0], uname
[2], uname
[4]])
202 vb
= ax
.Varbind(ax
.VALUE_OCTET_STR
, oid
, data
)
205 def cbr_timeResolution(self
, oid
):
207 # Arrives in fractional milliseconds
208 fuzz
= self
.safeReadvar(0, ["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.
219 return ax
.Varbind(ax
.VALUE_GAUGE32
, oid
, fuzz
)
221 def cbr_timePrecision(self
, oid
):
222 return self
.readCallbackSkeletonSimple(oid
, "precision",
225 def cbr_timeDistance(self
, oid
):
227 data
= self
.safeReadvar(0, ["rootdist"], raw
=True)
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
):
242 return self
.readCallbackSkeletonSimple(oid
, "stratum",
245 def cbr_statusActiveRefSourceID(self
, oid
):
247 syspeer
= self
.misc_getSyspeerID()
248 return ax
.Varbind(ax
.VALUE_GAUGE32
, oid
, syspeer
)
250 def cbr_statusActiveRefSourceName(self
, oid
):
252 data
= self
.safeReadvar(0, ["peeradr"])
255 data
= ntp
.util
.canonicalize_dns(data
["peeradr"])
256 return ax
.Varbind(ax
.VALUE_OCTET_STR
, oid
, data
)
258 def cbr_statusActiveOffset(self
, oid
):
260 data
= self
.safeReadvar(0, ["koffset"], raw
=True)
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
):
270 data
= self
.session
.readstat()
271 return ax
.Varbind(ax
.VALUE_GAUGE32
, oid
, len(data
))
272 except ntp
.packet
.ControlException
:
275 def cbr_statusDispersion(self
, oid
):
277 data
= self
.safeReadvar(0, ["rootdisp"], raw
=True)
280 return ax
.Varbind(ax
.VALUE_OCTET_STR
, oid
, data
["rootdisp"][1])
282 def cbr_statusEntityUptime(self
, oid
):
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
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"])
299 uptime
= uptime
["ss_reset"] * 100
300 return ax
.Varbind(ax
.VALUE_TIME_TICKS
, oid
, uptime
)
302 def cbr_statusDateTime(self
, oid
):
304 data
= self
.safeReadvar(0, ["reftime"])
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
315 data
= self
.safeReadvar(0, ["reftime"])
316 hasleap
= self
.safeReadvar(0, ["leap"])
317 if (data
is None) or (hasleap
is 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)
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
):
333 leap
= self
.safeReadvar(0, ["leap"])
338 pass # leap 1 == forward
340 leap
= -1 # leap 2 == backward
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",
349 def cbr_statusOutPkts(self
, oid
):
350 return self
.readCallbackSkeletonSimple(oid
, "io_sent",
353 def cbr_statusBadVersion(self
, oid
):
354 return self
.readCallbackSkeletonSimple(oid
, "ss_oldver",
357 def cbr_statusProtocolError(self
, oid
):
358 data
= self
.safeReadvar(0, ["ss_badformat", "ss_badauth"])
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 ##############################
384 # assocProtocolErrors
386 #########################
388 def cbr_entHeartbeatInterval(self
, oid
):
390 return ax
.Varbind(ax
.VALUE_GAUGE32
, oid
, self
.heartbeatInterval
)
392 def cbr_entNotifBits(self
, oid
):
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
):
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):
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":
435 def cbw_entNotifBits(self
, action
, varbind
, oldData
=None):
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":
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 # ========================================================================
475 # These are left as stubs for now. Information is lacking on where the
476 # data should come from.
478 def sub_statPktMode(self
):
481 def sub_statPktSent(self
):
484 def sub_statPktRecv(self
):
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,
498 def sub_assocRefID(self
):
499 def handler(oid
, associd
):
500 pdata
= self
.misc_getPeerData()
503 # elaborate code in util.py indicates this may not be stable
505 refid
= pdata
[associd
]["refid"][1]
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()
516 srcadr
= pdata
[associd
]["srcadr"][1]
518 socklen
= len(socket
.getaddrinfo(srcadr
, None)[0][-1])
519 except socket
.gaierror
:
521 if socklen
== 2: # ipv4
523 elif socklen
== 4: # ipv6
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()
537 srcadr
= pdata
[associd
]["srcadr"][1]
538 # WARNING: I am only guessing that this is correct
539 # Discover what type of address we have
541 sockinfo
= socket
.getaddrinfo(srcadr
, None)[0][-1]
543 ipv6
= True if len(sockinfo
) == 4 else False
544 except socket
.gaierror
:
545 addr
= None # how to handle?
547 # Convert address string to octets
550 pieces
= addr
.split(".")
553 srcadr
.append(int(piece
)) # feed it a list of ints
555 # Have gotten piece == "" before. Skip over that.
556 # Still try to return data because it is potential
557 # debugging information.
560 pieces
= addr
.split(":")
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()
574 offset
= pdata
[associd
]["offset"][1]
575 offset
= ntp
.util
.unitifyvar(offset
, "offset", width
=None,
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,
584 def sub_assocJitter(self
):
585 return self
.dynamicCallbackPeerdata("jitter", True,
588 def sub_assocDelay(self
):
589 return self
.dynamicCallbackPeerdata("delay", True,
592 def sub_assocDispersion(self
):
593 return self
.dynamicCallbackPeerdata("rootdisp", True,
596 def sub_assocStatInPkts(self
):
597 def handler(oid
, associd
):
598 inpkts
= self
.safeReadvar(associd
, ["received"])
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"])
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"])
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
:
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
667 self
.oldValues
["mode"] = newMode
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),
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
687 elif oldStratum
!= newStratum
:
688 self
.oldValues
["stratum"] = newStratum
689 datetime
= self
.safeReadvar(0, ["reftime"])
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),
698 ax
.Varbind(ax
.VALUE_GAUGE32
, ntpRootOID
+ (1, 2, 2),
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
714 elif oldSyspeer
!= newSyspeer
:
715 self
.oldValues
["syspeer"] = newSyspeer
716 datetime
= self
.safeReadvar(0, ["reftime"])
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),
726 ax
.Varbind(ax
.VALUE_GAUGE32
, ntpRootOID
+ (1, 2, 3),
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()
739 datetime
= self
.safeReadvar(0, ["reftime"])
743 datetime
= ntp
.util
.deformatNTPTime(datetime
["reftime"])
745 if which
in ("add", "both"):
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),
751 ax
.Varbind(ax
.VALUE_OCTET_STR
,
752 ntpRootOID
+ (1, 3, 1, 1, 2),
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"):
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),
764 ax
.Varbind(ax
.VALUE_OCTET_STR
,
765 ntpRootOID
+ (1, 3, 1, 1, 2),
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
777 def doNotifyLeapSecondAnnounced(self
, control
):
778 oldLeap
= self
.oldValues
.get("leap")
779 newLeap
= self
.safeReadvar(0, ["leap"])
782 newLeap
= newLeap
["leap"]
784 self
.oldValues
["leap"] = newLeap
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"])
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),
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
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:
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
)
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:
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")
874 # Yes, these are cached, for a very short time
875 pdata
= self
.misc_getPeerData()
878 ids
= self
.misc_getPeerIDs()
882 addr
= pdata
[associd
]["srcadr"][1]
883 name
= ntp
.util
.canonicalize_dns(addr
)
884 newAssoc
[associd
] = name
886 self
.oldValues
["assoc"] = newAssoc
888 elif oldAssoc
!= newAssoc
:
889 oldIDs
= oldAssoc
.keys()
890 newIDs
= newAssoc
.keys()
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
])
901 def misc_getMode(self
): # FIXME: not fully implemented
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
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
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
932 def misc_getSyspeerID(self
):
933 peers
= self
.misc_getPeerData()
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
:
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
947 return self
.session
.readvar(associd
, varlist
=variables
, raw
=raw
)
948 except ntp
.packet
.ControlException
:
951 def dynamicCallbackPeerdata(self
, variable
, raw
, valueType
):
952 rawindex
= 1 if raw
else 0
954 def handler(oid
, associd
):
955 pdata
= self
.misc_getPeerData()
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
)
972 associds
= self
.misc_getPeerIDs() # need the peer count
973 for i
in range(len(associds
)):
974 subs
[i
+1] = {"reader": readCallback
}
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
])
984 return ax
.Varbind(dataType
, oid
, data
[varname
])
986 def misc_getPeerIDs(self
):
987 peerids
= self
.cache
.get("peerids")
990 peerids
= [x
.associd
for x
in self
.session
.readstat()]
991 except ntp
.packet
.ControlException
:
994 self
.cache
.set("peerids", peerids
)
997 def misc_getPeerData(self
):
998 peerdata
= self
.cache
.get("peerdata")
1000 associds
= self
.misc_getPeerIDs()
1002 for aid
in associds
:
1004 pdata
= self
.safeReadvar(aid
, raw
=True)
1005 pdata
["peerstatus"] = self
.session
.rstatus
1008 peerdata
[aid
] = pdata
1009 self
.cache
.set("peerdata", peerdata
)
1013 def connect(address
):
1015 if type(address
) is str:
1016 sock
= socket
.socket(socket
.AF_UNIX
, socket
.SOCK_STREAM
)
1017 sock
.connect(address
)
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)
1026 log("connected to master agent at " + repr(address
), 3)
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
1039 snmpSocket
= connect(reconnectionAddr
)
1040 log("disconnected from master, attempting reconnect", 2)
1041 else: # Something else happened
1045 def daemonize(runfunc
, *runArgs
):
1048 log("Forking error " + str(pid
), 1)
1050 elif pid
> 0: # We are the parent
1051 log("Daemonization success, child pid: " + str(pid
), 3)
1054 # We must be the child
1060 # chdir should be here, change to what? root?
1063 if logfp
== sys
.stderr
:
1076 def loadSettings(filename
, optionList
):
1077 log("Loading config file: %s" % filename
, 3)
1078 if not os
.path
.isfile(filename
):
1081 with
open(filename
) as f
:
1083 lines
= ntp
.util
.parseConf(data
)
1085 isQuote
, token
= line
[0]
1086 if token
in optionList
:
1087 options
[token
] = line
[1:]
1091 def storeSettings(filename
, settings
):
1092 dirname
= os
.path
.dirname(filename
)
1093 if not os
.path
.exists(dirname
):
1094 os
.makedirs(dirname
)
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
:
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
)
1123 (options
, arguments
) = getopt
.getopt(
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
)
1133 masterAddr
= "/var/agentx/master"
1137 # Check for non-default config-file
1138 conffile
= "/etc/ntpsnmpd.conf"
1139 for (switch
, val
) in options
:
1140 if switch
in ("-c", "--configfile"):
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]
1152 host
, port
= val
.split(":")
1154 masterAddr
= (host
, port
)
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]
1166 for (switch
, val
) in options
:
1167 if switch
in ("-n", "--no-fork"):
1169 elif switch
in ("-x", "--master-addr"):
1171 host
, port
= val
.split(":")
1173 masterAddr
= (host
, port
)
1176 elif switch
in ("-d", "--debug-level"):
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())
1184 elif switch
in ("-h", "--help"):
1187 elif switch
in ("-l", "--logfile"):
1195 if logfp
!= sys
.stderr
:
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
)
1205 mainloop(sock
, hostname
)
1207 daemonize(mainloop
, sock
, hostname
)