1 from pvl.verkko import db, web |
1 from pvl.verkko import db, web |
|
2 from pvl.verkko.utils import parse_timedelta, IPv4Network |
2 |
3 |
3 from pvl.html import tags as html |
4 from pvl.html import tags as html |
4 |
5 |
5 import re |
6 import re |
|
7 import datetime |
6 import socket # dns |
8 import socket # dns |
7 |
9 |
8 import logging; log = logging.getLogger('pvl.verkko.hosts') |
10 import logging; log = logging.getLogger('pvl.verkko.hosts') |
9 |
11 |
10 # XXX: this should actually be DHCPHost |
12 # XXX: this should actually be DHCPHost |
12 DATE_FMT = '%Y%m%d' |
14 DATE_FMT = '%Y%m%d' |
13 |
15 |
14 MAC_HEX = r'([A-Za-z0-9]{2})' |
16 MAC_HEX = r'([A-Za-z0-9]{2})' |
15 MAC_SEP = r'[-:.]?' |
17 MAC_SEP = r'[-:.]?' |
16 MAC_RE = re.compile(MAC_SEP.join([MAC_HEX] * 6)) |
18 MAC_RE = re.compile(MAC_SEP.join([MAC_HEX] * 6)) |
|
19 |
|
20 @classmethod |
|
21 def query (cls, session, seen=None) : |
|
22 """ |
|
23 seen - select hosts seen during given timedelta period |
|
24 """ |
|
25 |
|
26 query = session.query(cls) |
|
27 |
|
28 return query |
17 |
29 |
18 @classmethod |
30 @classmethod |
19 def normalize_mac (cls, mac) : |
31 def normalize_mac (cls, mac) : |
20 match = cls.MAC_RE.search(mac) |
32 match = cls.MAC_RE.search(mac) |
21 |
33 |
43 def render_name (self) : |
55 def render_name (self) : |
44 if self.name : |
56 if self.name : |
45 return self.name.decode('ascii', 'replace') |
57 return self.name.decode('ascii', 'replace') |
46 else : |
58 else : |
47 return None |
59 return None |
|
60 |
|
61 STATES = { |
|
62 'DHCPACK': 'ack', |
|
63 'DHCPNAK': 'nak', |
|
64 'DHCPRELEASE': 'release', |
|
65 'DHCPDISCOVER': 'search', |
|
66 'DHCPREQUEST': 'search', |
|
67 'DHCPOFFER': 'search', |
|
68 } |
|
69 |
|
70 def state_class (self) : |
|
71 if self.state in self.STATES : |
|
72 return 'dhcp-' + self.STATES[self.state] |
|
73 |
|
74 else : |
|
75 return None |
48 |
76 |
49 def when (self) : |
77 def when (self) : |
50 return '{frm} - {to}'.format( |
78 return '{frm} - {to}'.format( |
51 frm = self.first_seen.strftime(self.DATE_FMT), |
79 frm = self.first_seen.strftime(self.DATE_FMT), |
52 to = self.last_seen.strftime(self.DATE_FMT), |
80 to = self.last_seen.strftime(self.DATE_FMT), |
73 def __unicode__ (self) : |
101 def __unicode__ (self) : |
74 return u"{host.ip} ({host.mac})".format(host=self) |
102 return u"{host.ip} ({host.mac})".format(host=self) |
75 |
103 |
76 db.mapper(Host, db.dhcp_hosts, properties=dict( |
104 db.mapper(Host, db.dhcp_hosts, properties=dict( |
77 id = db.dhcp_hosts.c.rowid, |
105 id = db.dhcp_hosts.c.rowid, |
78 #_mac = db.dhcp_hosts.c.mac, |
106 state = db.dhcp_hosts.c.last_msg, |
79 #_name = db.dhcp_hosts.c.name, |
|
80 )) |
107 )) |
81 |
108 |
82 class BaseHandler (web.Handler) : |
109 class BaseHandler (web.Handler) : |
83 HOST_ATTRS = { |
110 HOST_ATTRS = { |
84 'id': Host.id, |
111 'id': Host.id, |
85 'ip': Host.ip, |
112 'net': Host.gw, |
86 'mac': Host.mac, |
113 'ip': Host.ip, |
87 'name': Host.name, |
114 'mac': Host.mac, |
88 'seen': Host.last_seen, |
115 'name': Host.name, |
|
116 'seen': Host.last_seen, |
|
117 'state': Host.state, |
89 } |
118 } |
90 |
119 |
91 HOST_SORT = Host.last_seen.desc() |
120 HOST_SORT = Host.last_seen.desc() |
92 |
121 |
93 def query (self) : |
122 def query (self) : |
112 COLS = ( |
141 COLS = ( |
113 #title sort filter class |
142 #title sort filter class |
114 ('IP', 'ip', 'ip', 'ip' ), |
143 ('IP', 'ip', 'ip', 'ip' ), |
115 ('MAC', 'mac', 'mac', 'mac' ), |
144 ('MAC', 'mac', 'mac', 'mac' ), |
116 ('Hostname', 'name', False, False ), |
145 ('Hostname', 'name', False, False ), |
117 ('Seen', 'seen', False, False ), |
146 ('Network', 'net', 'net', False ), |
|
147 ('Seen', 'seen', 'seen', False ), |
|
148 ('State', 'state', 'state', False ), |
118 ) |
149 ) |
119 |
150 |
120 def url (**opts) : |
151 def url (**opts) : |
121 args = dict() |
152 args = dict() |
122 |
153 |
165 html.a(href=self.url(ListHandler, mac=host.mac))( |
196 html.a(href=self.url(ListHandler, mac=host.mac))( |
166 host.render_mac() |
197 host.render_mac() |
167 ) |
198 ) |
168 ), |
199 ), |
169 html.td(host.render_name()), |
200 html.td(host.render_name()), |
|
201 html.td( |
|
202 host.gw |
|
203 ), |
170 html.td(host.when()), |
204 html.td(host.when()), |
|
205 html.td(class_=host.state_class())(host.state), |
171 ) for i, host in enumerate(hosts) |
206 ) for i, host in enumerate(hosts) |
172 ), |
207 ), |
173 html.tfoot( |
208 html.tfoot( |
174 html.tr( |
209 html.tr( |
175 html.td(colspan=(1 + len(COLS)))( |
210 html.td(colspan=(1 + len(COLS)))( |
188 table, |
223 table, |
189 ) |
224 ) |
190 |
225 |
191 def render_host (self, host, hosts) : |
226 def render_host (self, host, hosts) : |
192 attrs = ( |
227 attrs = ( |
|
228 ('Network', host.gw), |
193 ('IP', host.ip), |
229 ('IP', host.ip), |
194 ('MAC', host.mac), |
230 ('MAC', host.mac), |
195 ('Hostname', host.name), |
231 ('Hostname', host.name), |
196 ('DNS', host.dns()), |
232 ('DNS', host.dns()), |
197 ('First seen', host.first_seen), |
233 ('First seen', host.first_seen), |
198 ('Last seen', host.last_seen), |
234 ('Last seen', host.last_seen), |
|
235 ('Last state', host.state), |
199 ) |
236 ) |
200 |
237 |
201 return ( |
238 return ( |
202 html.h2('Host'), |
239 html.h2('Host'), |
203 html.dl( |
240 html.dl( |
226 def render (self) : |
263 def render (self) : |
227 return self.render_host(self.host, self.hosts) |
264 return self.render_host(self.host, self.hosts) |
228 |
265 |
229 class ListHandler (BaseHandler) : |
266 class ListHandler (BaseHandler) : |
230 def process (self) : |
267 def process (self) : |
231 self.hosts = self.query() |
268 hosts = self.query() |
232 |
269 |
233 # filter? |
270 # filter? |
234 self.filters = {} |
271 self.filters = {} |
235 |
272 |
236 for attr in self.HOST_ATTRS : |
273 for attr in self.HOST_ATTRS : |
237 value = self.request.args.get(attr) |
274 value = self.request.args.get(attr) |
238 |
275 |
239 if not value : |
276 if not value : |
240 continue |
277 continue |
241 |
278 |
242 # preprocess |
279 if attr == 'seen' : |
243 like = False |
280 if value.isdigit() : |
244 |
281 # specific date |
245 if value.endswith('*') : |
282 date = datetime.datetime.strptime(value, Host.DATE_FMT).date() |
246 like = value.replace('*', '%') |
283 |
247 |
284 filter = db.between(date.strftime(Host.DATE_FMT), |
248 elif attr == 'mac' : |
285 db.func.strftime(Host.DATE_FMT, Host.first_seen), |
249 value = Host.normalize_mac(value) |
286 db.func.strftime(Host.DATE_FMT, Host.last_seen) |
250 |
287 ) |
251 # filter |
288 else : |
252 col = self.HOST_ATTRS[attr] |
289 # recent |
253 |
290 timedelta = parse_timedelta(value) |
254 if like : |
291 |
255 filter = (col.like(like)) |
292 # to seconds |
|
293 timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds |
|
294 |
|
295 # XXX: for sqlite, pgsql should handle this natively? |
|
296 # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout |
|
297 filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout) |
|
298 |
|
299 elif attr == 'ip' : |
|
300 # parse as network expression |
|
301 ip = IPv4Network(value) |
|
302 |
|
303 if ip.masklen == 32 : |
|
304 filter = (Host.ip == value) |
|
305 else : |
|
306 # XXX: column is IPv4 string literal format... |
|
307 filter = ((Host.ip.op('&')(ip.mask)) == ip.base) |
|
308 |
256 else : |
309 else : |
257 filter = (col == value) |
310 # preprocess |
|
311 like = False |
|
312 |
|
313 if value.endswith('*') : |
|
314 like = value.replace('*', '%') |
|
315 |
|
316 elif attr == 'mac' : |
|
317 value = Host.normalize_mac(value) |
|
318 |
|
319 # filter |
|
320 col = self.HOST_ATTRS[attr] |
|
321 |
|
322 if like : |
|
323 filter = (col.like(like)) |
|
324 else : |
|
325 filter = (col == value) |
258 |
326 |
259 self.hosts = self.hosts.filter(filter) |
327 hosts = hosts.filter(filter) |
260 self.filters[attr] = value |
328 self.filters[attr] = value |
|
329 |
|
330 self.hosts = hosts |
261 |
331 |
262 def title (self) : |
332 def title (self) : |
263 if self.filters : |
333 if self.filters : |
264 return "DHCP Hosts: {filters}".format(filters=', '.join(self.filters.itervalues())) |
334 return "DHCP Hosts: {filters}".format(filters=', '.join(self.filters.itervalues())) |
265 else : |
335 else : |