snmp-agentx-ntpsec/src/agentx-ntpsec.py
Wolfgang Hottgenroth 679b9492d7 period fix
2025-03-01 23:50:09 +01:00

345 lines
12 KiB
Python

pass_valueimport ntp.packe0
import threading
from contextlib import AbstractContextManager
import time
import argparse
import os
import sys
import pwd
import grp
import logging
import logging.handlers
import pyagentx
LOGGING_LEVEL=logging.DEBUG
BASE_OID_ENTERPRISE = '1.3.6.1.4.1'
BASE_OID_HOTTIS = BASE_OID_ENTERPRISE + '.9676'
BASE_OID_HOTTIS_NTPSEC = BASE_OID_HOTTIS + '.123'
# just the prefix where the objects are below
LOCAL_PREFIX = '1'
PEERS_PREFIX = '2'
NUMBER_OF_PEERS_PREFIX = PEERS_PREFIX + '.1'
# this is for a table
# 2 is the prefix
# the first 1 is for the table in the mib
# the second 1 is for the entries in the table in the mib
TABLE_OF_PEERS_PREFIX = PEERS_PREFIX + '.2.1'
def int_scale1k(x):
return int (x * 1000)
def int_scale1M(x):
return int (x * 1000000)
def pass_value(x):
return x
LOCAL_SERVER_KEYS = [
['leap', pyagentx.TYPE_INTEGER, pass_value],
['stratum', pyagentx.TYPE_INTEGER, pass_value],
['precision', pyagentx.TYPE_INTEGER, pass_value],
['rootdelay', pyagentx.TYPE_INTEGER, int_scale1M],
['rootdisp', pyagentx.TYPE_INTEGER, int_scale1k],
['refid', pyagentx.TYPE_OCTETSTRING, pass_value],
['reftime', pyagentx.TYPE_OCTETSTRING, pass_value],
['tc', pyagentx.TYPE_INTEGER, pass_value],
['peer', pyagentx.TYPE_INTEGER, pass_value],
['offset', pyagentx.TYPE_INTEGER, int_scale1M],
['frequency', pyagentx.TYPE_INTEGER, int_scale1k],
['sys_jitter', pyagentx.TYPE_INTEGER, int_scale1M],
['clk_jitter', pyagentx.TYPE_INTEGER, int_scale1M],
['clock', pyagentx.TYPE_OCTETSTRING, pass_value],
['processor', pyagentx.TYPE_OCTETSTRING, pass_value],
['system', pyagentx.TYPE_OCTETSTRING, pass_value],
['version', pyagentx.TYPE_OCTETSTRING, pass_value],
['clk_wander', pyagentx.TYPE_INTEGER, int_scale1M],
['tai', pyagentx.TYPE_INTEGER, pass_value],
['leapsec', pyagentx.TYPE_OCTETSTRING, pass_value],
['expire', pyagentx.TYPE_OCTETSTRING, pass_value],
['mintc', pyagentx.TYPE_INTEGER, pass_value]
]
PEER_KEYS = [
['srcadr', pyagentx.TYPE_OCTETSTRING, pass_value],
['srcport', pyagentx.TYPE_INTEGER, pass_value],
['dstadr', pyagentx.TYPE_OCTETSTRING, pass_value],
['dstport', pyagentx.TYPE_INTEGER, pass_value],
['leap', pyagentx.TYPE_INTEGER, pass_value],
['hmode', pyagentx.TYPE_INTEGER, pass_value],
['stratum', pyagentx.TYPE_INTEGER, pass_value],
['ppoll', pyagentx.TYPE_INTEGER, pass_value],
['hpoll', pyagentx.TYPE_INTEGER, pass_value],
['precision', pyagentx.TYPE_INTEGER, pass_value],
['rootdelay', pyagentx.TYPE_INTEGER, int_scale1k],
['rootdisp', pyagentx.TYPE_INTEGER, int_scale1k],
['refid', pyagentx.TYPE_OCTETSTRING, pass_value],
['reftime', pyagentx.TYPE_OCTETSTRING, pass_value],
['rec', pyagentx.TYPE_OCTETSTRING, pass_value],
['xmt', pyagentx.TYPE_OCTETSTRING, pass_value],
['reach', pyagentx.TYPE_INTEGER, pass_value],
['unreach', pyagentx.TYPE_INTEGER, pass_value],
['delay-s', pyagentx.TYPE_OCTETSTRING, pass_value],
['delay', pyagentx.TYPE_INTEGER, int_scale1k],
['offset', pyagentx.TYPE_INTEGER, int_scale1M],
['jitter', pyagentx.TYPE_INTEGER, int_scale1M],
['dispersion', pyagentx.TYPE_INTEGER, int_scale1k],
['keyid', pyagentx.TYPE_INTEGER, pass_value],
['filtdelay', pyagentx.TYPE_OCTETSTRING, pass_value],
['filtoffset', pyagentx.TYPE_OCTETSTRING, pass_value],
['pmode', pyagentx.TYPE_INTEGER, pass_value],
['filtdisp', pyagentx.TYPE_OCTETSTRING, pass_value],
['flash', pyagentx.TYPE_INTEGER, pass_value],
['headway', pyagentx.TYPE_INTEGER, pass_value],
['ntscookies', pyagentx.TYPE_INTEGER, pass_value]
]
class DataStore(AbstractContextManager):
def __init__(self):
self.lock = threading.Lock()
self.data = {}
def update_data(self, data):
self.data.clear()
self.data.update(data)
def __enter__(self):
self.lock.acquire()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.lock.release()
class NtpDataCollector(threading.Thread):
def __init__(self, ntpserver='localhost', period=60):
self.ntpserver = ntpserver
self.period = period
self.stop_event = threading.Event()
self.session = ntp.packet.ControlSession()
self.session.openhost(self.ntpserver)
threading.Thread.__init__(self)
def run(self):
while not self.stop_event.is_set():
logger.debug('Query ntp server')
ntpserver_vars = self.session.readvar(0)
logger.debug(f"{ntpserver_vars=}")
tmp_data_store = {}
tmp_data_store['local'] = dict(ntpserver_vars)
peers = self.session.readstat()
tmp_data_store['peers'] = {}
for peer in peers:
peer_vars = self.session.readvar(peer.associd)
tmp_data_store['peers'][peer.associd] = dict(peer_vars)
logger.debug(f"{peer.associd=}, {peer_vars=}")
with globalDataStore as ds:
ds.update_data(tmp_data_store)
time.sleep(self.period)
logger.info('NtpDataCollector terminating')
def stop(self):
self.stop_event.set()
self.join()
class NtpsecDataUpdater(pyagentx.Updater):
def update(self):
with globalDataStore as ds:
if ds.data:
try:
for index, data_spec in enumerate(LOCAL_SERVER_KEYS, start=1):
# logger.debug(f"local: {index=} {data_spec=}")
oid_prefix = f"{LOCAL_PREFIX}.{index}"
self._data[oid_prefix] = {
'name': oid_prefix,
'type': data_spec[1],
'value': data_spec[2](ds.data['local'][data_spec[0]])
}
number_of_peers = len(ds.data['peers'])
# logger.debug(f"number of peers: {number_of_peers}")
number_of_peers_oid_prefix = f"{NUMBER_OF_PEERS_PREFIX}"
self._data[number_of_peers_oid_prefix] = {
'name': number_of_peers_oid_prefix,
'type': pyagentx.TYPE_INTEGER,
'value': number_of_peers
}
for peer_index, (associd, peer) in enumerate(ds.data['peers'].items(), start=1):
# logger.debug(f"peer: {peer}")
index_oid_prefix = f"{TABLE_OF_PEERS_PREFIX}.1.{peer_index}"
self._data[index_oid_prefix] = {
'name': index_oid_prefix,
'type': pyagentx.TYPE_INTEGER,
'value': peer_index
}
associd_oid_prefix = f"{TABLE_OF_PEERS_PREFIX}.2.{peer_index}"
self._data[associd_oid_prefix] = {
'name': associd_oid_prefix,
'type': pyagentx.TYPE_INTEGER,
'value': associd
}
for key_index, data_spec in enumerate(PEER_KEYS, start=3):
# logger.debug(f"peer: {associd=} {key_index=} {data_spec=}")
oid_prefix = f"{TABLE_OF_PEERS_PREFIX}.{key_index}.{peer_index}"
self._data[oid_prefix] = {
'name': oid_prefix,
'type': data_spec[1],
'value': data_spec[2](peer[data_spec[0]])
}
except Exception as e:
logger.error(f"Failed to update: {type(e)} {e}")
class NtpsecAgent(pyagentx.Agent):
def __init__(self, period=1, agent_id='NtpsecAgent', socket_path=None):
logger.info('Agent created')
self.period = period
super().__init__()
def setup(self):
logger.info('Agent setup')
self.register(BASE_OID_HOTTIS_NTPSEC, NtpsecDataUpdater, freq=self.period)
def daemonize(pid_filename):
if os.fork() > 0:
sys.exit(0)
os.setsid()
pid = os.fork()
if pid > 0:
with open(pid_filename, 'w') as pid_file:
pid_file.write(str(pid))
sys.exit(0)
sys.stdout.flush()
sys.stderr.flush()
with open("/dev/null", "r") as devnull:
os.dup2(devnull.fileno(), sys.stdin.fileno())
with open("/tmp/agentx-ntpsec.log", "a+") as log:
os.dup2(log.fileno(), sys.stdout.fileno())
os.dup2(log.fileno(), sys.stderr.fileno())
logger.removeHandler(stdout_handler)
pyagentx.setup_logging(debug=True)
def set_user_group(user, group):
if group:
try:
gid = grp.getgrnam(group).gr_gid
except KeyError:
logger.error(f"Group {group} does not exist")
sys.exit(1)
os.setgid(gid)
if user:
try:
uid = pwd.getpwnam(user).pw_uid
except KeyError:
logger.error(f"user {user} does not exist")
sys.exit(1)
os.setuid(uid)
if __name__ == '__main__':
logging.basicConfig(
level=LOGGING_LEVEL,
format="%(name)s - %(levelname)s - %(message)s",
handlers=[logging.handlers.SysLogHandler(address='/dev/log')]
)
logger = logging.getLogger('agentx-ntpsec')
stdout_handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
stdout_handler.setFormatter(formatter)
logger.addHandler(stdout_handler)
pid_filename = '/tmp/agentx-ntpsec.pid'
parser = argparse.ArgumentParser(description='snmpd agentx extension for ntpsec')
parser.add_argument('--period', '-p',
help='Period to query the NTP server, in seconds, default 60s',
required=False,
default=60)
parser.add_argument('--ntpserver', '-n',
help='NTP server to query, default is localhost',
required=False,
default='localhost')
parser.add_argument('--daemonize', '-d',
help='Run process in background',
required=False,
action='store_true',
default=False)
parser.add_argument('--pid',
help=f"pid-file when running as daemon, default is {pid_filename}",
required=False,
default=pid_filename)
parser.add_argument('-u', '--user',
help="Set uid of process",
required=False,
default='')
parser.add_argument('-g', '--group',
help="Set gid of process",
required=False,
default='')
args = parser.parse_args()
if args.daemonize:
daemonize(pid_filename)
set_user_group(args.user, args.group)
if args.group:
try:
gid = grp.getgrnam(args.group).gr_gid
except KeyError:
logger.error(f"Group {args.group} does not exist")
sys.exit(1)
os.setgid(gid)
if args.user:
try:
uid = pwd.getpwnam(args.user).pw_uid
except KeyError:
logger.error(f"user {args.user} does not exist")
sys.exit(1)
os.setuid(uid)
ntpserver = args.ntpserver
period = args.period
try:
globalDataStore = DataStore()
ndc = NtpDataCollector(ntpserver=ntpserver, period=period)
ndc.start()
nsax = NtpsecAgent(period=period)
nsax.start()
except Exception as e:
logger.error(f"Unhandled exception: {e}")
nsax.stop()
ndc.stop()
except KeyboardInterrupt:
nsax.stop()
ndc.stop()