terom@236: #!/usr/bin/python terom@236: terom@236: """ terom@236: Analyze WLAN STA logs. terom@236: terom@236: Jul 3 23:05:04 buffalo-g300n-647682 daemon.info hostapd: wlan0-1: STA aa:bb:cc:dd:ee:ff WPA: group key handshake completed (RSN) terom@236: terom@236: """ terom@236: terom@236: __version__ = '0.1' terom@236: terom@236: import pvl.args terom@236: import pvl.syslog.args terom@243: import pvl.web.args terom@236: import pvl.rrd.hosts terom@243: import pvl.verkko.wlan terom@236: terom@236: import optparse terom@236: import logging; log = logging.getLogger('main') terom@236: terom@236: WLAN_STA_PROG = 'hostapd' terom@236: terom@236: def parse_argv (argv, doc = __doc__) : terom@236: """ terom@236: Parse command-line argv, returning (options, args). terom@236: """ terom@236: terom@236: prog = argv.pop(0) terom@236: args = argv terom@236: terom@236: # optparse terom@236: parser = optparse.OptionParser( terom@236: prog = prog, terom@236: usage = '%prog: [options] [ [...]]', terom@236: version = __version__, terom@236: description = doc, terom@236: ) terom@236: terom@236: # common terom@236: parser.add_option_group(pvl.args.parser(parser)) terom@236: parser.add_option_group(pvl.syslog.args.parser(parser, prog=WLAN_STA_PROG)) terom@238: parser.add_option_group(pvl.verkko.db.parser(parser, table=db.wlan_sta)) terom@243: parser.add_option_group(pvl.web.args.parser(parser)) terom@236: terom@236: parser.add_option('--interfaces', metavar='PATH', terom@236: help="Load interface/node names from mapping file") terom@236: terom@236: # parse terom@236: options, args = parser.parse_args(args) terom@236: terom@236: # apply terom@236: pvl.args.apply(options) terom@236: terom@236: return options, args terom@236: terom@236: import re terom@238: from pvl.verkko import db terom@236: terom@240: class KeyTimestampDatabase (object) : terom@240: """ terom@240: A pvl.verkko.db table that tracks events by key/timestamp. terom@240: """ terom@238: terom@240: DB_TABLE = None terom@240: DB_LAST_SEEN = None terom@240: DB_COUNT = None terom@240: DB_DISTINCT = None terom@240: terom@240: def __init__ (self, db) : terom@236: """ terom@240: db - pvl.verkko.db.Database terom@240: """ terom@240: terom@238: self.db = db terom@236: terom@240: def select (self, interval=None) : terom@238: """ terom@238: SELECT unique gw/ip hosts, for given interval. terom@238: """ terom@238: terom@240: query = db.select(self.DB_DISTINCT, distinct=True) terom@238: terom@238: if interval : terom@238: # timedelta terom@238: query = query.where(db.func.now() - self.DB_LAST_SEEN < interval) terom@238: terom@238: return self.db.select(query) terom@238: terom@240: def insert (self, key, timestamp, update) : terom@238: """ terom@240: INSERT new row. terom@238: """ terom@238: terom@238: query = self.DB_TABLE.insert().values(**key).values(**update).values( terom@238: first_seen = timestamp, terom@238: last_seen = timestamp, terom@238: ) terom@238: terom@244: if self.DB_COUNT is not None : terom@238: query = query.values(count=1) terom@238: terom@238: # -> id terom@238: return self.db.insert(query) terom@238: terom@240: def update (self, key, timestamp, update) : terom@238: """ terom@240: UPDATE existing row, or return False if not found. terom@238: """ terom@238: terom@238: table = self.DB_TABLE terom@238: query = table.update() terom@238: terom@238: for col, value in key.iteritems() : terom@238: query = query.where(table.c[col] == value) terom@238: terom@238: query = query.values(last_seen=timestamp) terom@238: terom@240: if self.DB_COUNT is not None : terom@240: query = query.values(count=db.func.coalesce(self.DB_COUNT, 0) + 1) terom@238: terom@238: query = query.values(**update) terom@238: terom@238: # -> any matched rows? terom@238: return self.db.update(query) terom@238: terom@240: def touch (self, key, timestamp, update, **opts) : terom@238: # update existing? terom@240: if self.update(key, timestamp, update, **opts) : terom@240: log.info("Update: %s: %s: %s", key, timestamp, update) terom@238: else : terom@240: log.info("Insert: %s: %s: %s", key, timestamp, update) terom@240: self.insert(key, timestamp, update, **opts) terom@240: terom@240: terom@240: class WlanStaDatabase (KeyTimestampDatabase) : terom@240: HOSTAPD_STA_RE = re.compile(r'(?P.+?): STA (?P.+?) (?P.+)') terom@240: terom@240: DB_TABLE = db.wlan_sta terom@240: DB_LAST_SEEN = db.wlan_sta.c.last_seen terom@240: DB_COUNT = db.wlan_sta.c.count terom@240: terom@240: DB_DISTINCT = (db.wlan_sta.c.sta, ) terom@240: terom@240: def __init__ (self, db, interface_map=None) : terom@240: """ terom@240: interface_map - {(hostname, interface): (nodename, wlan)} terom@240: """ terom@240: KeyTimestampDatabase.__init__(self, db) terom@240: self.interface_map = interface_map terom@238: terom@236: def parse (self, item) : terom@236: """ terom@236: Parse fields from a hostapd syslog message. terom@236: """ terom@236: terom@236: match = self.HOSTAPD_STA_RE.match(item['msg']) terom@236: terom@236: if not match : terom@236: return None terom@236: terom@238: return match.groupdict() terom@236: terom@240: def lookup_wlan (self, host, iface) : terom@240: """ terom@240: Lookup ap/ssid by host/iface. terom@240: """ terom@240: mapping = None terom@240: if self.interface_map : terom@240: mapping = self.interface_map.get((host, iface)) terom@240: terom@240: if mapping : terom@240: return mapping terom@240: else : terom@240: # as-is terom@240: log.warning("Unknown host/iface: %s/%s", host, iface) terom@240: return host, iface terom@240: terom@238: def __call__ (self, item) : terom@238: match = self.parse(item) terom@238: terom@238: if not match : terom@238: return terom@238: terom@238: # lookup? terom@240: ap, ssid = self.lookup_wlan(item['host'], match['wlan']) terom@236: terom@238: # update/insert terom@238: self.touch( terom@240: dict(ap=ap, wlan=ssid, sta=match['sta']), terom@240: item['timestamp'], terom@240: dict(msg=match['msg']), terom@236: ) terom@236: terom@236: def main (argv) : terom@236: options, args = parse_argv(argv) terom@238: terom@238: # database terom@238: db = pvl.verkko.db.apply(options) terom@236: terom@236: if options.interfaces : terom@236: interfaces = dict(pvl.rrd.hosts.map_interfaces(options, open(options.interfaces))) terom@236: else : terom@236: interfaces = None terom@236: terom@236: # syslog terom@236: log.info("Open up syslog...") terom@243: syslog = pvl.syslog.args.apply(options, optional=True) terom@243: terom@243: if syslog : terom@243: # handler terom@243: handler = WlanStaDatabase(db, interface_map=interfaces) terom@238: terom@243: log.info("Enter mainloop...") terom@243: for source in syslog.main() : terom@243: for item in source: terom@243: handler(item) terom@243: else : terom@243: # run web terom@243: application = pvl.web.args.apply(options, pvl.verkko.wlan.Application, db) terom@243: return pvl.web.args.main(options, application) terom@236: terom@236: return 0 terom@236: terom@236: if __name__ == '__main__': terom@236: import sys terom@236: terom@236: sys.exit(main(sys.argv))