bin/pvl.verkko-dhcp
changeset 15 66f81f4b6aa7
child 16 51509b5ce1c0
equal deleted inserted replaced
14:02c21749cb4f 15:66f81f4b6aa7
       
     1 #!/usr/bin/env python
       
     2 
       
     3 """
       
     4     Monitor DHCP use.
       
     5 """
       
     6 
       
     7 __version__ = '0.0'
       
     8 
       
     9 import pvl.args
       
    10 import pvl.syslog.args
       
    11 
       
    12 import pvl.verkko.db as db
       
    13 import pvl.syslog.dhcp
       
    14 
       
    15 import logging, optparse
       
    16 
       
    17 log = logging.getLogger('main')
       
    18 
       
    19 # name of process in syslog
       
    20 DHCP_SYSLOG_PROG = 'dhcpd'
       
    21 
       
    22 def parse_options (argv) :
       
    23     """
       
    24         Parse command-line arguments.
       
    25     """
       
    26 
       
    27     prog = argv[0]
       
    28 
       
    29     parser = optparse.OptionParser(
       
    30             prog        = prog,
       
    31             usage       = '%prog: [options]',
       
    32             version     = __version__,
       
    33 
       
    34             # module docstring
       
    35             description = __doc__,
       
    36     )
       
    37     
       
    38     # options
       
    39     parser.add_option_group(pvl.args.parser(parser))
       
    40 
       
    41     ## syslog
       
    42     parser.add_option_group(pvl.syslog.args.parser(parser, prog=DHCP_SYSLOG_PROG))
       
    43 
       
    44     ## XXX: networks
       
    45     parser.add_option('--network',              metavar='NET', action='append',
       
    46             help="Filter leases by network prefix as plugin instance")
       
    47 
       
    48     parser.add_option('--gateway',              metavar='GW/IFACE', action='append',
       
    49             help="Filter messages by gateway/interface as plugin instance")
       
    50 
       
    51     ## hosts
       
    52     parser.add_option('--database',             metavar='URI',
       
    53             help="Track hosts in given database")
       
    54 
       
    55     parser.add_option('--create',               action='store_true',
       
    56             help="Initialize database")
       
    57 
       
    58     # defaults
       
    59     parser.set_defaults(
       
    60 
       
    61     )
       
    62     
       
    63     # parse
       
    64     options, args = parser.parse_args(argv[1:])
       
    65     
       
    66     # apply
       
    67     pvl.args.apply(options, prog)
       
    68 
       
    69     if not options.database :
       
    70         parser.error("Missing required option: --database")
       
    71 
       
    72     return options, args
       
    73 
       
    74 class DHCPHostsDatabase (db.Database) :
       
    75 
       
    76     def create (self) :
       
    77         """
       
    78             CREATE TABLEs
       
    79         """
       
    80 
       
    81         log.info("Creating database tables: dhcp_hosts")
       
    82         db.dhcp_hosts.create(self.engine)
       
    83 
       
    84     def insert (self, attrs) :
       
    85         """
       
    86             INSERT new host
       
    87         """
       
    88 
       
    89         query = db.dhcp_hosts.insert().values(
       
    90                 ip          = attrs['ip'],
       
    91                 mac         = attrs['mac'],
       
    92                 gw          = attrs['gw'],
       
    93 
       
    94                 first_seen  = attrs['timestamp'],
       
    95 
       
    96                 name        = attrs['name'],
       
    97                 last_seen   = attrs['timestamp'],
       
    98                 last_msg    = attrs['state'],
       
    99         )
       
   100         result = self.engine.execute(query, **attrs)
       
   101         id, = result.inserted_primary_key
       
   102         
       
   103         return id
       
   104 
       
   105     def update (self, attrs) :
       
   106         """
       
   107             UPDATE existing host, or return False if not found.
       
   108         """
       
   109 
       
   110         table = db.dhcp_hosts
       
   111 
       
   112         query = table.update(
       
   113                 # where
       
   114                 (table.c.ip == attrs['ip']) & (table.c.mac == attrs['mac']) & (table.c.gw == attrs['gw']),
       
   115 
       
   116                 # set
       
   117                 name        = attrs['name'],
       
   118                 last_seen   = attrs['timestamp'],
       
   119                 last_msg    = attrs['state'],
       
   120         )
       
   121         result = self.engine.execute(query, **attrs)
       
   122         
       
   123         # any matched rows?
       
   124         return result.rowcount > 0
       
   125 
       
   126     def process (self, item) :
       
   127         """
       
   128             Process given DHCP syslog message to update the hosts table.
       
   129         """
       
   130         
       
   131         # ignore unless we have enough info to fully identify the client
       
   132         # this means that we omit DHCPDISCOVER messages, but we get the OFFER/REQUEST/ACK
       
   133         if any(name not in item for name in ('lease', 'hwaddr', 'gateway')) :
       
   134             # ignore; we require these
       
   135             return
       
   136 
       
   137         # db: syslog
       
   138         ATTR_MAP = (
       
   139             ('ip',          'lease'),
       
   140             ('mac',         'hwaddr'),
       
   141             ('gw',          'gateway'),
       
   142             ('name',        'hostname'),
       
   143 
       
   144             ('timestamp',   'timestamp'),
       
   145             ('state',       'type'),
       
   146             ('error',       'error'),
       
   147         )
       
   148 
       
   149         # attrs
       
   150         attrs = dict((key, item.get(name)) for key, name in ATTR_MAP)
       
   151 
       
   152         # update existing?
       
   153         if self.update(attrs) :
       
   154             log.info("Update: %s", attrs)
       
   155 
       
   156         else :
       
   157             # new
       
   158             log.info("Insert: %s", attrs)
       
   159             self.insert(attrs)
       
   160 
       
   161 class DHCPSyslogHandler (object) :
       
   162     """
       
   163         Process lines from syslog
       
   164     """
       
   165 
       
   166     def __init__ (self, db) :
       
   167         self.db = db
       
   168 
       
   169         # XXX
       
   170         self.filter = pvl.syslog.dhcp.DHCPSyslogFilter()
       
   171 
       
   172     def process (self, item) :
       
   173         """
       
   174             Handle a single item read from syslog to DB.
       
   175         filter = pvl.syslog.dhcp.DHCPSyslogFilter()
       
   176         """
       
   177 
       
   178         dhcp_item = self.filter.parse(item['msg'])
       
   179         
       
   180         if not dhcp_item :
       
   181             # ignore
       
   182             return
       
   183 
       
   184         dhcp_item['timestamp'] = item['timestamp'] # XXX: fixup DHCPSyslogParser?
       
   185 
       
   186         self.db.process(dhcp_item)
       
   187 
       
   188     def main (self, source, poll) :
       
   189         """
       
   190             Read items from syslog source with given polling style.
       
   191 
       
   192             TODO: poll = false on SIGINT
       
   193         """
       
   194         
       
   195         parser = pvl.syslog.parser.SyslogParser()
       
   196         
       
   197         # mainloop
       
   198         while True :
       
   199             # process from source
       
   200             for item in parser.process(source) :
       
   201                 self.process(item)
       
   202             
       
   203             if poll is False :
       
   204                 # done
       
   205                 break
       
   206             else :
       
   207                 # wait
       
   208                 source.poll(poll)
       
   209 
       
   210             log.debug("tick")
       
   211 
       
   212 def main (argv) :
       
   213     options, args = parse_options(argv)
       
   214 
       
   215     # syslog
       
   216     log.info("Open up syslog...")
       
   217     syslog_parser = pvl.syslog.parser.SyslogParser(prog=DHCP_SYSLOG_PROG) # filter by prog
       
   218 
       
   219     if options.syslog_fifo :
       
   220         source = pvl.syslog.fifo.Fifo(options.syslog_fifo)
       
   221         poll = None # no timeout
       
   222 
       
   223     elif options.syslog_tail :
       
   224         # continuous file tail
       
   225         source = pvl.syslog.tail.TailFile(options.syslog_file)
       
   226         poll = options.syslog_tail # polling interval
       
   227 
       
   228     elif options.syslog_file :
       
   229         # one-shot file read
       
   230         source = pvl.syslog.tail.TailFile(options.syslog_file)
       
   231         poll = False # do not poll-loop
       
   232 
       
   233     else :
       
   234         log.error("No syslog source given")
       
   235         return 1
       
   236     
       
   237     # db
       
   238     log.info("Open up database: %s", options.database)
       
   239     database = DHCPHostsDatabase(options.database)
       
   240 
       
   241     if options.create :
       
   242         database.create()
       
   243     
       
   244     # handler + main
       
   245     handler = DHCPSyslogHandler(database)
       
   246 
       
   247     log.info("Enter mainloop...")
       
   248     handler.main(source, poll)
       
   249     
       
   250     # done
       
   251     return 0
       
   252 
       
   253 if __name__ == '__main__':
       
   254     import sys
       
   255 
       
   256     sys.exit(main(sys.argv))