terom@180: from pvl.verkko import web, db, table terom@14: from pvl.verkko.utils import parse_timedelta, IPv4Network terom@0: terom@199: from pvl.web import html, response terom@0: terom@8: import re terom@14: import datetime terom@0: import socket # dns terom@0: terom@0: import logging; log = logging.getLogger('pvl.verkko.hosts') terom@0: terom@178: ## Model terom@178: import json terom@178: import time terom@178: terom@178: def dt2ts (dt) : terom@178: return int(time.mktime(dt.timetuple())) terom@178: terom@178: def ts2dt (ts) : terom@178: return datetime.datetime.fromtimestamp(ts) terom@178: terom@178: # TODO: this should be DHCPHost terom@0: class Host (object) : terom@0: DATE_FMT = '%Y%m%d' terom@27: TIME_FMT = '%H:%M:%S' terom@8: terom@8: MAC_HEX = r'([A-Za-z0-9]{2})' terom@8: MAC_SEP = r'[-:.]?' terom@8: MAC_RE = re.compile(MAC_SEP.join([MAC_HEX] * 6)) terom@8: terom@8: @classmethod terom@8: def normalize_mac (cls, mac) : terom@8: match = cls.MAC_RE.search(mac) terom@8: terom@8: if not match : terom@8: raise ValueError(mac) terom@8: terom@8: else : terom@8: return ':'.join(hh.lower() for hh in match.groups()) terom@0: terom@0: def __init__ (self, ip, mac, name=None) : terom@0: self.ip = ip terom@0: self.mac = mac terom@0: self.name = name terom@0: terom@0: def render_mac (self) : terom@0: if not self.mac : terom@0: return None terom@0: terom@0: elif len(self.mac) > (6 * 2 + 5) : terom@0: return u'???' terom@0: terom@0: else : terom@0: return unicode(self.mac) terom@0: terom@179: def network (self) : terom@179: return self.gw terom@179: terom@0: def render_name (self) : terom@0: if self.name : terom@0: return self.name.decode('ascii', 'replace') terom@0: else : terom@0: return None terom@14: terom@14: STATES = { terom@14: 'DHCPACK': 'ack', terom@14: 'DHCPNAK': 'nak', terom@14: 'DHCPRELEASE': 'release', terom@14: 'DHCPDISCOVER': 'search', terom@14: 'DHCPREQUEST': 'search', terom@14: 'DHCPOFFER': 'search', terom@14: } terom@14: terom@14: def state_class (self) : terom@16: if self.error : terom@16: return 'dhcp-error' terom@16: terom@16: elif self.state in self.STATES : terom@14: return 'dhcp-' + self.STATES[self.state] terom@14: terom@14: else : terom@14: return None terom@0: terom@16: def state_title (self) : terom@16: return self.error # or None terom@16: terom@16: def render_state (self) : terom@16: if self.error : terom@16: return "{self.state}: {self.error}".format(self=self) terom@16: else : terom@16: return self.state terom@27: terom@27: @classmethod terom@27: def format_datetime (cls, dt) : terom@27: if (datetime.datetime.now() - dt).days : terom@27: return dt.strftime(cls.DATE_FMT) terom@27: terom@27: else : terom@27: return dt.strftime(cls.TIME_FMT) terom@179: terom@26: def seen (self) : terom@16: return ( terom@27: html.span(title=self.first_seen)(self.format_datetime(self.first_seen)), terom@16: '-', terom@27: html.span(title=self.last_seen)(self.format_datetime(self.last_seen)) terom@0: ) terom@0: terom@0: def dns (self) : terom@0: """ terom@0: Reverse-DNS lookup. terom@0: """ terom@0: terom@0: if not self.ip : terom@0: return None terom@0: terom@0: sockaddrs = set(sockaddr for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(self.ip, 0, 0, 0, 0, socket.AI_NUMERICHOST)) terom@0: terom@0: for sockaddr in sockaddrs : terom@0: try : terom@0: host, port = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD) terom@0: except socket.gaierror : terom@0: continue terom@0: terom@0: return host terom@0: terom@0: def __unicode__ (self) : terom@0: return u"{host.ip} ({host.mac})".format(host=self) terom@0: terom@0: db.mapper(Host, db.dhcp_hosts, properties=dict( terom@16: #id = db.dhcp_hosts.c.rowid, terom@16: #state = db.dhcp_hosts.c., terom@0: )) terom@0: terom@178: ## Controller terom@180: class HostsTable (table.Table) : terom@179: """ terom@186: of hosts. terom@179: """ terom@179: terom@183: ITEMS = "Hosts" terom@180: COLUMNS = ( terom@180: table.Column('ip', "IP", Host.ip, terom@180: rowfilter = True, terom@180: ), terom@180: table.Column('mac', "MAC", Host.mac, Host.render_mac, terom@180: rowfilter = True, terom@180: ), terom@180: table.Column('name', "Hostname", Host.name, Host.render_name, ), terom@180: table.Column('gw', "Network", Host.gw, Host.network, ), terom@180: table.Column('seen', "Seen", Host.last_seen, Host.seen, ), terom@180: table.Column('state', "State", Host.count, terom@180: rowtitle = Host.state_title, terom@180: rowcss = Host.state_class, terom@180: ), terom@178: ) terom@179: terom@180: # XXX: have to set again terom@180: ATTRS = dict((col.attr, col) for col in COLUMNS) terom@180: terom@180: # default terom@180: SORT = Host.last_seen.desc() terom@180: PAGE = 10 terom@180: terom@180: class HostsHandler (table.TableHandler, web.DatabaseHandler) : terom@180: """ terom@180: Combined database +
terom@180: """ terom@180: terom@180: CSS = web.DatabaseHandler.CSS + table.TableHandler.CSS + ( terom@180: "/static/dhcp/hosts.css", terom@179: ) terom@179: terom@186: # view terom@180: TABLE = HostsTable terom@0: terom@5: def query (self) : terom@179: """ terom@179: Database SELECT query. terom@179: """ terom@179: terom@180: return self.db.query(Host) terom@36: terom@179: def filter_seen (self, value) : terom@179: """ terom@179: Return filter expression for given attr == value terom@179: """ terom@179: terom@179: column = Host.last_seen terom@179: terom@179: if value.isdigit() : terom@179: # specific date terom@179: date = datetime.datetime.strptime(value, Host.DATE_FMT).date() terom@179: terom@179: return db.between(date.strftime(Host.DATE_FMT), terom@179: db.func.strftime(Host.DATE_FMT, Host.first_seen), terom@179: db.func.strftime(Host.DATE_FMT, Host.last_seen) terom@179: ) terom@179: else : terom@179: # recent terom@179: timedelta = parse_timedelta(value) terom@179: terom@179: return ((db.func.now() - Host.last_seen) < timedelta) terom@179: terom@179: # XXX: for sqlite, pgsql should handle this natively? terom@179: # to seconds terom@179: #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds terom@179: terom@179: # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout terom@179: #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout) terom@179: terom@179: def filter_ip (self, value) : terom@179: column = Host.ip terom@179: terom@179: # column is IPv4 string literal format... terom@179: if '/' in value : terom@179: return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value))) terom@179: else : terom@179: return (db.func.inet(Host.ip) == db.func.inet(value)) terom@179: terom@179: def filter_mac (self, value) : terom@179: return self.filter_attr('mac', Host.normalize_mac(value)) terom@179: terom@180: class ItemHandler (HostsHandler) : terom@178: """ terom@178: A specific DHCP host, along with a list of related hosts. terom@178: """ terom@179: terom@5: def process (self, id) : terom@5: self.hosts = self.query() terom@5: self.host = self.hosts.get(id) terom@5: terom@5: if not self.host : terom@5: raise web.NotFound("No such host: {id}".format(id=id)) terom@30: terom@30: self.sorts, self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac))) terom@5: terom@5: def title (self) : terom@5: return u"DHCP Host: {self.host}".format(self=self) terom@5: terom@20: def render_host (self, host) : terom@20: """ terom@20: Details for specific host. terom@20: """ terom@20: terom@20: attrs = ( terom@20: ('Network', host.gw), terom@20: ('IP', host.ip), terom@20: ('MAC', host.mac), terom@20: ('Hostname', host.name), terom@20: ('DNS', host.dns()), terom@20: ('First seen', host.first_seen), terom@20: ('Last seen', host.last_seen), terom@20: ('Last state', host.render_state()), terom@20: ('Total messages', host.count), terom@20: ) terom@20: terom@20: return ( terom@20: html.dl( terom@20: (html.dt(title), html.dd(value)) for title, value in attrs terom@20: ) terom@20: ) terom@20: terom@5: def render (self) : terom@20: return ( terom@20: html.h2('Host'), terom@20: self.render_host(self.host), terom@20: terom@20: html.h2('Related'), terom@179: self.render_table(self.hosts, sort=self.sorts, hilight=dict(ip=self.host.ip, mac=self.host.mac)), terom@20: terom@20: html.a(href=self.url(ListHandler))(html('«'), 'Back'), terom@20: ) terom@20: terom@180: class ListHandler (HostsHandler) : terom@178: """ terom@178: List of DHCP hosts for given filter. terom@178: """ terom@178: terom@180: TABLE_ITEM_URL = ItemHandler terom@16: terom@8: def process (self) : terom@180: # super terom@180: table.TableHandler.process(self) terom@180: terom@5: def title (self) : terom@8: if self.filters : terom@30: return "DHCP Hosts: {filters}".format(filters=self.filters_title()) terom@8: else : terom@8: return "DHCP Hosts" terom@180: terom@5: def render (self) : terom@8: return ( terom@180: self.render_table(self.query, filters=self.filters, sort=self.sorts, page=self.page), terom@8: terom@180: #html.a(href=self.url())(html('«'), 'Back') if self.filters else None, terom@8: ) terom@25: terom@180: class RealtimeHandler (HostsHandler) : terom@199: TITLE = "DHCP Hosts: Pseudo-Realtime.." terom@180: CSS = HostsHandler.CSS + ( terom@28: 'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css', terom@28: ) terom@25: JS = ( terom@25: #"/static/jquery/jquery.js", terom@25: 'http://code.jquery.com/jquery-1.8.2.js', terom@28: 'http://code.jquery.com/ui/1.9.0/jquery-ui.js', terom@158: '/static/dhcp/spin.js', terom@205: '/static/dhcp/table.js', terom@158: '/static/dhcp/hosts.js', terom@25: ) terom@25: terom@29: COLUMNS = ( terom@29: ( 'ip', 'IP', lambda host: host.ip ), terom@29: ( 'mac', 'MAC', lambda host: host.mac ), terom@29: ( 'name', 'Hostname', lambda host: host.name ), terom@29: ( 'gw', 'Network', lambda host: host.gw ), terom@29: ( 'seen', 'Seen', Host.seen, ), terom@29: ( 'state', 'State', lambda host: host.state ), terom@29: ) terom@29: terom@25: def process (self) : terom@30: """ terom@30: Either return JSON (if ?t=...), or fetch hosts/t for rendering. terom@30: """ terom@30: terom@25: t = self.request.args.get('t') terom@205: terom@205: if t : terom@205: # return json terom@205: t = ts2dt(int(t)) terom@205: terom@205: # query terom@205: hosts = self.query() terom@30: terom@30: # always sorted by last_seen terom@30: hosts = hosts.order_by(Host.last_seen.desc()) terom@30: terom@30: # filter terom@30: self.filters, hosts = self.filter(hosts) terom@29: terom@25: if t : terom@29: # return json terom@25: hosts = hosts.filter(Host.last_seen > t) terom@25: hosts = list(hosts) terom@25: hosts.reverse() terom@25: terom@25: if hosts : terom@205: # update timestamp to most recent terom@25: t = hosts[-1].last_seen terom@26: terom@205: # json terom@25: data = dict( terom@25: t = dt2ts(t), terom@205: hosts = [dict(self.table.json(host)) for host in hosts], terom@25: ) terom@25: terom@199: return response.json(data) terom@25: terom@25: else : terom@29: # render html terom@205: hosts = hosts.limit(self.table.PAGE) terom@25: terom@25: # XXX: testing terom@31: hosts = hosts.offset(1) terom@25: terom@205: # extract timestamp terom@205: for host in hosts : terom@205: self.t = host.last_seen terom@205: terom@205: break terom@205: terom@205: else : terom@205: # no hosts :< terom@205: self.t = datetime.datetime.now() terom@31: terom@205: # store terom@205: self.hosts = hosts terom@25: terom@30: def title (self) : terom@30: if self.filters : terom@30: return "{title}: {filters}".format(title=self.TITLE, filters=self.filters_title()) terom@30: else : terom@30: return self.TITLE terom@30: terom@25: def render (self) : terom@30: """ terom@30: Render page HTML and initial
, along with bootstrap JS (t0, configuration). terom@30: """ terom@205: terom@26: return html.div(id='wrapper')( terom@26: html.input(type='submit', id='refresh', value="Refresh"), terom@37: html.input(type='reset', id='pause', value="Pause"), terom@205: terom@205: self.table.render(self.hosts)(id='hosts-realtime'), terom@205: terom@205: html.script(type='text/javascript')( terom@205: """ terom@205: $(document).ready(HostsRealtime(Table($('#hosts-realtime'), {table_params}), {params})); terom@205: """.format( terom@205: table_params = json.dumps(dict( terom@205: item_url = self.url(ItemHandler, id='0'), terom@205: columns = [column.attr for column in self.table.columns], terom@205: )), terom@205: params = json.dumps(dict( terom@205: url = self.url(), terom@205: filters = self.filters, terom@205: t = dt2ts(self.t), terom@205: )), terom@26: ) terom@25: ) terom@25: ) terom@25: