#!/usr/bin/env python3 # -*- python -*- """ =head1 NAME cronjobs - Plugin to monitor the number of cronjobs running per user, gathering data from syslog. =head1 INSTALLATION Place in /etc/munin/plugins/ (or link it there using ln -s) =head1 CONFIGURATION Add this to your /etc/munin/plugin-conf.d/munin-node: [cronjobs] user root env.syslog_path /var/log/syslog # default value env.cron_ident_regex crond? # for finding cron entries in the syslog, case-insensitive The plugin needs to run as root in order to read from syslog. =head1 AUTHORS Copyright (C) 2019 pcy =head1 MAGIC MARKERS #%# family=auto #%# capabilities=autoconf =cut """ from datetime import datetime, timedelta, timezone import os import re import shutil import socket import sys import struct import time syslog_path = os.getenv('syslog_path', '/var/log/syslog') cron_ident_regex = os.getenv('cron_ident_regex', 'crond?') # expected format: # CRON[]: () CMD ... # example: # Aug 16 22:00:01 zap CRON[23060]: (root) CMD (/usr/local/bin/dnsconfig -s) cron_syslog_regex = re.compile('^.* %s %s\[[0-9]*\]: \((.*?)\)' # noqa: W605 % (socket.gethostname(), cron_ident_regex), re.I) STATEFILE = os.getenv('MUNIN_STATEFILE') def loadstate(): if not os.path.isfile(STATEFILE): return None with open(STATEFILE, 'rb') as f: tstamp = struct.unpack('d', f.read())[0] return datetime.fromtimestamp(tstamp, tz=timezone.utc) def savestate(state): with open(STATEFILE, 'wb') as f: f.write(struct.pack('d', state.timestamp())) def extrcronuser(line: str): t = cron_syslog_regex.search(line) if t is None: return None gr = t.groups() return gr[0] if len(gr) == 1 else None def getcronlines(): with open(syslog_path, 'r') as f: for x in f.readlines(): user = extrcronuser(x) if user is not None: yield (x, user) def getcronusers(lines): return set(x[1] for x in lines) def autoconf(): if shutil.which('crontab') is None: print("no (need cron installed)") elif not os.access(syslog_path, os.R_OK): print("no (cannot access syslog file '%s')" % syslog_path) else: print("yes") def config(): usernames = getcronusers(getcronlines()) print("""\ graph_title Cron jobs per user graph_vlabel jobs graph_category processes""") for n in usernames: print(n + ".label " + n) print(n + ".info jobs of user " + n) def fetch(): STATE = loadstate() # why is there no stdlib function for this?! localtz = timezone(timedelta(seconds=time.localtime().tm_gmtoff)) now = datetime.now() now_withtz = datetime.now(tz=localtz) yearsfx = ' ' + str(now.year) pyearsfx = ' ' + str(now.year - 1) cronlines = list(getcronlines()) allnames = getcronusers(cronlines) counts = {} hostname = socket.gethostname() for ln, name in cronlines: datestr = ln[:ln.index(hostname)].strip() logdate = datetime.strptime(datestr + yearsfx, "%b %d %H:%M:%S %Y") if logdate > now: logdate = datetime.strptime(datestr + pyearsfx, "%b %d %H:%M:%S %Y") # add timezone info (ugly hack), as strptime doesn't want to do this logdate = now_withtz + (logdate - now) if STATE is None or logdate > STATE: counts[name] = (counts[name] + 1) if name in counts else 1 for n in allnames: if n in counts: print("%s.value %d" % (n, counts[n])) else: print("%s.value 0" % n) savestate(now_withtz) if len(sys.argv) >= 2: if sys.argv[1] == 'autoconf': autoconf() elif sys.argv[1] == 'config': config() else: fetch() else: fetch()