1 #!/usr/bin/python |
|
2 |
|
3 """ |
|
4 Analyze WLAN STA logs. |
|
5 |
|
6 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) |
|
7 |
|
8 """ |
|
9 |
|
10 __version__ = '0.1' |
|
11 |
|
12 import pvl.args |
|
13 import pvl.syslog.args |
|
14 import pvl.rrd.hosts |
|
15 |
|
16 import optparse |
|
17 import logging; log = logging.getLogger('main') |
|
18 |
|
19 WLAN_STA_PROG = 'hostapd' |
|
20 |
|
21 def parse_argv (argv, doc = __doc__) : |
|
22 """ |
|
23 Parse command-line argv, returning (options, args). |
|
24 """ |
|
25 |
|
26 prog = argv.pop(0) |
|
27 args = argv |
|
28 |
|
29 # optparse |
|
30 parser = optparse.OptionParser( |
|
31 prog = prog, |
|
32 usage = '%prog: [options] [<input.txt> [...]]', |
|
33 version = __version__, |
|
34 description = doc, |
|
35 ) |
|
36 |
|
37 # common |
|
38 parser.add_option_group(pvl.args.parser(parser)) |
|
39 parser.add_option_group(pvl.syslog.args.parser(parser, prog=WLAN_STA_PROG)) |
|
40 parser.add_option_group(pvl.verkko.db.parser(parser, table=db.wlan_sta)) |
|
41 |
|
42 parser.add_option('--interfaces', metavar='PATH', |
|
43 help="Load interface/node names from mapping file") |
|
44 |
|
45 # parse |
|
46 options, args = parser.parse_args(args) |
|
47 |
|
48 # apply |
|
49 pvl.args.apply(options) |
|
50 |
|
51 return options, args |
|
52 |
|
53 import re |
|
54 from pvl.verkko import db |
|
55 |
|
56 class WlanStaDatabase (object) : |
|
57 HOSTAPD_STA_RE = re.compile(r'(?P<wlan>.+?): STA (?P<sta>.+?) (?P<msg>.+)') |
|
58 |
|
59 DB_TABLE = db.wlan_sta |
|
60 DB_LAST_SEEN = db.wlan_sta.c.last_seen |
|
61 DB_SELECT = (db.dhcp_hosts.c.gw, db.dhcp_hosts.c.ip) |
|
62 |
|
63 def __init__ (self, db, interface_map=None) : |
|
64 """ |
|
65 interface_map - {(hostname, interface): (nodename, wlan)} |
|
66 """ |
|
67 self.db = db |
|
68 self.interface_map = interface_map |
|
69 |
|
70 |
|
71 def select (self, distinct=DB_SELECT, interval=None) : |
|
72 """ |
|
73 SELECT unique gw/ip hosts, for given interval. |
|
74 """ |
|
75 |
|
76 query = db.select(distinct, distinct=True) |
|
77 |
|
78 if interval : |
|
79 # timedelta |
|
80 query = query.where(db.func.now() - self.DB_LAST_SEEN < interval) |
|
81 |
|
82 return self.db.select(query) |
|
83 |
|
84 def insert (self, key, update, timestamp, count=True) : |
|
85 """ |
|
86 INSERT new host |
|
87 """ |
|
88 |
|
89 query = self.DB_TABLE.insert().values(**key).values(**update).values( |
|
90 first_seen = timestamp, |
|
91 last_seen = timestamp, |
|
92 ) |
|
93 |
|
94 if count : |
|
95 query = query.values(count=1) |
|
96 |
|
97 # -> id |
|
98 return self.db.insert(query) |
|
99 |
|
100 def update (self, key, update, timestamp, count=True) : |
|
101 """ |
|
102 UPDATE existing host, or return False if not found. |
|
103 """ |
|
104 |
|
105 table = self.DB_TABLE |
|
106 query = table.update() |
|
107 |
|
108 for col, value in key.iteritems() : |
|
109 query = query.where(table.c[col] == value) |
|
110 |
|
111 query = query.values(last_seen=timestamp) |
|
112 |
|
113 if count : |
|
114 query = query.values(count=db.func.coalesce(table.c.count, 0) + 1) |
|
115 |
|
116 query = query.values(**update) |
|
117 |
|
118 # -> any matched rows? |
|
119 return self.db.update(query) |
|
120 |
|
121 def touch (self, key, update, timestamp, **opts) : |
|
122 # update existing? |
|
123 if self.update(key, update, timestamp, **opts) : |
|
124 log.info("Update: %s: %s: %s", key, update, timestamp) |
|
125 else : |
|
126 log.info("Insert: %s: %s: %s", key, update, timestamp) |
|
127 self.insert(key, update, timestamp, **opts) |
|
128 |
|
129 def parse (self, item) : |
|
130 """ |
|
131 Parse fields from a hostapd syslog message. |
|
132 """ |
|
133 |
|
134 match = self.HOSTAPD_STA_RE.match(item['msg']) |
|
135 |
|
136 if not match : |
|
137 return None |
|
138 |
|
139 return match.groupdict() |
|
140 |
|
141 def __call__ (self, item) : |
|
142 match = self.parse(item) |
|
143 |
|
144 if not match : |
|
145 return |
|
146 |
|
147 # lookup? |
|
148 ap, wlan = item['host'], match['wlan'] |
|
149 |
|
150 if self.interface_map : |
|
151 mapping = self.interface_map.get((ap, wlan)) |
|
152 |
|
153 if mapping : |
|
154 ap, wlan = mapping |
|
155 |
|
156 # update/insert |
|
157 self.touch( |
|
158 dict( |
|
159 ap = ap, |
|
160 wlan = wlan, |
|
161 sta = match['sta'], |
|
162 ), dict( |
|
163 msg = match['msg'], |
|
164 ), item['timestamp'] |
|
165 ) |
|
166 |
|
167 def main (argv) : |
|
168 options, args = parse_argv(argv) |
|
169 |
|
170 # database |
|
171 db = pvl.verkko.db.apply(options) |
|
172 |
|
173 if options.interfaces : |
|
174 interfaces = dict(pvl.rrd.hosts.map_interfaces(options, open(options.interfaces))) |
|
175 else : |
|
176 interfaces = None |
|
177 |
|
178 # syslog |
|
179 log.info("Open up syslog...") |
|
180 syslog = pvl.syslog.args.apply(options) |
|
181 |
|
182 # handler |
|
183 handler = WlanStaDatabase(db, interface_map=interfaces) |
|
184 |
|
185 log.info("Enter mainloop...") |
|
186 for source in syslog.main() : |
|
187 for item in source: |
|
188 handler(item) |
|
189 |
|
190 return 0 |
|
191 |
|
192 if __name__ == '__main__': |
|
193 import sys |
|
194 |
|
195 sys.exit(main(sys.argv)) |
|