148 |
148 |
149 def query (self) : |
149 def query (self) : |
150 return self.db.query(Host) |
150 return self.db.query(Host) |
151 |
151 |
152 def sort (self, hosts, default=HOST_SORT) : |
152 def sort (self, hosts, default=HOST_SORT) : |
153 self.sort = self.request.args.get('sort') |
153 sort = self.request.args.get('sort') |
154 |
154 |
155 if self.sort : |
155 if sort : |
156 sort = self.HOST_ATTRS[self.sort] |
156 order_by = self.HOST_ATTRS[sort] |
157 else : |
157 else : |
158 sort = default |
158 order_by = default |
159 |
159 |
160 log.debug("sort: %s", sort) |
160 log.debug("sort: %s", order_by) |
161 |
161 |
162 hosts = hosts.order_by(sort) |
162 hosts = hosts.order_by(order_by) |
163 |
163 |
164 # k |
164 # k |
165 return hosts |
165 return sort, hosts |
166 |
166 |
|
167 def filter_attr (self, attr, value) : |
|
168 """ |
|
169 Return filter expression for given attr == value |
|
170 """ |
|
171 |
|
172 if attr == 'seen' : |
|
173 column = Host.last_seen |
|
174 |
|
175 if value.isdigit() : |
|
176 # specific date |
|
177 date = datetime.datetime.strptime(value, Host.DATE_FMT).date() |
|
178 |
|
179 return db.between(date.strftime(Host.DATE_FMT), |
|
180 db.func.strftime(Host.DATE_FMT, Host.first_seen), |
|
181 db.func.strftime(Host.DATE_FMT, Host.last_seen) |
|
182 ) |
|
183 else : |
|
184 # recent |
|
185 timedelta = parse_timedelta(value) |
|
186 |
|
187 return ((db.func.now() - Host.last_seen) < timedelta) |
|
188 |
|
189 # XXX: for sqlite, pgsql should handle this natively? |
|
190 # to seconds |
|
191 #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds |
|
192 |
|
193 # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout |
|
194 #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout) |
|
195 |
|
196 elif attr == 'ip' : |
|
197 column = Host.ip |
|
198 |
|
199 # column is IPv4 string literal format... |
|
200 if '/' in value : |
|
201 return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value))) |
|
202 else : |
|
203 return (db.func.inet(Host.ip) == db.func.inet(value)) |
|
204 |
|
205 else : |
|
206 # preprocess |
|
207 like = False |
|
208 |
|
209 if value.endswith('*') : |
|
210 like = value.replace('*', '%') |
|
211 |
|
212 elif attr == 'mac' : |
|
213 value = Host.normalize_mac(value) |
|
214 |
|
215 # filter |
|
216 column = self.HOST_ATTRS[attr] |
|
217 |
|
218 if like : |
|
219 return (column.like(like)) |
|
220 else : |
|
221 return (column == value) |
|
222 |
|
223 def filter (self, hosts) : |
|
224 """ |
|
225 Apply filters from request.args against given hosts. |
|
226 |
|
227 Returns (filters, hosts). |
|
228 """ |
|
229 |
|
230 # filter? |
|
231 filters = {} |
|
232 |
|
233 for attr in self.HOST_ATTRS : |
|
234 values = self.request.args.getlist(attr) |
|
235 |
|
236 if not values : |
|
237 continue |
|
238 |
|
239 filter = db.or_(*[self.filter_attr(attr, value) for value in values]) |
|
240 |
|
241 log.debug("filter %s: %s", attr, filter) |
|
242 |
|
243 hosts = hosts.filter(filter) |
|
244 filters[attr] = values |
|
245 |
|
246 return filters, hosts |
|
247 |
|
248 def filters_title (self) : |
|
249 """ |
|
250 Return a string representing the applied filters. |
|
251 """ |
|
252 |
|
253 return ', '.join(value for values in self.filters.itervalues() for value in values) |
|
254 |
167 def render_hosts (self, hosts, title=None, filters=False, page=None, hilight=None) : |
255 def render_hosts (self, hosts, title=None, filters=False, page=None, hilight=None) : |
168 COLS = ( |
256 COLS = ( |
169 #title sort filter class |
257 #title sort filter class |
170 ('IP', 'ip', 'ip', 'ip' ), |
258 ('IP', 'ip', 'ip', 'ip' ), |
171 ('MAC', 'mac', 'mac', 'mac' ), |
259 ('MAC', 'mac', 'mac', 'mac' ), |
284 |
372 |
285 if filters is False : |
373 if filters is False : |
286 return table |
374 return table |
287 else : |
375 else : |
288 return html.form(method='get', action=self.url())( |
376 return html.form(method='get', action=self.url())( |
289 html.input(type='hidden', name='sort', value=self.sort), |
377 html.input(type='hidden', name='sort', value=self.sorts), |
290 table, |
378 table, |
291 ) |
379 ) |
292 |
380 |
293 class ItemHandler (BaseHandler) : |
381 class ItemHandler (BaseHandler) : |
294 def process (self, id) : |
382 def process (self, id) : |
295 self.hosts = self.query() |
383 self.hosts = self.query() |
296 self.host = self.hosts.get(id) |
384 self.host = self.hosts.get(id) |
297 |
385 |
298 if not self.host : |
386 if not self.host : |
299 raise web.NotFound("No such host: {id}".format(id=id)) |
387 raise web.NotFound("No such host: {id}".format(id=id)) |
300 |
388 |
301 self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac))) |
389 self.sorts, self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac))) |
302 |
390 |
303 def title (self) : |
391 def title (self) : |
304 return u"DHCP Host: {self.host}".format(self=self) |
392 return u"DHCP Host: {self.host}".format(self=self) |
305 |
393 |
306 def render_host (self, host) : |
394 def render_host (self, host) : |
360 ("Valid", dict(state=('DHCPACK', 'DHCPRELEASE'))), |
448 ("Valid", dict(state=('DHCPACK', 'DHCPRELEASE'))), |
361 ("Incomplete", dict(state=('DHCPDISCOVER', 'DHCPOFFER', 'DHCPREQUEST'))), |
449 ("Incomplete", dict(state=('DHCPDISCOVER', 'DHCPOFFER', 'DHCPREQUEST'))), |
362 ("Invalid", dict(state=('DHCPNAK', ))), |
450 ("Invalid", dict(state=('DHCPNAK', ))), |
363 ) |
451 ) |
364 |
452 |
365 def filter (self, attr, value) : |
|
366 """ |
|
367 Return filter expression for given attr == value |
|
368 """ |
|
369 |
|
370 if attr == 'seen' : |
|
371 column = Host.last_seen |
|
372 |
|
373 if value.isdigit() : |
|
374 # specific date |
|
375 date = datetime.datetime.strptime(value, Host.DATE_FMT).date() |
|
376 |
|
377 return db.between(date.strftime(Host.DATE_FMT), |
|
378 db.func.strftime(Host.DATE_FMT, Host.first_seen), |
|
379 db.func.strftime(Host.DATE_FMT, Host.last_seen) |
|
380 ) |
|
381 else : |
|
382 # recent |
|
383 timedelta = parse_timedelta(value) |
|
384 |
|
385 return ((db.func.now() - Host.last_seen) < timedelta) |
|
386 |
|
387 # XXX: for sqlite, pgsql should handle this natively? |
|
388 # to seconds |
|
389 #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds |
|
390 |
|
391 # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout |
|
392 #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout) |
|
393 |
|
394 elif attr == 'ip' : |
|
395 column = Host.ip |
|
396 |
|
397 # column is IPv4 string literal format... |
|
398 if '/' in value : |
|
399 return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value))) |
|
400 else : |
|
401 return (db.func.inet(Host.ip) == db.func.inet(value)) |
|
402 |
|
403 else : |
|
404 # preprocess |
|
405 like = False |
|
406 |
|
407 if value.endswith('*') : |
|
408 like = value.replace('*', '%') |
|
409 |
|
410 elif attr == 'mac' : |
|
411 value = Host.normalize_mac(value) |
|
412 |
|
413 # filter |
|
414 column = self.HOST_ATTRS[attr] |
|
415 |
|
416 if like : |
|
417 return (column.like(like)) |
|
418 else : |
|
419 return (column == value) |
|
420 |
|
421 def process (self) : |
453 def process (self) : |
422 hosts = self.query() |
454 hosts = self.query() |
423 |
455 |
424 # filter? |
456 # filter |
425 self.filters = {} |
457 self.filters, hosts = self.filter(hosts) |
426 |
|
427 for attr in self.HOST_ATTRS : |
|
428 values = self.request.args.getlist(attr) |
|
429 |
|
430 if not values : |
|
431 continue |
|
432 |
|
433 filter = db.or_(*[self.filter(attr, value) for value in values]) |
|
434 |
|
435 log.debug("filter %s: %s", attr, filter) |
|
436 |
|
437 hosts = hosts.filter(filter) |
|
438 self.filters[attr] = values |
|
439 |
458 |
440 # sort XXX: default per filter column? |
459 # sort XXX: default per filter column? |
441 hosts = self.sort(hosts) |
460 self.sorts, hosts = self.sort(hosts) |
442 |
461 |
443 # page? |
462 # page? |
444 self.page = self.request.args.get('page') |
463 self.page = self.request.args.get('page') |
445 |
464 |
446 if self.page : |
465 if self.page : |
447 self.page = int(self.page) |
466 self.page = int(self.page) |
448 |
467 |
449 hosts = hosts.offset(self.page * self.PAGE).limit(self.PAGE) |
468 hosts = hosts.offset(self.page * self.PAGE).limit(self.PAGE) |
450 |
469 |
451 self.hosts = hosts |
470 self.hosts = hosts |
452 |
471 |
453 def title (self) : |
472 def title (self) : |
454 if self.filters : |
473 if self.filters : |
455 return "DHCP Hosts: {filters}".format(filters=', '.join(value for values in self.filters.itervalues() for value in values)) |
474 return "DHCP Hosts: {filters}".format(filters=self.filters_title()) |
456 else : |
475 else : |
457 return "DHCP Hosts" |
476 return "DHCP Hosts" |
458 |
477 |
459 def render (self) : |
478 def render (self) : |
460 return ( |
479 return ( |
492 ( 'gw', 'Network', lambda host: host.gw ), |
511 ( 'gw', 'Network', lambda host: host.gw ), |
493 ( 'seen', 'Seen', Host.seen, ), |
512 ( 'seen', 'Seen', Host.seen, ), |
494 ( 'state', 'State', lambda host: host.state ), |
513 ( 'state', 'State', lambda host: host.state ), |
495 ) |
514 ) |
496 |
515 |
497 |
|
498 def process (self) : |
516 def process (self) : |
499 hosts = self.db.query(Host).order_by(Host.last_seen.desc()) |
517 """ |
|
518 Either return JSON (if ?t=...), or fetch hosts/t for rendering. |
|
519 """ |
|
520 |
|
521 hosts = self.db.query(Host) |
500 t = self.request.args.get('t') |
522 t = self.request.args.get('t') |
|
523 |
|
524 # always sorted by last_seen |
|
525 hosts = hosts.order_by(Host.last_seen.desc()) |
|
526 |
|
527 # filter |
|
528 self.filters, hosts = self.filter(hosts) |
501 |
529 |
502 def host_params (host) : |
530 def host_params (host) : |
503 yield 'id', host.id |
531 yield 'id', host.id |
504 |
532 |
505 for name, title, fvalue in self.COLUMNS : |
533 for name, title, fvalue in self.COLUMNS : |
543 # XXX: testing |
571 # XXX: testing |
544 self.hosts = self.hosts.offset(1) |
572 self.hosts = self.hosts.offset(1) |
545 |
573 |
546 self.t = self.hosts[0].last_seen |
574 self.t = self.hosts[0].last_seen |
547 |
575 |
|
576 def title (self) : |
|
577 if self.filters : |
|
578 return "{title}: {filters}".format(title=self.TITLE, filters=self.filters_title()) |
|
579 else : |
|
580 return self.TITLE |
|
581 |
548 def render (self) : |
582 def render (self) : |
|
583 """ |
|
584 Render page HTML and initial <table>, along with bootstrap JS (t0, configuration). |
|
585 """ |
|
586 |
549 def column (name, title, fvalue, host) : |
587 def column (name, title, fvalue, host) : |
550 cls = name |
588 cls = name |
551 |
589 |
552 if name == 'state' : |
590 if name == 'state' : |
553 cls = host.state_class() |
591 cls = host.state_class() |
554 |
592 |
555 return html.td(class_=cls)(fvalue(host)) |
593 return html.td(class_=cls)(fvalue(host)) |
556 |
594 |
557 params = dict( |
595 params = dict( |
558 url = self.url(), |
596 url = self.url(), |
|
597 filters = self.filters, |
559 t = dt2ts(self.t), |
598 t = dt2ts(self.t), |
560 host = self.url(ItemHandler, id='0'), |
599 host = self.url(ItemHandler, id='0'), |
561 columns = [name for name, title, fvalue in self.COLUMNS] |
600 columns = [name for name, title, fvalue in self.COLUMNS] |
562 ) |
601 ) |
563 params = json.dumps(params) |
602 params = json.dumps(params) |