bin/pvl.verkko-dhcp: re-implement syslog -> database based on pvl.collectd-dhcp; uses pvl.syslog from pvl-collectd
authorTero Marttila <terom@paivola.fi>
Thu, 18 Oct 2012 21:17:11 +0300
changeset 15 66f81f4b6aa7
parent 14 02c21749cb4f
child 16 51509b5ce1c0
bin/pvl.verkko-dhcp: re-implement syslog -> database based on pvl.collectd-dhcp; uses pvl.syslog from pvl-collectd
bin/pvl.verkko-dhcp
--- /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))