344 lines
12 KiB
Python
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()
|
|
|