bin/pvl.verkko-dhcp
changeset 17 29f0cf9220e0
parent 16 51509b5ce1c0
child 18 2d16489b8782
equal deleted inserted replaced
16:51509b5ce1c0 17:29f0cf9220e0
     9 import pvl.args
     9 import pvl.args
    10 import pvl.syslog.args
    10 import pvl.syslog.args
    11 
    11 
    12 import pvl.verkko.db as db
    12 import pvl.verkko.db as db
    13 import pvl.syslog.dhcp
    13 import pvl.syslog.dhcp
       
    14 import pvl.verkko.dhcp.leases
    14 
    15 
    15 import logging, optparse
    16 import logging, optparse
    16 
    17 
    17 log = logging.getLogger('main')
    18 log = logging.getLogger('main')
    18 
    19 
    38     # options
    39     # options
    39     parser.add_option_group(pvl.args.parser(parser))
    40     parser.add_option_group(pvl.args.parser(parser))
    40 
    41 
    41     ## syslog
    42     ## syslog
    42     parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG))
    43     parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG))
       
    44 
       
    45     ## leases
       
    46     parser.add_option('--leases-file',          metavar='FILE',
       
    47             help="Synchronize dhcpd leases from given file")
       
    48 
       
    49     parser.add_option('--leases-tail',          type='float', metavar='POLL',
       
    50             help="Continuously poll leases file XXX: overrides --syslog-tail")
       
    51 
    43 
    52 
    44     ## XXX: networks
    53     ## XXX: networks
    45     parser.add_option('--network',              metavar='NET', action='append',
    54     parser.add_option('--network',              metavar='NET', action='append',
    46             help="Filter leases by network prefix as plugin instance")
    55             help="Filter leases by network prefix as plugin instance")
    47 
    56 
    69     if not options.database :
    78     if not options.database :
    70         parser.error("Missing required option: --database")
    79         parser.error("Missing required option: --database")
    71 
    80 
    72     return options, args
    81     return options, args
    73 
    82 
    74 class DHCPHostsDatabase (db.Database) :
    83 class DHCPHostsDatabase (object) :
       
    84     """
       
    85         The dhcp_hosts table in our database.
       
    86     """
       
    87 
       
    88     def __init__ (self, db) :
       
    89         self.db = db
    75 
    90 
    76     def create (self) :
    91     def create (self) :
    77         """
    92         """
    78             CREATE TABLEs
    93             CREATE TABLEs
    79         """
    94         """
    80 
    95 
    81         log.info("Creating database tables: dhcp_hosts")
    96         log.info("Creating database tables: dhcp_hosts")
    82         db.dhcp_hosts.create(self.engine)
    97         db.dhcp_hosts.create(self.db.engine)
    83 
    98 
    84     def insert (self, attrs) :
    99     def insert (self, attrs) :
    85         """
   100         """
    86             INSERT new host
   101             INSERT new host
    87         """
   102         """
    98                 state       = attrs['state'],
   113                 state       = attrs['state'],
    99                 
   114                 
   100                 name        = attrs.get('name'),
   115                 name        = attrs.get('name'),
   101                 error       = attrs.get('error'),
   116                 error       = attrs.get('error'),
   102         )
   117         )
   103         result = self.engine.execute(query)
   118         
   104         id, = result.inserted_primary_key
   119         # -> id
   105         
   120         return self.db.insert(query)
   106         return id
       
   107 
   121 
   108     def update (self, attrs) :
   122     def update (self, attrs) :
   109         """
   123         """
   110             UPDATE existing host, or return False if not found.
   124             UPDATE existing host, or return False if not found.
   111         """
   125         """
   126             query = query.values(name = attrs['name'])
   140             query = query.values(name = attrs['name'])
   127         
   141         
   128         if 'error' in attrs :
   142         if 'error' in attrs :
   129             query = query.values(error = attrs['error'])
   143             query = query.values(error = attrs['error'])
   130 
   144 
   131         result = self.engine.execute(query)
       
   132         
       
   133         # any matched rows?
   145         # any matched rows?
   134         return result.rowcount > 0
   146         return self.db.update(query)
   135 
   147 
   136     def process (self, item) :
   148     def process (self, item) :
   137         """
   149         """
   138             Process given DHCP syslog message to update the hosts table.
   150             Process given DHCP syslog message to update the hosts table.
   139         """
   151         """
   177         else :
   189         else :
   178             # new
   190             # new
   179             log.info("Insert: %s", attrs)
   191             log.info("Insert: %s", attrs)
   180             self.insert(attrs)
   192             self.insert(attrs)
   181 
   193 
   182 class DHCPSyslogHandler (object) :
   194 class DHCPLeasesDatabase (object) :
   183     """
       
   184         Process lines from syslog
       
   185     """
       
   186 
       
   187     def __init__ (self, db) :
   195     def __init__ (self, db) :
   188         self.db = db
   196         self.db = db
   189 
   197 
       
   198     def create (self) :
       
   199         """
       
   200             CREATE TABLEs
       
   201         """
       
   202 
       
   203         log.info("Creating database tables: dhcp_leases")
       
   204         db.dhcp_leases.create(self.db.engine)
       
   205 
       
   206     def update (self, lease) :
       
   207         """
       
   208             Try an extend an existing lease?
       
   209         """
       
   210 
       
   211         c = db.dhcp_leases.c
       
   212 
       
   213         ip = lease['lease']
       
   214         mac = lease.get('hwaddr')
       
   215         starts = lease['starts']
       
   216         ends = lease['ends']
       
   217 
       
   218         update = db.dhcp_leases.update()
       
   219         
       
   220         if mac :
       
   221             # renew lease..?
       
   222             update = update.where((c.ip == ip) & (c.mac == mac) & ((starts < c.ends) | (c.ends == None)))
       
   223         else :
       
   224             # new state for lease..?
       
   225             update = update.where((c.ip == ip) & ((starts < c.ends) | (c.ends == ends)))
       
   226 
       
   227         update = update.values(
       
   228                 state       = lease['binding-state'],
       
   229                 next        = lease.get('next-binding-state'),
       
   230                 ends        = lease['ends'],
       
   231         )
       
   232 
       
   233         if lease.get('client-hostname') :
       
   234             update = update.values(hostname = lease['client-hostname'])
       
   235 
       
   236         return self.db.update(update) > 0
       
   237 
       
   238     def insert (self, lease) :
       
   239         """
       
   240             Record a new lease.
       
   241         """
       
   242 
       
   243         c = db.dhcp_leases.c
       
   244 
       
   245         query = db.dhcp_leases.insert().values(
       
   246             ip          = lease['lease'],
       
   247             mac         = lease['hwaddr'],
       
   248             hostname    = lease.get('client-hostname'),
       
   249 
       
   250             starts      = lease['starts'],
       
   251             ends        = lease['ends'],
       
   252 
       
   253             state       = lease['binding-state'],
       
   254             next        = lease.get('next-binding-state'),
       
   255         )
       
   256 
       
   257         return self.db.insert(query)
       
   258 
       
   259     def process (self, lease) :
       
   260         """
       
   261             Process given DHCP lease to update currently active lease, or insert a new one.
       
   262         """
       
   263 
       
   264         # update existing?
       
   265         if self.update(lease) :
       
   266             log.info("Update: %s", lease)
       
   267 
       
   268         elif lease.get('hwaddr') :
       
   269             # new
       
   270             id = self.insert(lease)
       
   271 
       
   272             log.info("Insert: %s -> %d", lease, id)
       
   273 
       
   274         else :
       
   275             # may be a free lease
       
   276             log.warn("Ignored lease: %s", lease)
       
   277 
       
   278 class DHCPHandler (object) :
       
   279     """
       
   280         Process lines from syslog
       
   281     """
       
   282 
       
   283     def __init__ (self, db, syslog, leases) :
       
   284         self.db = db
       
   285 
       
   286         self.syslog = syslog
       
   287         self.leases = leases
       
   288 
       
   289         self.hostdb = DHCPHostsDatabase(db)
       
   290         self.leasedb = DHCPLeasesDatabase(db)
       
   291 
   190         # XXX
   292         # XXX
   191         self.filter = pvl.syslog.dhcp.DHCPSyslogFilter()
   293         self.filter = pvl.syslog.dhcp.DHCPSyslogFilter()
   192 
   294 
   193     def process (self, item) :
   295     def createdb (self) :
       
   296         """
       
   297             Initialize database tables.
       
   298         """
       
   299 
       
   300         self.hostdb.create()
       
   301         self.leasedb.create()
       
   302 
       
   303     def process_syslog (self, item) :
   194         """
   304         """
   195             Handle a single item read from syslog to DB.
   305             Handle a single item read from syslog to DB.
   196         filter = pvl.syslog.dhcp.DHCPSyslogFilter()
       
   197         """
   306         """
   198 
   307 
   199         dhcp_item = self.filter.parse(item['msg'])
   308         dhcp_item = self.filter.parse(item['msg'])
   200         
   309         
   201         if not dhcp_item :
   310         if not dhcp_item :
   205         dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser?
   314         dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser?
   206 
   315 
   207         if item.get('error') :
   316         if item.get('error') :
   208             item['error'] = self.filter.parse_error(item['error'])
   317             item['error'] = self.filter.parse_error(item['error'])
   209 
   318 
   210         self.db.process(dhcp_item)
   319         self.hostdb.process(dhcp_item)
   211 
   320 
   212     def main (self, source, poll) :
   321     def process_leases (self, leases) :
       
   322         """
       
   323             Handle given changed lease.
       
   324         """
       
   325         
       
   326         for lease in leases :
       
   327             log.info("%s", lease['lease'])
       
   328 
       
   329             self.leasedb.process(lease)
       
   330 
       
   331     def sync_leases (self, leases) :
       
   332         """
       
   333             Sync the entire given set of valid leases.
       
   334         """
       
   335         
       
   336         log.info("%d leases", len(leases))
       
   337 
       
   338         # XXX: any difference? Mostly removed leases, but... they should expire by themselves, right?
       
   339         self.process_leases(leases)
       
   340 
       
   341     def main (self, poll) :
   213         """
   342         """
   214             Read items from syslog source with given polling style.
   343             Read items from syslog source with given polling style.
   215 
   344 
   216             TODO: poll = false on SIGINT
   345             TODO: poll = false on SIGINT
   217         """
   346         """
   218         
   347         
   219         parser = pvl.syslog.parser.SyslogParser()
   348         parser = pvl.syslog.parser.SyslogParser()
   220         
   349         
   221         # mainloop
   350         # mainloop
   222         while True :
   351         while True :
   223             # process from source
   352             if self.syslog :
   224             for item in parser.process(source) :
   353                 # process from source
   225                 self.process(item)
   354                 for item in parser.process(self.syslog) :
       
   355                     self.process_syslog(item)
   226             
   356             
       
   357             if self.leases :
       
   358                 # process internally
       
   359                 #sync, leases = self.leases.process()
       
   360                 leases = self.leases.process()
       
   361 
       
   362                 #if sync :
       
   363                 #    self.sync_leases(leases)
       
   364                 if leases :
       
   365                     self.process_leases(leases)
       
   366                 #else :
       
   367                 #    pass
       
   368 
   227             if poll is False :
   369             if poll is False :
   228                 # done
   370                 # done
   229                 break
   371                 break
   230             else :
   372             else :
   231                 # wait
   373                 # wait
   240     if not options.database :
   382     if not options.database :
   241         log.error("No database given")
   383         log.error("No database given")
   242         return 1
   384         return 1
   243 
   385 
   244     log.info("Open up database: %s", options.database)
   386     log.info("Open up database: %s", options.database)
   245     database = DHCPHostsDatabase(options.database)
   387     database = pvl.verkko.db.Database(options.database)
   246 
   388    
   247     if options.create :
       
   248         database.create()
       
   249     
       
   250     # syslog
   389     # syslog
   251     log.info("Open up syslog...")
   390     log.info("Open up syslog...")
   252     syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog
   391     syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog
   253 
   392 
   254     if options.syslog_fifo :
   393     if options.syslog_fifo :
   255         source = pvl.syslog.fifo.Fifo(options.syslog_fifo)
   394         syslog = pvl.syslog.fifo.Fifo(options.syslog_fifo)
   256         poll = None # no timeout
   395         poll = None # no timeout
   257 
   396 
   258     elif options.syslog_tail :
   397     elif options.syslog_tail :
   259         # continuous file tail
   398         # continuous file tail
   260         source = pvl.syslog.tail.TailFile(options.syslog_file)
   399         syslog = pvl.syslog.tail.TailFile(options.syslog_file)
   261         poll = options.syslog_tail # polling interval
   400         poll = options.syslog_tail # polling interval
   262 
   401 
   263     elif options.syslog_file :
   402     elif options.syslog_file :
   264         # one-shot file read
   403         # one-shot file read
   265         source = pvl.syslog.tail.TailFile(options.syslog_file)
   404         syslog = pvl.syslog.tail.TailFile(options.syslog_file)
   266         poll = False # do not poll-loop
   405         poll = False # do not poll-loop
   267 
   406 
   268     else :
   407     else :
       
   408         syslog = None
       
   409         poll = False
   269         log.warning("No syslog source given")
   410         log.warning("No syslog source given")
   270         return 0
   411 
       
   412     # leases
       
   413     if options.leases_file :
       
   414         log.info("Open up DHCP leases...")
       
   415         leases = pvl.verkko.dhcp.leases.DHCPLeasesDatabase(options.leases_file)
       
   416 
       
   417         # force polling interval
       
   418         if options.leases_tail :
       
   419             # XXX: min between syslog/leases?
       
   420             poll = options.leases_tail
       
   421 
       
   422     else :
       
   423         leases = None
   271 
   424 
   272     # handler + main
   425     # handler + main
   273     handler = DHCPSyslogHandler(database)
   426     handler = DHCPHandler(database, syslog, leases)
   274 
   427 
       
   428     if options.create :
       
   429         handler.createdb()
       
   430  
   275     log.info("Enter mainloop...")
   431     log.info("Enter mainloop...")
   276     handler.main(source, poll)
   432     handler.main(poll)
   277     
   433     
   278     # done
   434     # done
   279     return 0
   435     return 0
   280 
   436 
   281 if __name__ == '__main__':
   437 if __name__ == '__main__':