snmp-agentx-ntpsec/src/agentx-ntpsec.py
2025-02-25 10:36:24 +01:00

344 lines
12 KiB
Python

import ntp.packet
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_scale1k],
['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, agent_id='NtpsecAgent', socket_path=None):
logger.info('Agent created')
super().__init__()
def setup(self):
logger.info('Agent setup')
self.register(BASE_OID_HOTTIS_NTPSEC, NtpsecDataUpdater, freq=1)
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()
nsax.start()
except Exception as e:
logger.error(f"Unhandled exception: {e}")
nsax.stop()
ndc.stop()
except KeyboardInterrupt:
nsax.stop()
ndc.stop()