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: