terom@178: from pvl.verkko import web, db terom@14: from pvl.verkko.utils import parse_timedelta, IPv4Network terom@0: terom@151: from pvl.web import html terom@0: terom@8: import re terom@14: import datetime terom@0: import socket # dns terom@16: import math 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@14: def query (cls, session, seen=None) : terom@14: """ terom@14: seen - select hosts seen during given timedelta period terom@14: """ terom@14: terom@14: query = session.query(cls) terom@14: terom@14: return query terom@14: terom@14: @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@179: def column (attr, title, column, rowhtml=None, sort=True, filter=True, colcss=True, rowfilter=True, rowtitle=None, rowcss=None) : terom@179: """ terom@179: web.Table column spec. terom@179: """ terom@179: terom@179: return (attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss) terom@179: terom@178: class BaseHandler (web.DatabaseHandler) : terom@178: """ terom@178: Common controller stuff for DHCP hosts terom@178: """ terom@26: terom@178: CSS = ( terom@178: "/static/dhcp/hosts.css", terom@178: ) terom@178: JS = ( terom@178: #"/static/jquery/jquery.js" terom@178: ) terom@179: terom@179: TABLE = Host terom@179: TABLE_COLUMNS = ( terom@179: #column('id', "#", Host.id ), terom@179: column('ip', "IP", Host.ip, ), terom@179: column('mac', "MAC", Host.mac, Host.render_mac), terom@179: column('name', "Hostname", Host.name, Host.render_name, rowfilter=False), terom@179: column('gw', "Network", Host.gw, Host.network, rowfilter=False), terom@179: column('seen', "Seen", Host.last_seen, Host.seen, rowfilter=False), terom@179: column('state', "State", Host.count, rowtitle=Host.state_title, rowcss=Host.state_class, rowfilter=False), terom@179: ) terom@179: terom@179: # attr -> column terom@179: TABLE_ATTRS = dict((attr, column) for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in TABLE_COLUMNS) terom@179: terom@179: # default sort terom@179: TABLE_SORT = Host.last_seen.desc() terom@179: terom@179: # items per page terom@179: TABLE_PAGE = 10 terom@179: terom@179: # target for items terom@179: TABLE_URL = None terom@179: TABLE_ITEM_URL = None terom@0: terom@5: def query (self) : terom@179: """ terom@179: Database SELECT query. terom@179: """ terom@179: terom@179: return self.db.query(self.TABLE) terom@16: terom@179: def sort (self, query, default=TABLE_SORT) : terom@179: """ terom@179: Apply ?sort= from requset args to query. terom@179: terom@179: Return { attr: sort }, query terom@179: """ terom@179: terom@30: sort = self.request.args.get('sort') terom@0: terom@30: if sort : terom@36: name = sort.lstrip('+-') terom@36: else : terom@36: name = None terom@36: terom@36: if name : terom@179: order_by = self.TABLE_ATTRS[name] terom@3: else : terom@30: order_by = default terom@36: terom@179: # prefix -> ordering terom@36: if not sort : terom@36: pass terom@36: elif sort.startswith('+') : terom@36: order_by = order_by.asc() terom@36: elif sort.startswith('-') : terom@36: order_by = order_by.desc() terom@36: else : terom@36: pass terom@179: terom@179: # apply terom@30: log.debug("sort: %s", order_by) terom@16: terom@179: query = query.order_by(order_by) terom@3: terom@179: return sort, query terom@5: 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@30: def filter_attr (self, attr, value) : terom@30: """ terom@30: Return filter expression for given attr == value terom@30: """ terom@30: terom@179: # preprocess terom@179: like = False terom@30: terom@179: if value.endswith('*') : terom@179: like = value.replace('*', '%') terom@30: terom@179: # filter terom@179: column = self.TABLE_ATTRS[attr] terom@30: terom@179: if like : terom@179: return (column.like(like)) terom@30: else : terom@179: return (column == value) terom@179: terom@179: def _filter (self, attr, values) : terom@179: """ terom@179: Apply filters for given attr -> (value, expression) terom@179: """ terom@30: terom@179: for value in values : terom@179: value = value.strip() terom@179: terom@179: # ignore empty fields terom@179: if not value : terom@179: continue terom@30: terom@179: # lookup attr-specific filter terom@179: filter = getattr(self, 'filter_{attr}'.format(attr=attr), None) terom@179: terom@179: if filter : terom@179: filter = filter(value) terom@30: else : terom@179: # use generic terom@179: filter = self.filter_attr(attr, value) terom@179: terom@179: log.debug("%s: %s: %s", attr, value, filter) terom@179: terom@179: yield value, filter terom@30: terom@179: def filter (self, query) : terom@30: """ terom@30: Apply filters from request.args against given hosts. terom@30: terom@30: Returns (filters, hosts). terom@30: """ terom@30: terom@30: # filter? terom@30: filters = {} terom@30: terom@179: for attr in self.TABLE_ATTRS : terom@179: # from request args terom@179: values = self.request.args.getlist(attr) terom@179: terom@179: # lookup attr filters as expressions terom@179: value_filters = list(self._filter(attr, values)) terom@30: terom@41: # ignore empty fields terom@179: if not value_filters : terom@30: continue terom@179: terom@179: # filtering values, and filter expressions terom@179: values, expressions = zip(*value_filters) terom@30: terom@179: # apply terom@179: query = query.filter(db.or_(*expressions)) terom@30: filters[attr] = values terom@30: terom@179: return filters, query terom@30: terom@30: def filters_title (self) : terom@30: """ terom@30: Return a string representing the applied filters. terom@30: """ terom@30: terom@30: return ', '.join(value for values in self.filters.itervalues() for value in values) terom@30: terom@179: def render_table (self, query, caption=None, sort=None, filters=None, page=None, hilight=None) : terom@179: """ terom@179: Return element. Wrapped in if filters. terom@4: terom@179: query - filter()'d sort()'d SELECT query() terom@179: caption - optional
terom@179: sort - None for no sorting ui, sort-attr otherwise. terom@179: filters - None for no filtering ui, dict of filters otherwise. terom@179: page - display pagination for given page terom@179: hilight - { attr: value } cells to hilight terom@179: """ terom@179: terom@179: def url (filters=filters, sort=sort, **opts) : terom@179: """ terom@179: URL for table with given opts, keeping our sorting/filtering unless overriden. terom@179: """ terom@179: terom@11: args = dict() terom@11: terom@11: if filters : terom@11: args.update(filters) terom@179: terom@179: if sort : terom@179: args['sort'] = sort terom@179: terom@179: if opts : terom@179: args.update(opts) terom@9: terom@179: return self.url(self.TABLE_URL, **args) terom@179: terom@179: def sorturl (attr, sort=sort) : terom@179: """ terom@179: URL for table sorted by given column, reversing direction if already sorting by given column. terom@179: """ terom@179: terom@179: if not sort : terom@36: sort = attr terom@179: elif sort.lstrip('+-') != attr : terom@36: sort = attr terom@179: elif sort.startswith('-') : terom@36: sort = "+" + attr terom@36: else : terom@36: sort = "-" + attr terom@36: terom@179: return url(sort=sort) terom@36: terom@179: def itemurl (item) : terom@16: """ terom@179: URL for given item, by id. terom@179: """ terom@179: terom@179: if self.TABLE_ITEM_URL : terom@179: # separate page terom@179: return self.url(self.TABLE_ITEM_URL, id=item.id) terom@179: else : terom@179: # to our table terom@179: return url() + '#{id}'.format(id=item.id) terom@179: terom@179: def render_filter (attr) : terom@179: """ terom@179: Render filter-input for column. terom@179: """ terom@179: terom@179: value = filters.get(attr) terom@179: terom@179: if value : terom@179: # XXX: multi-valued filters? terom@179: value = value[0] terom@179: else : terom@179: value = None terom@179: terom@179: return html.input(type='text', name=attr, value=value) terom@179: terom@179: def render_head () : terom@179: """ terom@179: Yield header, filter rows for columns in table header. terom@179: """ terom@179: terom@179: # id terom@179: yield html.td('#'), html.td(html.input(type='submit', value=u'\u00BF')) terom@179: terom@179: for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS : terom@179: header = title terom@179: terom@179: if sort : terom@179: header = html.a(href=sorturl(attr))(header) terom@179: terom@179: header = html.th(header) terom@179: terom@179: if filters is not None and filter : terom@179: filter = render_filter(attr) terom@179: else : terom@179: filter = None terom@179: terom@179: if colcss is True : terom@179: colcss = attr terom@179: terom@179: filter = html.td(class_=colcss)(filter) terom@179: terom@179: yield header, filter terom@179: terom@179: def render_cell (attr, value, rowhtml=None, colcss=True, filter=None, rowtitle=None, rowcss=None, hilight=hilight) : terom@179: """ terom@179: Render a single cell. terom@179: terom@179: colcss - css class for column; True -> attr terom@179: filter - render filter link for value? terom@179: htmlvalue - rendered value? terom@179: title - mouseover title for cell terom@179: rowcss - css class for row terom@179: """ terom@179: terom@179: if not rowhtml : terom@179: rowhtml = value terom@179: terom@179: if filter : terom@179: cell = html.a(href=url(filters=None, **{attr: value}))(rowhtml) terom@179: else : terom@179: cell = rowhtml terom@179: terom@179: if colcss is True : terom@179: colcss = attr terom@179: terom@179: if hilight : terom@179: hilight = attr in hilight and value in hilight[attr] terom@179: terom@179: css = (colcss, rowcss, 'hilight' if hilight else None) terom@179: css = ' '.join(cls for cls in css if cls) terom@179: terom@179: return html.td(class_=css, title=rowtitle)(cell) terom@179: terom@179: def render_row (item) : terom@179: """ terom@179: Yield columns for row. terom@179: """ terom@179: terom@179: for attr, title, column, sort, filter, colcss, rowhtml, rowfilter, rowtitle, rowcss in self.TABLE_COLUMNS : terom@179: # XXX: this is sometimes broken, figure out how to index by column terom@179: value = getattr(item, attr) terom@179: terom@179: if rowhtml : terom@179: rowhtml = rowhtml(item) terom@179: else : terom@179: rowhtml = value terom@179: terom@179: if rowtitle : terom@179: rowtitle = rowtitle(item) terom@179: else : terom@179: rowtitle = None terom@179: terom@179: if rowcss : terom@179: rowcss = rowcss(item) terom@179: else : terom@179: rowcss = None terom@179: terom@179: yield render_cell(attr, value, terom@179: rowhtml = rowhtml, terom@179: colcss = colcss, terom@179: filter = value if rowfilter else None, terom@179: rowtitle = rowtitle, terom@179: rowcss = rowcss, terom@179: ) terom@179: terom@179: def render_body (rows) : terom@179: """ terom@179: Yield rows. terom@179: """ terom@179: terom@179: for i, item in enumerate(rows) : terom@179: yield html.tr(class_=('alternate' if i % 2 else None), id=item.id)( terom@179: html.th( terom@179: html.a(href=itemurl(item))("#") terom@179: ), terom@179: terom@179: render_row(item) terom@179: ) terom@179: terom@179: def render_pagination (page, count=None) : terom@179: """ terom@179: Render pagination links. terom@16: """ terom@16: terom@16: if count is not None : terom@179: pages = int(math.ceil(count / self.TABLE_PAGE)) terom@16: else : terom@16: pages = None terom@16: terom@16: if page > 0 : terom@16: yield html.a(href=url(page=0))(html("«« First")) terom@16: yield html.a(href=url(page=(page - 1)))(html("« Prev")) terom@16: terom@16: yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=(pages or '???'))) terom@16: terom@16: yield html.a(href=url(page=(page + 1)))(html("» Next")) terom@16: terom@21: terom@179: def render_foot () : terom@179: # XXX: does separate SELECT count() terom@179: count = query.count() terom@21: terom@179: if page : terom@179: return render_pagination(page, count) terom@179: else : terom@179: return "{count} hosts".format(count=count) terom@179: terom@179: # columns for the two header rows terom@179: headers, filtering = zip(*list(render_head())) terom@179: terom@179: # render table terom@9: table = html.table( terom@179: html.caption(caption) if caption else None, terom@5: html.thead( terom@179: html.tr(headers), terom@179: # filters? terom@179: html.tr(class_='filter')(filtering) if filters is not None else None, terom@5: ), terom@5: html.tbody( terom@179: render_body(query) terom@12: ), terom@12: html.tfoot( terom@12: html.tr( terom@179: html.td(colspan=(1 + len(self.TABLE_COLUMNS)))( terom@179: render_foot() terom@12: ) terom@12: ) terom@5: ) terom@5: ) terom@9: terom@179: # filters form? terom@179: if filters is None : terom@9: return table terom@9: else : terom@179: return html.form(method='get', action=url(filters=None, sort=None))( terom@179: html.input(type='hidden', name='sort', value=sort), terom@9: table, terom@9: ) terom@4: terom@5: class ItemHandler (BaseHandler) : 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@5: terom@5: class ListHandler (BaseHandler) : terom@178: """ terom@178: List of DHCP hosts for given filter. terom@178: """ terom@178: terom@179: TABLE_PAGE = 10 terom@16: terom@8: def process (self) : terom@14: hosts = self.query() terom@9: terom@30: # filter terom@30: self.filters, hosts = self.filter(hosts) terom@16: terom@19: # sort XXX: default per filter column? terom@30: self.sorts, hosts = self.sort(hosts) terom@16: terom@16: # page? terom@16: self.page = self.request.args.get('page') terom@16: terom@16: if self.page : terom@16: self.page = int(self.page) terom@16: terom@16: hosts = hosts.offset(self.page * self.PAGE).limit(self.PAGE) terom@16: terom@14: self.hosts = hosts terom@30: 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@5: terom@5: def render (self) : terom@8: return ( terom@179: self.render_table(self.hosts, filters=self.filters, sort=self.sorts, page=self.page), terom@8: terom@8: html.a(href=self.url())(html('«'), 'Back') if self.filters else None, terom@8: ) terom@25: terom@179: # XXX: terom@179: BaseHandler.TABLE_URL = ListHandler terom@179: BaseHandler.TABLE_ITEM_URL = ItemHandler terom@179: terom@30: class RealtimeHandler (BaseHandler) : terom@178: TITLE = "DHCP Pseudo-Realtime hosts.." terom@178: CSS = BaseHandler.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@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@30: hosts = self.db.query(Host) terom@25: t = self.request.args.get('t') 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@29: def host_params (host) : terom@29: yield 'id', host.id terom@29: terom@29: for name, title, fvalue in self.COLUMNS : terom@29: value = fvalue(host) terom@29: terom@29: if name == 'seen' : terom@29: # XXX: hackfix html() rendering terom@29: value = unicode(html.div(value)) terom@29: terom@29: yield name, value terom@29: terom@29: # special terom@29: yield 'state_class', host.state_class() terom@25: terom@25: if t : terom@29: # return json terom@25: t = ts2dt(int(t)) terom@25: 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@25: t = hosts[-1].last_seen terom@29: hosts = [dict(host_params(host)) for host in hosts] terom@26: terom@25: else : terom@25: hosts = [] terom@25: terom@25: data = dict( terom@25: t = dt2ts(t), terom@25: hosts = hosts, terom@25: ) terom@25: terom@25: return web.Response(json.dumps(data), mimetype='text/json') terom@25: terom@25: else : terom@29: # render html terom@31: hosts = hosts.limit(10) terom@25: terom@25: # XXX: testing terom@31: hosts = hosts.offset(1) terom@25: terom@31: self.hosts = list(hosts) terom@31: terom@31: if self.hosts : terom@31: self.t = self.hosts[0].last_seen terom@31: else : terom@31: self.t = datetime.datetime.now() 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@30: terom@29: def column (name, title, fvalue, host) : terom@29: cls = name terom@29: terom@29: if name == 'state' : terom@29: cls = host.state_class() terom@29: terom@29: return html.td(class_=cls)(fvalue(host)) terom@29: terom@25: params = dict( terom@25: url = self.url(), terom@30: filters = self.filters, terom@25: t = dt2ts(self.t), terom@26: host = self.url(ItemHandler, id='0'), terom@29: columns = [name for name, title, fvalue in self.COLUMNS] terom@25: ) terom@25: params = json.dumps(params) terom@26: 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@26: html.table(id='hosts')( terom@26: html.thead( terom@26: html.tr( terom@29: html.th('#'), terom@29: ( terom@29: html.th(class_=name)(title) for name, title, fvalue in self.COLUMNS terom@29: ) terom@26: ), terom@26: ), terom@26: html.tbody( terom@26: html.tr(id=host.id)( terom@26: html.td(html.a(href=self.url(ItemHandler, id=host.id))('#')), terom@26: ( terom@29: column(name, title, fvalue, host) for name, title, fvalue in self.COLUMNS terom@26: ), terom@26: ) for host in self.hosts terom@26: ) terom@25: ), terom@25: html.script(type='text/javascript')(""" terom@25: $(document).ready(hosts_realtime({params})); terom@25: """.format(params=params) terom@25: ) terom@25: ) terom@25: