File: //usr/bin/ntpsnmpd
#! /usr/bin/python3 -s
# -*- coding: utf-8 -*-
from __future__ import print_function, division
import sys
import os
import getopt
import time
import socket
import subprocess
try:
import ntp.packet
import ntp.util
import ntp.agentx_packet
ax = ntp.agentx_packet
from ntp.agentx import PacketControl
except ImportError as e:
sys.stderr.write(
"ntpsnmpd: can't find Python NTP library.\n")
sys.stderr.write("%s\n" % e)
sys.exit(1)
# TODO This is either necessary, or a different workaround is.
ntp.util.deunicode_units()
logfp = sys.stderr
nofork = False
debug = 0
defaultTimeout = 30
log = (lambda msg, msgdbg: ntp.util.dolog(logfp, msg, debug, msgdbg))
ntpRootOID = (1, 3, 6, 1, 2, 1, 197) # mib-2 . 197, aka: NTPv4-MIB
snmpTrapOID = (1, 3, 6, 1, 6, 3, 1, 1, 4, 1, 0)
snmpSysUptime = (1, 3, 6, 1, 2, 1, 1, 3, 0)
DEFHOST = "localhost"
DEFLOG = "ntpsnmpd.log"
class DataSource(ntp.agentx.MIBControl):
def __init__(self, hostname=DEFHOST, settingsFile=None, notifySpin=0.1):
# This is defined as a dict tree because it is simpler, and avoids
# certain edge cases
# OIDs are relative from ntp root
ntp.agentx.MIBControl.__init__(self, mibRoot=ntpRootOID)
# MIB node init
# block 0
self.addNode((0,)) # ntpEntNotifications
self.addNode((0, 1)) # ntpEntNotifModeChange
self.addNode((0, 2)) # ntpEntNotifStratumChange
self.addNode((0, 3)) # ntpEntNotifSyspeerChange
self.addNode((0, 4)) # ntpEntNotifAddAssociation
self.addNode((0, 5)) # ntpEntNotifRemoveAsociation
self.addNode((0, 6)) # ntpEntNotifConfigChanged
self.addNode((0, 7)) # ntpEntNotifLeapSecondAnnounced
self.addNode((0, 8)) # ntpEntNotifHeartbeat
# block 1
# block 1, 1
self.addNode((1, 1, 1, 0), # ntpNetSoftwareName utf8str
(lambda oid: self.cbr_systemInfo(oid, "name")))
self.addNode((1, 1, 2, 0), # ntpEntSoftwareVersion utf8str
(lambda oid: self.cbr_systemInfo(oid, "version")))
self.addNode((1, 1, 3, 0), # ntpEntSoftwareVendor utf8str
(lambda oid: self.cbr_systemInfo(oid, "vendor")))
self.addNode((1, 1, 4, 0), # ntpEntSystemType utf8str
(lambda oid: self.cbr_systemInfo(oid, "system")))
self.addNode((1, 1, 5, 0), # ntpEntTimeResolution uint32
self.cbr_timeResolution)
self.addNode((1, 1, 6, 0), # ntpEntTimePrecision int32
self.cbr_timePrecision)
self.addNode((1, 1, 7, 0), # ntpEntTimeDistance DisplayString
self.cbr_timeDistance)
# block 1, 2
self.addNode((1, 2, 1, 0), # ntpEntStatusCurrentMode INTEGER {...}
self.cbr_statusCurrentMode)
self.addNode((1, 2, 2, 0), # ntpEntStatusStratum NtpStratum
self.cbr_statusStratum)
self.addNode((1, 2, 3, 0), # ntpEntStatusActiveRefSourceId uint32
self.cbr_statusActiveRefSourceID)
self.addNode((1, 2, 4, 0), # ntpEntStatusActiveRefSourceName utf8str
self.cbr_statusActiveRefSourceName)
self.addNode((1, 2, 5, 0), # ntpEntStatusActiveOffset DisplayString
self.cbr_statusActiveOffset)
self.addNode((1, 2, 6, 0), # ntpEntStatusNumberOfRefSources unit32
self.cbr_statusNumRefSources)
self.addNode((1, 2, 7, 0), # ntpEntStatusDispersion DisplayString
self.cbr_statusDispersion)
self.addNode((1, 2, 8, 0), # ntpEntStatusEntityUptime TimeTicks
self.cbr_statusEntityUptime)
self.addNode((1, 2, 9, 0), # ntpEntStatusDateTime NtpDateTime
self.cbr_statusDateTime)
self.addNode((1, 2, 10, 0), # ntpEntStatusLeapSecond NtpDateTime
self.cbr_statusLeapSecond)
self.addNode((1, 2, 11, 0), # ntpEntStatusLeapSecondDirection int32
self.cbr_statusLeapSecDirection)
self.addNode((1, 2, 12, 0), # ntpEntStatusInPkts Counter32
self.cbr_statusInPkts)
self.addNode((1, 2, 13, 0), # ntpEntStatusOutPkts Counter32
self.cbr_statusOutPkts)
self.addNode((1, 2, 14, 0), # ntpEntStatusBadVersion Counter32
self.cbr_statusBadVersion)
self.addNode((1, 2, 15, 0), # ntpEntStatusProtocolError Counter32
self.cbr_statusProtocolError)
self.addNode((1, 2, 16, 0), # ntpEntStatusNotifications Counter32
self.cbr_statusNotifications)
self.addNode((1, 2, 17, 1, 1)) # ntpEntStatPktMode INTEGER {...}
self.addNode((1, 2, 17, 1, 2)) # ntpEntStatPktSent Counter32
self.addNode((1, 2, 17, 1, 3)) # ntpEntStatPktRecived Counter32
# block 1, 3
self.addNode((1, 3, 1, 1, 1), # ntpAssocId uint32 (1..99999)
dynamic=self.sub_assocID)
self.addNode((1, 3, 1, 1, 2), # ntpAssocName utf8str
dynamic=self.sub_assocName)
self.addNode((1, 3, 1, 1, 3), # ntpAssocRefId DisplayString
dynamic=self.sub_assocRefID)
self.addNode((1, 3, 1, 1, 4), # ntpAssocAddressType InetAddressType
dynamic=self.sub_assocAddrType)
self.addNode((1, 3, 1, 1, 5), # ntpAssocAddress InetAddress SIZE
dynamic=self.sub_assocAddr)
self.addNode((1, 3, 1, 1, 6), # ntpAssocOffset DisplayString
dynamic=self.sub_assocOffset)
self.addNode((1, 3, 1, 1, 7), # ntpAssocStratum NtpStratum
dynamic=self.sub_assocStratum)
self.addNode((1, 3, 1, 1, 8), # ntpAssocStatusJitter DisplayString
dynamic=self.sub_assocJitter)
self.addNode((1, 3, 1, 1, 9), # ntpAssocStatusDelay DisplayString
dynamic=self.sub_assocDelay)
self.addNode((1, 3, 1, 1, 10), # ntpAssocStatusDispersion DisplayStr
dynamic=self.sub_assocDispersion)
self.addNode((1, 3, 2, 1, 1), # ntpAssocStatInPkts Counter32
dynamic=self.sub_assocStatInPkts)
self.addNode((1, 3, 2, 1, 2), # ntpAssocStatOutPkts Counter32
dynamic=self.sub_assocStatOutPkts)
self.addNode((1, 3, 2, 1, 3), # ntpAssocStatProtocolError Counter32
dynamic=self.sub_assocStatProtoErr)
# block 1, 4
self.addNode((1, 4, 1, 0), # ntpEntHeartbeatInterval unit32
self.cbr_entHeartbeatInterval,
self.cbw_entHeartbeatInterval)
self.addNode((1, 4, 2, 0), # ntpEntNotifBits BITS {...}
self.cbr_entNotifBits,
self.cbw_entNotifBits)
# block 1, 5
self.addNode((1, 5, 1, 0), # ntpEntNotifMessage utf8str
self.cbr_entNotifMessage)
# block 2 # all compliance statements
# print(repr(self.oidTree))
# print(self.oidTree[1]["subids"][1][1][0])
self.session = ntp.packet.ControlSession()
self.hostname = hostname if hostname else DEFHOST
self.session.openhost(self.hostname)
self.settingsFilename = settingsFile
# Cache so we don't hammer ntpd, default 1 second timeout
# Timeout default pulled from a hat: we don't want it to last for
# long, just not flood ntpd with duplicatte requests during a walk.
self.cache = ntp.util.Cache(1)
self.oldValues = {} # Used by notifications to detect changes
# spinGap so we don't spam ntpd with requests during notify checks
self.notifySpinTime = notifySpin
self.lastNotifyCheck = 0
self.lastHeartbeat = 0 # Timestamp used for heartbeat notifications
self.heartbeatInterval = 0 # should save to disk
self.sentNotifications = 0
# Notify bits, they control whether the daemon sends notifications.
# these are saved to disk
self.notifyModeChange = False # 1
self.notifyStratumChange = False # 2
self.notifySyspeerChange = False # 3
self.notifyAddAssociation = False # 4
self.notifyRMAssociation = False # 5
self.notifyConfigChange = False # 6 [This is not implemented]
self.notifyLeapSecondAnnounced = False # 7
self.notifyHeartbeat = False # 8
self.misc_loadDynamicSettings()
# =============================================================
# Data read callbacks start here
# comment divider lines represent not yet implemented callbacks
# =============================================================
# Blank: notification OIDs
def cbr_systemInfo(self, oid, category=None):
if category == "name": # The product name of the running NTP
data = "NTPsec"
elif category == "version": # version string
data = ntp.util.stdversion()
elif category == "vendor": # vendor/author name
data = "Internet Civil Engineering Institute"
elif category == "system": # system / hardware info
proc = subprocess.Popen(["uname", "-srm"],
stdout=subprocess.PIPE)
data = proc.communicate()[0]
vb = ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
return vb
def cbr_timeResolution(self, oid):
# Uinteger32
# Arrives in fractional milliseconds
fuzz = self.safeReadvar(0, ["fuzz"])
if fuzz is None:
return None
fuzz = fuzz["fuzz"]
# We want to emit fractions of seconds
# Yes we are flooring instead of rounding: don't want to emit a
# resolution value higher than ntpd actually produces.
if fuzz != 0:
fuzz = int(1 / fuzz)
else:
fuzz = 0
return ax.Varbind(ax.VALUE_GAUGE32, oid, fuzz)
def cbr_timePrecision(self, oid):
return self.readCallbackSkeletonSimple(oid, "precision",
ax.VALUE_INTEGER)
def cbr_timeDistance(self, oid):
# Displaystring
data = self.safeReadvar(0, ["rootdist"], raw=True)
if data is None:
return None
data = ntp.util.unitifyvar(data["rootdist"][1], "rootdist",
width=None, unitSpace=True)
return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
# Blank: ntpEntStatus
def cbr_statusCurrentMode(self, oid):
mode = self.misc_getMode()
return ax.Varbind(ax.VALUE_INTEGER, oid, mode)
def cbr_statusStratum(self, oid):
# NTPstratum
return self.readCallbackSkeletonSimple(oid, "stratum",
ax.VALUE_GAUGE32)
def cbr_statusActiveRefSourceID(self, oid):
# range of uint32
syspeer = self.misc_getSyspeerID()
return ax.Varbind(ax.VALUE_GAUGE32, oid, syspeer)
def cbr_statusActiveRefSourceName(self, oid):
# utf8
data = self.safeReadvar(0, ["peeradr"])
if data is None:
return None
data = ntp.util.canonicalize_dns(data["peeradr"])
return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
def cbr_statusActiveOffset(self, oid):
# DisplayString
data = self.safeReadvar(0, ["koffset"], raw=True)
if data is None:
return None
data = ntp.util.unitifyvar(data["koffset"][1], "koffset",
width=None, unitSpace=True)
return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
def cbr_statusNumRefSources(self, oid):
# range of uint32
try:
data = self.session.readstat()
return ax.Varbind(ax.VALUE_GAUGE32, oid, len(data))
except ntp.packet.ControlException:
return None
def cbr_statusDispersion(self, oid):
# DisplayString
data = self.safeReadvar(0, ["rootdisp"], raw=True)
if data is None:
return None
return ax.Varbind(ax.VALUE_OCTET_STR, oid, data["rootdisp"][1])
def cbr_statusEntityUptime(self, oid):
# TimeTicks
# What the spec claims:
# The uptime of the NTP entity, (i.e., the time since ntpd was
# (re-)initialized not sysUptime!). The time is represented in
# hundreds of seconds since Jan 1, 1970 (00:00:00.000) UTC.
#
# First problem: TimeTicks represents hundred*ths* of seconds, could
# easily be a typo.
# Second problem: snmpwalk will happily give you a display of
# how long a period of time a value is, such as uptime since start.
# That is the opposite of what the spec claims.
#
# I am abandoning the spec, and going with what makes a lick of sense
uptime = self.safeReadvar(0, ["ss_reset"])
if uptime is None:
return None
uptime = uptime["ss_reset"] * 100
return ax.Varbind(ax.VALUE_TIME_TICKS, oid, uptime)
def cbr_statusDateTime(self, oid):
# NtpDateTime
data = self.safeReadvar(0, ["reftime"])
if data is None:
return None
txt = data["reftime"]
value = ntp.util.deformatNTPTime(txt)
return ax.Varbind(ax.VALUE_OCTET_STR, oid, value)
def cbr_statusLeapSecond(self, oid): # I am not confident in this yet
# NtpDateTime
DAY = 86400
fmt = "%.8x%.8x"
data = self.safeReadvar(0, ["reftime"])
hasleap = self.safeReadvar(0, ["leap"])
if (data is None) or (hasleap is None):
return None
data = data["reftime"]
hasleap = hasleap["leap"]
if hasleap in (1, 2):
seconds = int(data.split(".")[0], 0)
days = seconds // DAY
scheduled = (days * DAY) + (DAY - 1) # 23:59:59 of $CURRENT_DAY
formatted = fmt % (scheduled, 0)
else:
formatted = fmt % (0, 0)
value = ntp.util.hexstr2octets(formatted)
return ax.Varbind(ax.VALUE_OCTET_STR, oid, value)
def cbr_statusLeapSecDirection(self, oid):
# range of int32
leap = self.safeReadvar(0, ["leap"])
if leap is None:
return None
leap = leap["leap"]
if leap == 1:
pass # leap 1 == forward
elif leap == 2:
leap = -1 # leap 2 == backward
else:
leap = 0 # leap 0 or 3 == no change
return ax.Varbind(ax.VALUE_INTEGER, oid, leap)
def cbr_statusInPkts(self, oid):
return self.readCallbackSkeletonSimple(oid, "io_received",
ax.VALUE_COUNTER32)
def cbr_statusOutPkts(self, oid):
return self.readCallbackSkeletonSimple(oid, "io_sent",
ax.VALUE_COUNTER32)
def cbr_statusBadVersion(self, oid):
return self.readCallbackSkeletonSimple(oid, "ss_oldver",
ax.VALUE_COUNTER32)
def cbr_statusProtocolError(self, oid):
data = self.safeReadvar(0, ["ss_badformat", "ss_badauth"])
if data is None:
return None
protoerr = 0
for key in data.keys():
protoerr += data[key]
return ax.Varbind(ax.VALUE_COUNTER32, oid, protoerr)
def cbr_statusNotifications(self, oid):
return ax.Varbind(ax.VALUE_COUNTER32, oid, self.sentNotifications)
##############################
# == Dynamics ==
# assocID
# assocName
# assocRefID
# assocAddrType
# assocAddr
# assocOffset
# assocStratum
# assocJitter
# assocDelay
# assocDispersion
# assocInPackets
# assocOutPackets
# assocProtocolErrors
#########################
def cbr_entHeartbeatInterval(self, oid):
# uint32
return ax.Varbind(ax.VALUE_GAUGE32, oid, self.heartbeatInterval)
def cbr_entNotifBits(self, oid):
# BITS
data = ax.bools2Bits((False, # notUsed(0)
self.notifyModeChange,
self.notifyStratumChange,
self.notifySyspeerChange,
self.notifyAddAssociation,
self.notifyRMAssociation,
self.notifyConfigChange,
self.notifyLeapSecondAnnounced,
self.notifyHeartbeat))
return ax.Varbind(ax.VALUE_OCTET_STR, oid, data)
##########################
def cbr_entNotifMessage(self, oid):
# utf8str
return ax.Varbind(ax.VALUE_OCTET_STR, oid, "no event")
#########################
# =====================================
# Data write callbacks
# Returns an error value (or noError)
# Must check that the value is correct for the bind, this does not mean
# the type: the master agent handles that
# Actions: test, undo, commit, cleanup
# =====================================
def cbw_entHeartbeatInterval(self, action, varbind, oldData=None):
if action == "test":
return ax.ERR_NOERROR
elif action == "commit":
self.heartbeatInterval = varbind.payload
self.misc_storeDynamicSettings()
return ax.ERR_NOERROR
elif action == "undo":
self.heartbeatInterval = oldData
self.misc_storeDynamicSettings()
return ax.ERR_NOERROR
elif action == "cleanup":
pass
def cbw_entNotifBits(self, action, varbind, oldData=None):
if action == "test":
return ax.ERR_NOERROR
elif action == "commit":
(self.notifyModeChange,
self.notifyStratumChange,
self.notifySyspeerChange,
self.notifyAddAssociation,
self.notifyRMAssociation,
self.notifyConfigChange,
self.notifyLeapSecondAnnounced,
self.notifyHeartbeat) = ax.bits2Bools(varbind.payload, 8)
self.misc_storeDynamicSettings()
return ax.ERR_NOERROR
elif action == "undo":
(self.notifyModeChange,
self.notifyStratumChange,
self.notifySyspeerChange,
self.notifyAddAssociation,
self.notifyRMAssociation,
self.notifyConfigChange,
self.notifyLeapSecondAnnounced,
self.notifyHeartbeat) = ax.bits2Bools(oldData, 8)
self.misc_storeDynamicSettings()
return ax.ERR_NOERROR
elif action == "cleanup":
pass
# ========================================================================
# Dynamic tree generator callbacks
#
# The structure of these callbacks is somewhat complicated because they
# share code that is potentially finicky.
#
# The dynamicCallbackSkeleton() method handles the construction of the
# MIB tree, and the placement of the handler() within it. It also provides
# some useful data to the handler() via the readCallback() layer.
# ========================================================================
# Packet Mode Table
# These are left as stubs for now. Information is lacking on where the
# data should come from.
def sub_statPktMode(self):
pass
def sub_statPktSent(self):
pass
def sub_statPktRecv(self):
pass
# Association Table
def sub_assocID(self):
def handler(oid, associd):
return ax.Varbind(ax.VALUE_GAUGE32, oid, associd)
return self.dynamicCallbackSkeleton(handler)
def sub_assocName(self):
return self.dynamicCallbackPeerdata("srcadr", True,
ax.VALUE_OCTET_STR)
def sub_assocRefID(self):
def handler(oid, associd):
pdata = self.misc_getPeerData()
if pdata is None:
return None
# elaborate code in util.py indicates this may not be stable
try:
refid = pdata[associd]["refid"][1]
except IndexError:
refid = ""
return ax.Varbind(ax.VALUE_OCTET_STR, oid, refid)
return self.dynamicCallbackSkeleton(handler)
def sub_assocAddrType(self):
def handler(oid, associd):
pdata = self.misc_getPeerData()
if pdata is None:
return None
srcadr = pdata[associd]["srcadr"][1]
try:
socklen = len(socket.getaddrinfo(srcadr, None)[0][-1])
except socket.gaierror:
socklen = None
if socklen == 2: # ipv4
addrtype = 1
elif socklen == 4: # ipv6
addrtype = 2
else:
# there is also ipv4z and ipv6z..... don't know how to
# detect those yet. Or if I even need to.
addrtype = 0 # is this ok? or should it return a NULL?
return ax.Varbind(ax.VALUE_INTEGER, oid, addrtype)
return self.dynamicCallbackSkeleton(handler)
def sub_assocAddr(self):
def handler(oid, associd):
pdata = self.misc_getPeerData()
if pdata is None:
return None
srcadr = pdata[associd]["srcadr"][1]
# WARNING: I am only guessing that this is correct
# Discover what type of address we have
try:
sockinfo = socket.getaddrinfo(srcadr, None)[0][-1]
addr = sockinfo[0]
ipv6 = True if len(sockinfo) == 4 else False
except socket.gaierror:
addr = None # how to handle?
ipv6 = None
# Convert address string to octets
srcadr = []
if not ipv6:
pieces = addr.split(".")
for piece in pieces:
try:
srcadr.append(int(piece)) # feed it a list of ints
except ValueError:
# Have gotten piece == "" before. Skip over that.
# Still try to return data because it is potential
# debugging information.
continue
elif ipv6:
pieces = addr.split(":")
for piece in pieces:
srcadr.append(ntp.util.hexstr2octets(piece))
srcadr = "".join(srcadr) # feed it an octet string
# The octet string encoder can handle either chars or 0-255
# ints. We use both of those options.
return ax.Varbind(ax.VALUE_OCTET_STR, oid, srcadr)
return self.dynamicCallbackSkeleton(handler)
def sub_assocOffset(self):
def handler(oid, associd):
pdata = self.misc_getPeerData()
if pdata is None:
return None
offset = pdata[associd]["offset"][1]
offset = ntp.util.unitifyvar(offset, "offset", width=None,
unitSpace=True)
return ax.Varbind(ax.VALUE_OCTET_STR, oid, offset)
return self.dynamicCallbackSkeleton(handler)
def sub_assocStratum(self):
return self.dynamicCallbackPeerdata("stratum", False,
ax.VALUE_GAUGE32)
def sub_assocJitter(self):
return self.dynamicCallbackPeerdata("jitter", True,
ax.VALUE_OCTET_STR)
def sub_assocDelay(self):
return self.dynamicCallbackPeerdata("delay", True,
ax.VALUE_OCTET_STR)
def sub_assocDispersion(self):
return self.dynamicCallbackPeerdata("rootdisp", True,
ax.VALUE_OCTET_STR)
def sub_assocStatInPkts(self):
def handler(oid, associd):
inpkts = self.safeReadvar(associd, ["received"])
if inpkts is None:
return None
inpkts = inpkts["received"]
return ax.Varbind(ax.VALUE_COUNTER32, oid, inpkts)
return self.dynamicCallbackSkeleton(handler)
def sub_assocStatOutPkts(self):
def handler(oid, associd):
outpkts = self.safeReadvar(associd, ["sent"])
if outpkts is None:
return None
outpkts = outpkts["sent"]
return ax.Varbind(ax.VALUE_COUNTER32, oid, outpkts)
return self.dynamicCallbackSkeleton(handler)
def sub_assocStatProtoErr(self):
def handler(oid, associd):
pvars = self.safeReadvar(associd, ["badauth", "bogusorg",
"seldisp", "selbroken"])
if pvars is None:
return None
protoerr = 0
for key in pvars.keys():
protoerr += pvars[key]
return ax.Varbind(ax.VALUE_COUNTER32, oid, protoerr)
return self.dynamicCallbackSkeleton(handler)
# =====================================
# Notification handlers
# =====================================
def checkNotifications(self, control):
currentTime = time.time()
if (currentTime - self.lastNotifyCheck) < self.notifySpinTime:
return
self.lastNotifyCheck = currentTime
if self.notifyModeChange:
self.doNotifyModeChange(control)
if self.notifyStratumChange:
self.doNotifyStratumChange(control)
if self.notifySyspeerChange:
self.doNotifySyspeerChange(control)
# Both add and remove have to look at the same data, don't want them
# stepping on each other. Therefore the functions are combined.
if self.notifyAddAssociation and self.notifyRMAssociation:
self.doNotifyChangeAssociation(control, "both")
elif self.notifyAddAssociation:
self.doNotifyChangeAssociation(control, "add")
elif self.notifyRMAssociation:
self.doNotifyChangeAssociation(control, "rm")
if self.notifyConfigChange:
self.doNotifyConfigChange(control)
if self.notifyLeapSecondAnnounced:
self.doNotifyLeapSecondAnnounced(control)
if self.notifyHeartbeat:
self.doNotifyHeartbeat(control)
def doNotifyModeChange(self, control):
oldMode = self.oldValues.get("mode")
newMode = self.misc_getMode() # connection failure handled by method
if oldMode is None:
self.oldValues["mode"] = newMode
return
elif oldMode != newMode:
self.oldValues["mode"] = newMode
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 1))),
ax.Varbind(ax.VALUE_INTEGER, ntpRootOID + (1, 2, 1),
newMode)]
control.sendNotify(vl)
self.sentNotifications += 1
def doNotifyStratumChange(self, control):
oldStratum = self.oldValues.get("stratum")
newStratum = self.safeReadvar(0, ["stratum"])
if newStratum is None:
return # couldn't read
newStratum = newStratum["stratum"]
if oldStratum is None:
self.oldValues["stratum"] = newStratum
return
elif oldStratum != newStratum:
self.oldValues["stratum"] = newStratum
datetime = self.safeReadvar(0, ["reftime"])
if datetime is None:
datetime = ""
else:
datetime = ntp.util.deformatNTPTime(datetime["reftime"])
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 2))),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
datetime),
ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (1, 2, 2),
newStratum),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
"Stratum changed")] # Uh... what goes here?
control.sendNotify(vl)
self.sentNotifications += 1
def doNotifySyspeerChange(self, control):
oldSyspeer = self.oldValues.get("syspeer")
newSyspeer = self.safeReadvar(0, ["peeradr"])
if newSyspeer is None:
return # couldn't read
newSyspeer = newSyspeer["peeradr"]
if oldSyspeer is None:
self.oldValues["syspeer"] = newSyspeer
return
elif oldSyspeer != newSyspeer:
self.oldValues["syspeer"] = newSyspeer
datetime = self.safeReadvar(0, ["reftime"])
if datetime is None:
datetime = ""
else:
datetime = ntp.util.deformatNTPTime(datetime["reftime"])
syspeer = self.misc_getSyspeerID()
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 3))),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
datetime),
ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (1, 2, 3),
syspeer),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
"SysPeer changed")] # Uh... what goes here?
control.sendNotify(vl)
self.sentNotifications += 1
def doNotifyChangeAssociation(self, control, which):
# Add and remove are combined because they use the same data source
# and it would be easy to have them stepping on each other.
changes = self.misc_getAssocListChanges()
if changes is None:
return
datetime = self.safeReadvar(0, ["reftime"])
if datetime is None:
datetime = ""
else:
datetime = ntp.util.deformatNTPTime(datetime["reftime"])
adds, rms = changes
if which in ("add", "both"):
for name in adds:
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 4))), # Add
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
datetime),
ax.Varbind(ax.VALUE_OCTET_STR,
ntpRootOID + (1, 3, 1, 1, 2),
name),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
"Association added")]
control.sendNotify(vl)
self.sentNotifications += 1
if which in ("rm", "both"):
for name in rms:
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 5))), # Remove
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
datetime),
ax.Varbind(ax.VALUE_OCTET_STR,
ntpRootOID + (1, 3, 1, 1, 2),
name),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
"Association removed")]
control.sendNotify(vl)
self.sentNotifications += 1
def doNotifyConfigChange(self, control):
# This left unimplemented because the MIB wants something we can't
# and/or shouldn't provide
pass
def doNotifyLeapSecondAnnounced(self, control):
oldLeap = self.oldValues.get("leap")
newLeap = self.safeReadvar(0, ["leap"])
if newLeap is None:
return
newLeap = newLeap["leap"]
if oldLeap is None:
self.oldValues["leap"] = newLeap
return
if oldLeap != newLeap:
self.oldValues["leap"] = newLeap
if (oldLeap in (0, 3)) and (newLeap in (1, 2)):
# changed noleap or unsync to a leap announcement
datetime = self.safeReadvar(0, ["reftime"])
if datetime is None:
datetime = ""
else:
datetime = ntp.util.deformatNTPTime(datetime["reftime"])
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 7))),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 2, 9),
datetime),
ax.Varbind(ax.VALUE_OCTET_STR, ntpRootOID + (1, 5, 1),
"Leap second announced")]
control.sendNotify(vl)
self.sentNotifications += 1
def doNotifyHeartbeat(self, control): # TODO: check if ntpd running?
vl = [ax.Varbind(ax.VALUE_OID, snmpTrapOID,
ax.OID(ntpRootOID + (0, 8))),
ax.Varbind(ax.VALUE_GAUGE32, ntpRootOID + (0, 1, 4, 1),
self.heartbeatInterval)]
if self.heartbeatInterval == 0: # interval == 0 means send once
self.notifyHeartbeat = False
control.sendNotify(vl)
self.sentNotifications += 1
else:
current = ntp.util.monoclock()
if (current - self.lastHeartbeat) > self.heartbeatInterval:
self.lastHeartbeat = current
control.sendNotify(vl)
self.sentNotifications += 1
# =====================================
# Misc data helpers (not part of the MIB proper)
# =====================================
def misc_loadDynamicSettings(self):
if self.settingsFilename is None:
return
def boolify(d, k):
return True if d[k][0][1] == "True" else False
optionList = ("notify-mode-change", "notify-stratum-change",
"notify-syspeer-change", "notify-add-association",
"notify-rm-association", "notify-leap-announced",
"notify-heartbeat", "heartbeat-interval")
settings = loadSettings(self.settingsFilename, optionList)
if settings is None:
return
for key in settings.keys():
if key == "notify-mode-change":
self.notifyModeChange = boolify(settings, key)
elif key == "notify-stratum-change":
self.notifyStratumChange = boolify(settings, key)
elif key == "notify-syspeer-change":
self.notifySyspeerChange = boolify(settings, key)
elif key == "notify-add-association":
self.notifyAddAssociation = boolify(settings, key)
elif key == "notify-rm-association":
self.notifyRMAssociation = boolify(settings, key)
elif key == "notify-leap-announced":
self.notifyLeapSecondAnnounced = boolify(settings, key)
elif key == "notify-heartbeat":
self.notifyHeartbeat = boolify(settings, key)
elif key == "heartbeat-interval":
self.heartbeatInterval = settings[key][0][1]
def misc_storeDynamicSettings(self):
if self.settingsFilename is None:
return
settings = {}
settings["notify-mode-change"] = str(self.notifyModeChange)
settings["notify-stratum-change"] = str(self.notifyStratumChange)
settings["notify-syspeer-change"] = str(self.notifySyspeerChange)
settings["notify-add-association"] = str(self.notifyAddAssociation)
settings["notify-rm-association"] = str(self.notifyRMAssociation)
settings["notify-leap-announced"] = str(self.notifyLeapSecondAnnounced)
settings["notify-heartbeat"] = str(self.notifyHeartbeat)
settings["heartbeat-interval"] = str(self.heartbeatInterval)
storeSettings(self.settingsFilename, settings)
def misc_getAssocListChanges(self):
# We need to keep the names, because those won't be available
# after they have been removed.
oldAssoc = self.oldValues.get("assoc")
newAssoc = {}
# Yes, these are cached, for a very short time
pdata = self.misc_getPeerData()
if pdata is None:
return
ids = self.misc_getPeerIDs()
if ids is None:
return
for associd in ids:
addr = pdata[associd]["srcadr"][1]
name = ntp.util.canonicalize_dns(addr)
newAssoc[associd] = name
if oldAssoc is None:
self.oldValues["assoc"] = newAssoc
return
elif oldAssoc != newAssoc:
oldIDs = oldAssoc.keys()
newIDs = newAssoc.keys()
adds = []
rms = []
for associd in oldIDs + newIDs:
if associd not in newIDs: # removed
rms.append(oldAssoc[associd])
if associd not in oldIDs: # added
adds.append(newAssoc[associd])
return (adds, rms)
return
def misc_getMode(self): # FIXME: not fully implemented
try:
# Don't care about the data, this is a ploy to get the rstatus
self.session.readvar(0, ["stratum"])
except ntp.packet.ControlException as e:
if e.message == ntp.packet.SERR_SOCKET:
# Can't connect, ntpd probably not running
return 1
else:
raise e
rstatus = self.session.rstatus # a ploy to get the system status
source = ntp.control.CTL_SYS_SOURCE(rstatus)
if source == ntp.control.CTL_SST_TS_UNSPEC:
mode = 2 # Not yet synced
elif False:
mode = 3 # No reference configured
elif source == ntp.control.CTL_SST_TS_LOCAL:
mode = 4 # Distributing local clock (low accuracy)
elif source in (ntp.control.CTL_SST_TS_ATOM,
ntp.control.CTL_SST_TS_LF,
ntp.control.CTL_SST_TS_HF,
ntp.control.CTL_SST_TS_UHF):
# I am not sure if I should be including the radios in this
mode = 5 # Synced to local refclock
elif source == ntp.control.CTL_SST_TS_NTP:
# Should this include "other"? That covers things like chrony...
mode = 6 # Sync to remote NTP
else:
mode = 99 # Unknown
return mode
def misc_getSyspeerID(self):
peers = self.misc_getPeerData()
syspeer = 0
for associd in peers.keys():
rstatus = peers[associd]["peerstatus"]
if (ntp.control.CTL_PEER_STATVAL(rstatus) & 0x7) == \
ntp.control.CTL_PST_SEL_SYSPEER:
syspeer = associd
break
return syspeer
def safeReadvar(self, associd, variables=None, raw=False):
# Use this when we want to catch packet errors, but don't care
# about what they are
try:
return self.session.readvar(associd, varlist=variables, raw=raw)
except ntp.packet.ControlException:
return None
def dynamicCallbackPeerdata(self, variable, raw, valueType):
rawindex = 1 if raw else 0
def handler(oid, associd):
pdata = self.misc_getPeerData()
if pdata is None:
return None
value = pdata[associd][variable][rawindex]
return ax.Varbind(valueType, oid, value)
return self.dynamicCallbackSkeleton(handler)
def dynamicCallbackSkeleton(self, handler):
# Build a dynamic MIB tree, installing the provided handler in it
def readCallback(oid):
# This function assumes that it is a leaf node and that the
# last number in the OID is the index.
index = oid.subids[-1] # if called properly this works (Ha!)
index -= 1 # SNMP reserves index 0, effectively 1-based lists
associd = self.misc_getPeerIDs()[index]
return handler(oid, associd)
subs = {}
associds = self.misc_getPeerIDs() # need the peer count
for i in range(len(associds)):
subs[i+1] = {"reader": readCallback}
return subs
def readCallbackSkeletonSimple(self, oid, varname, dataType):
# Used for entries that just need a simple variable retrevial
# but do not need any processing.
data = self.safeReadvar(0, [varname])
if data is None:
return None
else:
return ax.Varbind(dataType, oid, data[varname])
def misc_getPeerIDs(self):
peerids = self.cache.get("peerids")
if peerids is None:
try:
peerids = [x.associd for x in self.session.readstat()]
except ntp.packet.ControlException:
peerids = []
peerids.sort()
self.cache.set("peerids", peerids)
return peerids
def misc_getPeerData(self):
peerdata = self.cache.get("peerdata")
if peerdata is None:
associds = self.misc_getPeerIDs()
peerdata = {}
for aid in associds:
try:
pdata = self.safeReadvar(aid, raw=True)
pdata["peerstatus"] = self.session.rstatus
except IOError:
continue
peerdata[aid] = pdata
self.cache.set("peerdata", peerdata)
return peerdata
def connect(address):
try:
if type(address) is str:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(address)
else:
host, port = address[0], address[1]
af, _, _, _, _ = socket.getaddrinfo(host, port)[0]
sock = socket.socket(af, socket.SOCK_STREAM)
sock.connect((host, port))
except socket.error as msg:
log("Connection to %s failure: %s" % (repr(address), repr(msg)), 1)
sys.exit(1)
log("connected to master agent at " + repr(address), 3)
return sock
def mainloop(snmpSocket, reconnectionAddr, host=None):
log("initing loop", 3)
dbase = DataSource(host, "/var/ntpsntpd/notify.conf")
while True: # Loop reconnection attempts
control = PacketControl(snmpSocket, dbase, logfp=logfp, debug=debug)
control.loopCallback = dbase.checkNotifications
control.initNewSession()
if not control.mainloop(True): # disconnected
snmpSocket.close()
snmpSocket = connect(reconnectionAddr)
log("disconnected from master, attempting reconnect", 2)
else: # Something else happened
break
def daemonize(runfunc, *runArgs):
pid = os.fork()
if pid < 0:
log("Forking error " + str(pid), 1)
sys.exit(pid)
elif pid > 0: # We are the parent
log("Daemonization success, child pid: " + str(pid), 3)
sys.exit(0)
# We must be the child
os.umask(0)
sid = os.setsid()
# chdir should be here, change to what? root?
global logfp
if logfp == sys.stderr:
logfp = None
sys.stdin.close()
sys.stdin = None
sys.stdout.close()
sys.stdout = None
sys.stderr.close()
sys.stderr = None
runfunc(*runArgs)
def loadSettings(filename, optionList):
log("Loading config file: %s" % filename, 3)
if not os.path.isfile(filename):
return None
options = {}
with open(filename) as f:
data = f.read()
lines = ntp.util.parseConf(data)
for line in lines:
isQuote, token = line[0]
if token in optionList:
options[token] = line[1:]
return options
def storeSettings(filename, settings):
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
data = []
for key in settings.keys():
data.append("%s %s\n" % (key, settings[key]))
data = "".join(data)
with open(filename, "w") as f:
f.write(data)
usage = """
USAGE: ntpsnmpd [-n] [ntp host]
Flg Arg Option-Name Description
-n no no-fork Do not fork and daemonize.
-x Adr master-addr Specify address for connecting to the master agent
- default /var/agentx/master
-d no debug-level Increase output debug message level
- may appear multiple times
-l Str logfile Logs debug messages to the provided filename
-D Int set-debug-level Set the output debug message level
- may appear multiple times
-h no help Print a usage message.
-V no version Output version information and exit
"""
if __name__ == "__main__":
bin_ver = "ntpsec-1.2.2a"
if ntp.util.stdversion() != bin_ver:
sys.stderr.write("Module/Binary version mismatch\n")
sys.stderr.write("Binary: %s\n" % bin_ver)
sys.stderr.write("Module: %s\n" % ntp.util.stdversion())
try:
(options, arguments) = getopt.getopt(
sys.argv[1:],
"nx:dD:Vhl:c:",
["no-fork", "master-address=", "debug-level", "set-debug-level=",
"version", "help", "logfile=", "configfile="])
except getopt.GetoptError as e:
sys.stderr.write("%s\n" % e)
sys.stderr.write(usage)
raise SystemExit(1)
masterAddr = "/var/agentx/master"
logfile = DEFLOG
hostname = DEFHOST
# Check for non-default config-file
conffile = "/etc/ntpsnmpd.conf"
for (switch, val) in options:
if switch in ("-c", "--configfile"):
conffile = val
break
# Load configuration file
conf = loadSettings(conffile,
("master-addr", "logfile", "loglevel", "ntp-addr"))
if conf is not None:
for key in conf.keys():
if key == "master-addr": # Address of the SNMP master daemon
val = conf[key][0][1]
if ":" in val:
host, port = val.split(":")
port = int(port)
masterAddr = (host, port)
else:
masterAddr = val
elif key == "logfile":
logfile = conf[key][0][1]
elif key == "ntp-addr": # Address of the NTP daemon
hostname = conf[key][0][1]
elif key == "loglevel":
errmsg = "Error: loglevel parameter '%s' not a number\n"
debug = conf[key][0][1]
fileLogging = False
for (switch, val) in options:
if switch in ("-n", "--no-fork"):
nofork = True
elif switch in ("-x", "--master-addr"):
if ":" in val:
host, port = val.split(":")
port = int(port)
masterAddr = (host, port)
else:
masterAddr = val
elif switch in ("-d", "--debug-level"):
debug += 1
elif switch in ("-D", "--set-debug-level"):
errmsg = "Error: -D parameter '%s' not a number\n"
debug = ntp.util.safeargcast(val, int, errmsg, usage)
elif switch in ("-V", "--version"):
print("ntpsnmpd %s" % ntp.util.stdversion())
raise SystemExit(0)
elif switch in ("-h", "--help"):
print(usage)
raise SystemExit(0)
elif switch in ("-l", "--logfile"):
logfile = val
fileLogging = True
if not nofork:
fileLogging = True
if fileLogging:
if logfp != sys.stderr:
logfp.close()
logfp = open(logfile, "a", 1) # 1 => line buffered
hostname = arguments[0] if arguments else DEFHOST
# Connect here so it can always report a connection error
sock = connect(masterAddr)
if nofork:
mainloop(sock, hostname)
else:
daemonize(mainloop, sock, hostname)