terom@15: #!/usr/bin/env python terom@15: terom@15: """ terom@15: Monitor DHCP use. terom@15: """ terom@15: terom@15: __version__ = '0.0' terom@15: terom@15: import pvl.args terom@15: import pvl.syslog.args terom@15: terom@15: import pvl.verkko.db as db terom@15: import pvl.syslog.dhcp terom@17: import pvl.verkko.dhcp.leases terom@15: terom@15: import logging, optparse terom@15: terom@15: log = logging.getLogger('main') terom@15: terom@15: # name of process in syslog terom@15: DHCP_SYSLOG_PROG = 'dhcpd' terom@15: terom@15: def parse_options (argv) : terom@15: """ terom@15: Parse command-line arguments. terom@15: """ terom@15: terom@15: prog = argv[0] terom@15: terom@15: parser = optparse.OptionParser( terom@15: prog = prog, terom@15: usage = '%prog: [options]', terom@15: version = __version__, terom@15: terom@15: # module docstring terom@15: description = __doc__, terom@15: ) terom@15: terom@15: # options terom@15: parser.add_option_group(pvl.args.parser(parser)) terom@15: terom@15: ## syslog terom@15: parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG)) terom@15: terom@17: ## leases terom@17: parser.add_option('--leases-file', metavar='FILE', terom@17: help="Synchronize dhcpd leases from given file") terom@17: terom@17: parser.add_option('--leases-tail', type='float', metavar='POLL', terom@17: help="Continuously poll leases file XXX: overrides --syslog-tail") terom@17: terom@17: terom@15: ## XXX: networks terom@15: parser.add_option('--network', metavar='NET', action='append', terom@15: help="Filter leases by network prefix as plugin instance") terom@15: terom@15: parser.add_option('--gateway', metavar='GW/IFACE', action='append', terom@15: help="Filter messages by gateway/interface as plugin instance") terom@15: terom@15: ## hosts terom@15: parser.add_option('--database', metavar='URI', terom@15: help="Track hosts in given database") terom@15: terom@15: parser.add_option('--create', action='store_true', terom@15: help="Initialize database") terom@15: terom@15: # defaults terom@15: parser.set_defaults( terom@15: terom@15: ) terom@15: terom@15: # parse terom@15: options, args = parser.parse_args(argv[1:]) terom@15: terom@15: # apply terom@15: pvl.args.apply(options, prog) terom@15: terom@15: if not options.database : terom@15: parser.error("Missing required option: --database") terom@15: terom@15: return options, args terom@15: terom@17: class DHCPHostsDatabase (object) : terom@17: """ terom@17: The dhcp_hosts table in our database. terom@17: """ terom@17: terom@17: def __init__ (self, db) : terom@17: self.db = db terom@15: terom@15: def create (self) : terom@15: """ terom@15: CREATE TABLEs terom@15: """ terom@15: terom@15: log.info("Creating database tables: dhcp_hosts") terom@33: db.dhcp_hosts.create(self.db.engine, checkfirst=True) terom@15: terom@15: def insert (self, attrs) : terom@15: """ terom@15: INSERT new host terom@15: """ terom@15: terom@15: query = db.dhcp_hosts.insert().values( terom@15: ip = attrs['ip'], terom@15: mac = attrs['mac'], terom@15: gw = attrs['gw'], terom@15: terom@15: first_seen = attrs['timestamp'], terom@16: count = 1, terom@15: terom@15: last_seen = attrs['timestamp'], terom@16: state = attrs['state'], terom@16: terom@16: name = attrs.get('name'), terom@16: error = attrs.get('error'), terom@15: ) terom@15: terom@17: # -> id terom@17: return self.db.insert(query) terom@15: terom@15: def update (self, attrs) : terom@15: """ terom@15: UPDATE existing host, or return False if not found. terom@15: """ terom@15: terom@15: table = db.dhcp_hosts terom@15: terom@16: query = table.update() terom@16: query = query.where((table.c.ip == attrs['ip']) & (table.c.mac == attrs['mac']) & (table.c.gw == attrs['gw'])) terom@16: query = query.values( terom@16: count = db.func.coalesce(table.c.count, 0) + 1, terom@15: terom@15: # set terom@15: last_seen = attrs['timestamp'], terom@16: state = attrs['state'], terom@15: ) terom@16: terom@16: if 'name' in attrs : terom@16: query = query.values(name = attrs['name']) terom@16: terom@16: if 'error' in attrs : terom@16: query = query.values(error = attrs['error']) terom@16: terom@15: # any matched rows? terom@17: return self.db.update(query) terom@15: terom@15: def process (self, item) : terom@15: """ terom@15: Process given DHCP syslog message to update the hosts table. terom@15: """ terom@16: terom@16: attrs = {} terom@15: terom@15: # ignore unless we have enough info to fully identify the client terom@15: # this means that we omit DHCPDISCOVER messages, but we get the OFFER/REQUEST/ACK terom@15: if any(name not in item for name in ('lease', 'hwaddr', 'gateway')) : terom@15: # ignore; we require these terom@15: return terom@15: terom@16: # do not override error from request on NAK; clear otherwise terom@16: if item.get('type') == 'DHCPNAK' : terom@16: pass terom@16: else : terom@16: attrs['error'] = item.get('error') terom@16: terom@16: # do not override name unless known terom@16: if item.get('name') : terom@16: attrs['name'] = item.get('name') terom@16: terom@15: # db: syslog terom@15: ATTR_MAP = ( terom@15: ('ip', 'lease'), terom@15: ('mac', 'hwaddr'), terom@15: ('gw', 'gateway'), terom@15: terom@15: ('timestamp', 'timestamp'), terom@15: ('state', 'type'), terom@15: ) terom@15: terom@16: # generic attrs terom@16: for key, name in ATTR_MAP : terom@16: attrs[key] = item.get(name) terom@15: terom@15: # update existing? terom@15: if self.update(attrs) : terom@15: log.info("Update: %s", attrs) terom@15: terom@15: else : terom@15: # new terom@15: log.info("Insert: %s", attrs) terom@15: self.insert(attrs) terom@15: terom@17: class DHCPLeasesDatabase (object) : terom@17: def __init__ (self, db) : terom@17: self.db = db terom@17: terom@17: def create (self) : terom@17: """ terom@17: CREATE TABLEs terom@17: """ terom@17: terom@17: log.info("Creating database tables: dhcp_leases") terom@33: db.dhcp_leases.create(self.db.engine, checkfirst=True) terom@17: terom@17: def update (self, lease) : terom@17: """ terom@17: Try an extend an existing lease? terom@17: """ terom@17: terom@17: c = db.dhcp_leases.c terom@17: terom@17: ip = lease['lease'] terom@17: mac = lease.get('hwaddr') terom@17: starts = lease['starts'] terom@38: ends = lease.get('ends') terom@17: terom@17: update = db.dhcp_leases.update() terom@17: terom@38: # XXX: if ends is None? terom@17: if mac : terom@17: # renew lease..? terom@17: update = update.where((c.ip == ip) & (c.mac == mac) & ((starts < c.ends) | (c.ends == None))) terom@17: else : terom@17: # new state for lease..? terom@17: update = update.where((c.ip == ip) & ((starts < c.ends) | (c.ends == ends))) terom@17: terom@17: update = update.values( terom@17: state = lease['binding-state'], terom@17: next = lease.get('next-binding-state'), terom@38: ends = ends, terom@17: ) terom@17: terom@17: if lease.get('client-hostname') : terom@17: update = update.values(hostname = lease['client-hostname']) terom@17: terom@17: return self.db.update(update) > 0 terom@17: terom@17: def insert (self, lease) : terom@17: """ terom@17: Record a new lease. terom@17: """ terom@17: terom@17: c = db.dhcp_leases.c terom@17: terom@17: query = db.dhcp_leases.insert().values( terom@17: ip = lease['lease'], terom@17: mac = lease['hwaddr'], terom@17: hostname = lease.get('client-hostname'), terom@17: terom@17: starts = lease['starts'], terom@38: ends = lease.get('ends'), terom@17: terom@17: state = lease['binding-state'], terom@17: next = lease.get('next-binding-state'), terom@17: ) terom@17: terom@17: return self.db.insert(query) terom@17: terom@17: def process (self, lease) : terom@17: """ terom@17: Process given DHCP lease to update currently active lease, or insert a new one. terom@17: """ terom@17: terom@17: # update existing? terom@17: if self.update(lease) : terom@17: log.info("Update: %s", lease) terom@17: terom@17: elif lease.get('hwaddr') : terom@17: # new terom@17: id = self.insert(lease) terom@17: terom@17: log.info("Insert: %s -> %d", lease, id) terom@17: terom@17: else : terom@17: # may be a free lease terom@17: log.warn("Ignored lease: %s", lease) terom@17: terom@34: # XXX: mainloop terom@34: import time terom@34: terom@17: class DHCPHandler (object) : terom@15: """ terom@15: Process lines from syslog terom@15: """ terom@15: terom@17: def __init__ (self, db, syslog, leases) : terom@15: self.db = db terom@15: terom@17: self.syslog = syslog terom@17: self.leases = leases terom@17: terom@17: self.hostdb = DHCPHostsDatabase(db) terom@17: self.leasedb = DHCPLeasesDatabase(db) terom@17: terom@15: # XXX terom@15: self.filter = pvl.syslog.dhcp.DHCPSyslogFilter() terom@15: terom@17: def createdb (self) : terom@17: """ terom@17: Initialize database tables. terom@17: """ terom@17: terom@17: self.hostdb.create() terom@17: self.leasedb.create() terom@17: terom@17: def process_syslog (self, item) : terom@15: """ terom@15: Handle a single item read from syslog to DB. terom@15: """ terom@15: terom@15: dhcp_item = self.filter.parse(item['msg']) terom@77: terom@77: log.debug("%s: %s", item, dhcp_item) terom@15: terom@15: if not dhcp_item : terom@15: # ignore terom@15: return terom@15: terom@15: dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser? terom@15: terom@16: if item.get('error') : terom@16: item['error'] = self.filter.parse_error(item['error']) terom@16: terom@17: self.hostdb.process(dhcp_item) terom@15: terom@17: def process_leases (self, leases) : terom@17: """ terom@17: Handle given changed lease. terom@17: """ terom@17: terom@17: for lease in leases : terom@17: log.info("%s", lease['lease']) terom@17: terom@17: self.leasedb.process(lease) terom@17: terom@17: def sync_leases (self, leases) : terom@17: """ terom@17: Sync the entire given set of valid leases. terom@17: """ terom@17: terom@17: log.info("%d leases", len(leases)) terom@17: terom@17: # XXX: any difference? Mostly removed leases, but... they should expire by themselves, right? terom@17: self.process_leases(leases) terom@17: terom@17: def main (self, poll) : terom@15: """ terom@15: Read items from syslog source with given polling style. terom@15: terom@15: TODO: poll = false on SIGINT terom@15: """ terom@15: terom@15: # mainloop terom@15: while True : terom@17: if self.syslog : terom@17: # process from source terom@64: for item in self.syslog : terom@17: self.process_syslog(item) terom@15: terom@17: if self.leases : terom@17: # process internally terom@17: #sync, leases = self.leases.process() terom@17: leases = self.leases.process() terom@17: terom@17: #if sync : terom@17: # self.sync_leases(leases) terom@17: if leases : terom@17: self.process_leases(leases) terom@17: #else : terom@17: # pass terom@17: terom@64: if not poll : terom@15: # done terom@15: break terom@64: terom@34: elif self.syslog : terom@15: # wait terom@115: self.syslog.select(poll) terom@64: terom@34: else : terom@64: # XXX: for --leases-tail terom@34: time.sleep(poll) terom@15: terom@15: log.debug("tick") terom@15: terom@15: def main (argv) : terom@15: options, args = parse_options(argv) terom@15: terom@16: # db terom@16: if not options.database : terom@16: log.error("No database given") terom@16: return 1 terom@16: terom@16: log.info("Open up database: %s", options.database) terom@17: database = pvl.verkko.db.Database(options.database) terom@17: terom@15: # syslog terom@15: log.info("Open up syslog...") terom@64: syslog = pvl.syslog.args.apply(options, optional=True) terom@15: terom@64: if syslog : terom@115: poll = syslog.poll terom@15: else : terom@16: log.warning("No syslog source given") terom@64: poll = None terom@17: terom@17: # leases terom@17: if options.leases_file : terom@17: log.info("Open up DHCP leases...") terom@17: leases = pvl.verkko.dhcp.leases.DHCPLeasesDatabase(options.leases_file) terom@17: terom@17: # force polling interval terom@17: if options.leases_tail : terom@17: # XXX: min between syslog/leases? terom@17: poll = options.leases_tail terom@17: terom@17: else : terom@17: leases = None terom@15: terom@15: # handler + main terom@17: handler = DHCPHandler(database, syslog, leases) terom@15: terom@17: if options.create : terom@17: handler.createdb() terom@17: terom@15: log.info("Enter mainloop...") terom@17: handler.main(poll) terom@15: terom@15: # done terom@15: return 0 terom@15: terom@15: if __name__ == '__main__': terom@15: import sys terom@15: terom@15: sys.exit(main(sys.argv))