--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/pvl.verkko-dhcp Thu Oct 18 21:17:11 2012 +0300
@@ -0,0 +1,256 @@
+#!/usr/bin/env python
+
+"""
+ Monitor DHCP use.
+"""
+
+__version__ = '0.0'
+
+import pvl.args
+import pvl.syslog.args
+
+import pvl.verkko.db as db
+import pvl.syslog.dhcp
+
+import logging, optparse
+
+log = logging.getLogger('main')
+
+# name of process in syslog
+DHCP_SYSLOG_PROG = 'dhcpd'
+
+def parse_options (argv) :
+ """
+ Parse command-line arguments.
+ """
+
+ prog = argv[0]
+
+ parser = optparse.OptionParser(
+ prog = prog,
+ usage = '%prog: [options]',
+ version = __version__,
+
+ # module docstring
+ description = __doc__,
+ )
+
+ # options
+ parser.add_option_group(pvl.args.parser(parser))
+
+ ## syslog
+ parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG))
+
+ ## XXX: networks
+ parser.add_option('--network', metavar='NET', action='append',
+ help="Filter leases by network prefix as plugin instance")
+
+ parser.add_option('--gateway', metavar='GW/IFACE', action='append',
+ help="Filter messages by gateway/interface as plugin instance")
+
+ ## hosts
+ parser.add_option('--database', metavar='URI',
+ help="Track hosts in given database")
+
+ parser.add_option('--create', action='store_true',
+ help="Initialize database")
+
+ # defaults
+ parser.set_defaults(
+
+ )
+
+ # parse
+ options, args = parser.parse_args(argv[1:])
+
+ # apply
+ pvl.args.apply(options, prog)
+
+ if not options.database :
+ parser.error("Missing required option: --database")
+
+ return options, args
+
+class DHCPHostsDatabase (db.Database) :
+
+ def create (self) :
+ """
+ CREATE TABLEs
+ """
+
+ log.info("Creating database tables: dhcp_hosts")
+ db.dhcp_hosts.create(self.engine)
+
+ def insert (self, attrs) :
+ """
+ INSERT new host
+ """
+
+ query = db.dhcp_hosts.insert().values(
+ ip = attrs['ip'],
+ mac = attrs['mac'],
+ gw = attrs['gw'],
+
+ first_seen = attrs['timestamp'],
+
+ name = attrs['name'],
+ last_seen = attrs['timestamp'],
+ last_msg = attrs['state'],
+ )
+ result = self.engine.execute(query, **attrs)
+ id, = result.inserted_primary_key
+
+ return id
+
+ def update (self, attrs) :
+ """
+ UPDATE existing host, or return False if not found.
+ """
+
+ table = db.dhcp_hosts
+
+ query = table.update(
+ # where
+ (table.c.ip == attrs['ip']) & (table.c.mac == attrs['mac']) & (table.c.gw == attrs['gw']),
+
+ # set
+ name = attrs['name'],
+ last_seen = attrs['timestamp'],
+ last_msg = attrs['state'],
+ )
+ result = self.engine.execute(query, **attrs)
+
+ # any matched rows?
+ return result.rowcount > 0
+
+ def process (self, item) :
+ """
+ Process given DHCP syslog message to update the hosts table.
+ """
+
+ # ignore unless we have enough info to fully identify the client
+ # this means that we omit DHCPDISCOVER messages, but we get the OFFER/REQUEST/ACK
+ if any(name not in item for name in ('lease', 'hwaddr', 'gateway')) :
+ # ignore; we require these
+ return
+
+ # db: syslog
+ ATTR_MAP = (
+ ('ip', 'lease'),
+ ('mac', 'hwaddr'),
+ ('gw', 'gateway'),
+ ('name', 'hostname'),
+
+ ('timestamp', 'timestamp'),
+ ('state', 'type'),
+ ('error', 'error'),
+ )
+
+ # attrs
+ attrs = dict((key, item.get(name)) for key, name in ATTR_MAP)
+
+ # update existing?
+ if self.update(attrs) :
+ log.info("Update: %s", attrs)
+
+ else :
+ # new
+ log.info("Insert: %s", attrs)
+ self.insert(attrs)
+
+class DHCPSyslogHandler (object) :
+ """
+ Process lines from syslog
+ """
+
+ def __init__ (self, db) :
+ self.db = db
+
+ # XXX
+ self.filter = pvl.syslog.dhcp.DHCPSyslogFilter()
+
+ def process (self, item) :
+ """
+ Handle a single item read from syslog to DB.
+ filter = pvl.syslog.dhcp.DHCPSyslogFilter()
+ """
+
+ dhcp_item = self.filter.parse(item['msg'])
+
+ if not dhcp_item :
+ # ignore
+ return
+
+ dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser?
+
+ self.db.process(dhcp_item)
+
+ def main (self, source, poll) :
+ """
+ Read items from syslog source with given polling style.
+
+ TODO: poll = false on SIGINT
+ """
+
+ parser = pvl.syslog.parser.SyslogParser()
+
+ # mainloop
+ while True :
+ # process from source
+ for item in parser.process(source) :
+ self.process(item)
+
+ if poll is False :
+ # done
+ break
+ else :
+ # wait
+ source.poll(poll)
+
+ log.debug("tick")
+
+def main (argv) :
+ options, args = parse_options(argv)
+
+ # syslog
+ log.info("Open up syslog...")
+ syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog
+
+ if options.syslog_fifo :
+ source = pvl.syslog.fifo.Fifo(options.syslog_fifo)
+ poll = None # no timeout
+
+ elif options.syslog_tail :
+ # continuous file tail
+ source = pvl.syslog.tail.TailFile(options.syslog_file)
+ poll = options.syslog_tail # polling interval
+
+ elif options.syslog_file :
+ # one-shot file read
+ source = pvl.syslog.tail.TailFile(options.syslog_file)
+ poll = False # do not poll-loop
+
+ else :
+ log.error("No syslog source given")
+ return 1
+
+ # db
+ log.info("Open up database: %s", options.database)
+ database = DHCPHostsDatabase(options.database)
+
+ if options.create :
+ database.create()
+
+ # handler + main
+ handler = DHCPSyslogHandler(database)
+
+ log.info("Enter mainloop...")
+ handler.main(source, poll)
+
+ # done
+ return 0
+
+if __name__ == '__main__':
+ import sys
+
+ sys.exit(main(sys.argv))