--- a/MANIFEST.in Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-include etc/*.conf.dist
-include static/dhcp/*.css static/dhcp/*.js
-include static/rrd/*.css static/rrd/*.js
--- a/README Tue Feb 24 12:47:09 2015 +0200
+++ b/README Tue Feb 24 14:50:31 2015 +0200
@@ -1,3 +1,3 @@
-== Requirements ==
-* python-psycopg2
+= pvl-hosts =
+DNS/DHCP hosts management for ISC bind9 and dhcpd
--- a/bin/pvl.login-server Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,51 +0,0 @@
-#!/usr/bin/python
-
-"""
- pvl.verkko.rrd wsgi development server
-"""
-
-
-import pvl.args
-import pvl.ldap.args
-import pvl.login.auth
-import pvl.login.server
-import pvl.login.ssl
-import pvl.web.args
-
-
-import optparse
-import logging; log = logging.getLogger('pvl.login-server')
-
-
-def main (argv) :
- """
- pvl.login server
- """
-
- parser = optparse.OptionParser(main.__doc__)
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.web.args.parser(parser))
- parser.add_option_group(pvl.ldap.args.parser(parser))
-
- options, args = parser.parse_args(argv[1:])
- pvl.args.apply(options)
-
- # ldap
- ldap = pvl.ldap.args.apply(options)
-
- # app
- application = pvl.web.args.apply(options,
- pvl.login.server.LoginApplication,
- auth = pvl.login.auth.LDAPAuth(ldap),
- ssl = pvl.login.ssl.UsersCA('ssl/userca', 'ssl/users'),
- )
-
- # behind a reverse-proxy
- import werkzeug.contrib.fixers
-
- application = werkzeug.contrib.fixers.ProxyFix(application)
-
- pvl.web.args.main(options, application)
-
-if __name__ == '__main__':
- pvl.args.main(main)
--- a/bin/pvl.rrd-graph Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-#!/usr/bin/env python
-
-"""
- pvl.rrd graph output
-"""
-
-__version__ = '0.1'
-
-import pvl.args
-import pvl.rrd.args
-import pvl.rrd.graph
-
-import os.path
-
-import logging, optparse
-
-log = logging.getLogger('main')
-
-def parse_options (argv) :
- """
- Parse command-line arguments.
- """
-
- prog = argv[0]
-
- parser = optparse.OptionParser(
- prog = prog,
- usage = '%prog: [options]',
- version = __version__,
-
- # module docstring
- description = __doc__,
- )
-
- # options
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.rrd.args.parser(parser))
-
- parser.add_option('--style', metavar='STYLE', default='detail',
- help="overview/detail")
-
- parser.add_option('--interval', metavar='INTERVAL', default='daily',
- help="daily/weekly/yearly")
-
- # parse
- options, args = parser.parse_args(argv[1:])
-
- # apply
- pvl.args.apply(options, prog)
-
- return options, args
-
-def graph (options, rrds, rrd) :
- """
- Graph given rrd.
- """
-
-
- # out
- path, ext = os.path.splitext(rrd)
- ext = options.graph
- out = path + ext
-
- # graph
- log.info("%s -> %s", rrd, out)
-
- pvl.rrd.graph.collectd_ifoctets(options.style, options.interval, "Test", rrd, out)
-
-def main (argv) :
- """
- Usage: [options] rrd
- """
-
- options, args = parse_options(argv)
-
- # RRDDatabase
- rrds = pvl.rrd.args.apply(options)
-
- for rrd in args :
- graph(options, rrds, rrd)
-
- # done
- log.info("Exiting...")
- return 0
-
-if __name__ == '__main__':
- import sys
-
- sys.exit(main(sys.argv))
--- a/bin/pvl.rrd-interfaces Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,182 +0,0 @@
-#!/usr/bin/python
-
-"""
- Setup symlinks for pvl.verkko-rrd -> collectd based on define host/interface names
-"""
-
-__version__ = '0.1'
-
-import os
-
-import pvl.args
-import pvl.rrd.hosts
-from pvl.rrd.hosts import hostreverse
-
-import optparse
-import logging; log = logging.getLogger('main')
-
-COLLECTD_RRD = '/var/lib/collectd/rrd'
-COLLECTD_PLUGIN = 'interfaces'
-COLLECTD_TYPE = 'if_octets'
-
-def parse_argv (argv, doc = __doc__) :
- """
- Parse command-line argv, returning (options, args).
- """
-
- prog = argv.pop(0)
- args = argv
-
- # optparse
- parser = optparse.OptionParser(
- prog = prog,
- usage = '%prog: [options] [<input.txt> [...]]',
- version = __version__,
- description = doc,
- )
-
- # common
- parser.add_option_group(pvl.args.parser(parser))
-
- # options
- parser.add_option('--collectd-rrd', metavar='PATH', default=COLLECTD_RRD,
- help="Path to collectd rrd: %default")
- parser.add_option('--collectd-plugin', metavar='PLUGIN', default=None,
- help="Collectd plugin to use: <input>-<plugin>.txt")
- parser.add_option('--collectd-type', metavar='TYPE', default=COLLECTD_TYPE,
- help="Collectd type to use: %default")
-
- # interface is by plugin, snmp is by type...
- parser.add_option('--collectd-instance-plugin', action='store_const', dest='collectd_instance', const='plugin',
- help="Collectd by plugin instance")
- parser.add_option('--collectd-instance-type', action='store_const', dest='collectd_instance', const='type',
- help="Collectd by type instance")
-
- # hostnames
- parser.add_option('--reverse-host', action='store_true',
- help="Flip host.domain -> domain.host (default)")
- parser.add_option('--no-reverse-host', action='store_false', dest='reverse_host',
- help="Keep host.domain as host.domain")
- parser.add_option('--domain', metavar='DOMAIN',
- help="Append domain to collectd hostnames: <input>.txt -> <input>")
-
- # output
- parser.add_option('--rrd', metavar='PATH',
- help="Output directory for .rrd symlinks: <input>.txt -> <input>/")
- parser.add_option('--noop', action='store_true',
- help="Scan symlinks, but do not update")
-
- parser.set_defaults(
- collectd_instance = 'type',
- reverse_host = True,
- )
-
- # parse
- options, args = parser.parse_args(args)
-
- # apply
- pvl.args.apply(options)
-
- return options, args
-
-def sync_links (options, links, rrddir) :
- """
- Sync given (collectd, name) symlinks in given dir.
- """
-
- log.info("%s", rrddir)
-
- for rrdpath, (domain, host, port) in links :
- linkpath = os.path.join(rrddir,
- hostreverse(domain) if options.reverse_host else domain,
- hostreverse(host) if options.reverse_host else host,
- port + '.rrd'
- )
-
- # sync
- if os.path.exists(linkpath) and os.readlink(linkpath) == rrdpath :
- continue
-
- log.info("%s: %s", linkpath, rrdpath)
-
- yield linkpath, rrdpath
-
-def apply_links (options, links) :
- """
- Apply given symlinks
- """
-
- for link, path in links :
- linkdir = os.path.dirname(link)
-
- # do
- if not os.path.exists(linkdir) :
- log.warn("makedirs: %s", linkdir)
- os.makedirs(linkdir)
-
- os.symlink(path, link)
-
-def main (argv) :
- options, args = parse_argv(argv)
-
- for path in args :
- # <path>/<domain>-<plugin>.txt -> <path>/<domain>-<plugin>
- if '.txt' in path:
- basepath, _ = os.path.splitext(path)
- else:
- basepath = path
-
- # <path>/<domain> -> <domain>
- _, basename = os.path.split(basepath)
-
- # <path>/<domain>-<plugin> -> <path>/<domain>, <plugin>
- if '-' in basename :
- basename, collectd_plugin = basename.rsplit('-', 1)
- else :
- collectd_plugin = None
-
- # domain?
- if options.domain is None :
- # reverse-order hostname
- domain = hostreverse(basename)
- else :
- # may be ''
- domain = options.domain
-
- # output dir?
- if not options.rrd:
- log.error("no --rrd output dir given")
- return 1
-
- rrddir = options.rrd
-
- # generate links from spec
- links = list(pvl.rrd.hosts.collectd_interfaces(options, open(path),
- collectd_domain = domain,
- collectd_plugin = options.collectd_plugin or collectd_plugin or COLLECTD_PLUGIN,
- ))
-
- if not os.path.exists(rrddir) :
- log.error("given --rrd must already exist: %s", rrddir)
- return 1
-
- # sync missing links
- links = list(sync_links(options, links,
- rrddir = rrddir,
- ))
-
- # verbose
- if not options.quiet :
- for link, path in links :
- print link, '->', path
-
- # apply
- if not options.noop :
- apply_links(options, links)
-
- return 0
-
-if __name__ == '__main__':
- import sys
-
- sys.exit(main(sys.argv))
--- a/bin/pvl.switches-traps Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-import pvl.args
-import pvl.hosts
-import pvl.snmp.traps
-import pvl.syslog.args
-
-import collections
-import logging; log = logging.getLogger('pvl.switches-traps')
-import optparse
-
-
-def main (argv) :
- """
- Process SNMP traps from snmptrapd.
- """
-
- parser = optparse.OptionParser(main.__doc__)
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.hosts.optparser(parser))
- parser.add_option_group(pvl.syslog.args.parser(parser))
-
- # input
- options, args = parser.parse_args(argv[1:])
- pvl.args.apply(options)
-
- # syslog source
- syslog = pvl.syslog.args.apply(options)
-
- # XXX: associate with host data
- #hosts = pvl.hosts.apply(options, args)
-
- # main
- for item in syslog:
- host, values = pvl.snmp.traps.parse_snmptrapd_log(item['msg'])
-
- print '{}'.format(host)
-
- for field, value in values.iteritems():
- print '\t{:55} {}'.format(field, value)
-
- print
-
- return 0
-
-if __name__ == '__main__':
- pvl.args.main(main)
-
--- a/bin/pvl.verkko-dhcp Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-#!/usr/bin/python
-
-from werkzeug.serving import run_simple
-
-import pvl.args
-import pvl.web.args
-import pvl.verkko
-import pvl.verkko.dhcp
-
-from pvl.verkko import __version__
-import optparse
-import logging; log = logging.getLogger('main')
-
-def parse_argv (argv, doc = __doc__) :
- """
- Parse command-line argv, returning (options, args).
- """
-
- prog = argv.pop(0)
- args = argv
-
- # optparse
- parser = optparse.OptionParser(
- prog = prog,
- usage = '%prog: [options] [<user> [...]]',
- version = __version__,
- description = doc,
- )
-
- # common
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.web.args.parser(parser))
-
- parser.add_option('-d', '--database-read', metavar='URI', default='sqlite:///var/verkko.db',
- help="Database to use (readonly)")
-
- # parse
- options, args = parser.parse_args(args)
-
- # apply
- pvl.args.apply(options)
-
- return options, args
-
-def main (argv) :
- """
- pvl.verkko wsgi development server.
- """
-
- # parse cmdline
- options, args = parse_argv(argv, doc=__doc__)
-
- # open
- database = pvl.verkko.Database(options.database_read)
-
- # app
- application = pvl.web.args.apply(options, pvl.verkko.dhcp.Application, database)
- pvl.web.args.main(options, application)
-
-if __name__ == '__main__' :
- import sys
-
- sys.exit(main(sys.argv))
-
-
--- a/bin/pvl.verkko-rrd Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-#!/usr/bin/python
-
-"""
- pvl.verkko.rrd wsgi development server
-"""
-
-import werkzeug.serving
-
-import pvl.args
-import pvl.rrd.args
-import pvl.web.args
-import pvl.verkko.rrd
-
-from pvl.verkko import __version__
-import optparse
-import logging; log = logging.getLogger('main')
-
-def parse_argv (argv, doc = __doc__) :
- """
- Parse command-line argv, returning (options, args).
- """
-
- prog = argv.pop(0)
- args = argv
-
- # optparse
- parser = optparse.OptionParser(
- prog = prog,
- usage = '%prog: [options] [<user> [...]]',
- version = __version__,
- description = doc,
- )
-
- # common
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.rrd.args.parser(parser))
- parser.add_option_group(pvl.web.args.parser(parser))
-
- # parse
- options, args = parser.parse_args(args)
-
- # apply
- pvl.args.apply(options)
-
- return options, args
-
-def main (argv) :
- """
- pvl.verkko wsgi development server.
- """
-
- # parse cmdline
- options, args = parse_argv(argv, doc=__doc__)
-
- # rrd
- rrd = pvl.rrd.args.apply(options)
-
- # app
- application = pvl.web.args.apply(options, pvl.verkko.rrd.Application, rrd)
- pvl.web.args.main(options, application)
-
-if __name__ == '__main__' :
- import sys
-
- sys.exit(main(sys.argv))
-
-
--- a/bin/pvl.wlan-syslog Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,227 +0,0 @@
-#!/usr/bin/python
-
-"""
- Analyze WLAN STA logs.
-
- Jul 3 23:05:04 buffalo-g300n-647682 daemon.info hostapd: wlan0-1: STA aa:bb:cc:dd:ee:ff WPA: group key handshake completed (RSN)
-
-"""
-
-__version__ = '0.1'
-
-import pvl.args
-import pvl.syslog.args
-import pvl.web.args
-import pvl.rrd.hosts
-import pvl.verkko.wlan
-
-import optparse
-import logging; log = logging.getLogger('main')
-
-WLAN_STA_PROG = 'hostapd'
-
-def parse_argv (argv, doc = __doc__) :
- """
- Parse command-line argv, returning (options, args).
- """
-
- prog = argv.pop(0)
- args = argv
-
- # optparse
- parser = optparse.OptionParser(
- prog = prog,
- usage = '%prog: [options] [<input.txt> [...]]',
- version = __version__,
- description = doc,
- )
-
- # common
- parser.add_option_group(pvl.args.parser(parser))
- parser.add_option_group(pvl.syslog.args.parser(parser, prog=WLAN_STA_PROG))
- parser.add_option_group(pvl.verkko.db.parser(parser, table=db.wlan_sta))
- parser.add_option_group(pvl.web.args.parser(parser))
-
- parser.add_option('--interfaces', metavar='PATH',
- help="Load interface/node names from mapping file")
-
- # parse
- options, args = parser.parse_args(args)
-
- # apply
- pvl.args.apply(options)
-
- return options, args
-
-import re
-from pvl.verkko import db
-
-class KeyTimestampDatabase (object) :
- """
- A pvl.verkko.db table that tracks events by key/timestamp.
- """
-
- DB_TABLE = None
- DB_LAST_SEEN = None
- DB_COUNT = None
- DB_DISTINCT = None
-
- def __init__ (self, db) :
- """
- db - pvl.verkko.db.Database
- """
-
- self.db = db
-
- def select (self, interval=None) :
- """
- SELECT unique gw/ip hosts, for given interval.
- """
-
- query = db.select(self.DB_DISTINCT, distinct=True)
-
- if interval :
- # timedelta
- query = query.where(db.func.now() - self.DB_LAST_SEEN < interval)
-
- return self.db.select(query)
-
- def insert (self, key, timestamp, update) :
- """
- INSERT new row.
- """
-
- query = self.DB_TABLE.insert().values(**key).values(**update).values(
- first_seen = timestamp,
- last_seen = timestamp,
- )
-
- if self.DB_COUNT is not None :
- query = query.values(count=1)
-
- # -> id
- return self.db.insert(query)
-
- def update (self, key, timestamp, update) :
- """
- UPDATE existing row, or return False if not found.
- """
-
- table = self.DB_TABLE
- query = table.update()
-
- for col, value in key.iteritems() :
- query = query.where(table.c[col] == value)
-
- query = query.values(last_seen=timestamp)
-
- if self.DB_COUNT is not None :
- query = query.values(count=db.func.coalesce(self.DB_COUNT, 0) + 1)
-
- query = query.values(**update)
-
- # -> any matched rows?
- return self.db.update(query)
-
- def touch (self, key, timestamp, update, **opts) :
- # update existing?
- if self.update(key, timestamp, update, **opts) :
- log.info("Update: %s: %s: %s", key, timestamp, update)
- else :
- log.info("Insert: %s: %s: %s", key, timestamp, update)
- self.insert(key, timestamp, update, **opts)
-
-
-class WlanStaDatabase (KeyTimestampDatabase) :
- HOSTAPD_STA_RE = re.compile(r'(?P<wlan>.+?): STA (?P<sta>.+?) (?P<msg>.+)')
-
- DB_TABLE = db.wlan_sta
- DB_LAST_SEEN = db.wlan_sta.c.last_seen
- DB_COUNT = db.wlan_sta.c.count
-
- DB_DISTINCT = (db.wlan_sta.c.sta, )
-
- def __init__ (self, db, interface_map=None) :
- """
- interface_map - {(hostname, interface): (nodename, wlan)}
- """
- KeyTimestampDatabase.__init__(self, db)
- self.interface_map = interface_map
-
- def parse (self, item) :
- """
- Parse fields from a hostapd syslog message.
- """
-
- match = self.HOSTAPD_STA_RE.match(item['msg'])
-
- if not match :
- return None
-
- return match.groupdict()
-
- def lookup_wlan (self, host, iface) :
- """
- Lookup ap/ssid by host/iface.
- """
- mapping = None
- if self.interface_map :
- mapping = self.interface_map.get((host, iface))
-
- if mapping :
- return mapping
- else :
- # as-is
- log.warning("Unknown host/iface: %s/%s", host, iface)
- return host, iface
-
- def __call__ (self, item) :
- match = self.parse(item)
-
- if not match :
- return
-
- # lookup?
- ap, ssid = self.lookup_wlan(item['host'], match['wlan'])
-
- # update/insert
- self.touch(
- dict(ap=ap, wlan=ssid, sta=match['sta']),
- item['timestamp'],
- dict(msg=match['msg']),
- )
-
-def main (argv) :
- options, args = parse_argv(argv)
-
- # database
- db = pvl.verkko.db.apply(options)
-
- if options.interfaces :
- interfaces = dict(pvl.rrd.hosts.map_interfaces(options, open(options.interfaces)))
- else :
- interfaces = None
-
- # syslog
- log.info("Open up syslog...")
- syslog = pvl.syslog.args.apply(options, optional=True)
-
- if syslog :
- # handler
- handler = WlanStaDatabase(db, interface_map=interfaces)
-
- log.info("Enter mainloop...")
- for source in syslog.main() :
- for item in source:
- handler(item)
- else :
- # run web
- application = pvl.web.args.apply(options, pvl.verkko.wlan.Application, db)
- return pvl.web.args.main(options, application)
-
- return 0
-
-if __name__ == '__main__':
- import sys
-
- sys.exit(main(sys.argv))
--- a/etc/snmptrapd.conf Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-authCommunity log we1ooL2ru5ohnael8
-
-outputOption X
-format1 %B %N %w %q %W\t%v\n
-format2 %B\t%v\n
--- a/pvl/hosts.py Tue Feb 24 12:47:09 2015 +0200
+++ b/pvl/hosts.py Tue Feb 24 14:50:31 2015 +0200
@@ -13,6 +13,8 @@
import logging; log = logging.getLogger('pvl.hosts')
+__version__ = '0.7.3'
+
def optparser (parser) :
hosts = optparse.OptionGroup(parser, "Hosts input")
hosts.add_option('--hosts-charset', metavar='CHARSET', default='utf-8',
--- a/pvl/login/auth.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,103 +0,0 @@
-import ldap
-
-import pvl.ldap.domain
-import pvl.users.group
-
-import logging; log = logging.getLogger('pvl.login.auth')
-
-class AuthError (Exception) :
- def __init__ (self, error) :
- self.error = error
-
- def __unicode__ (self) :
- return u"Authenticating against the backend failed: {self.error}".format(self=self)
-
-class LDAPAuth (object) :
- def __init__ (self, ldap) :
- self.ldap = ldap
-
- def auth (self, username, password) :
- """
- Attempt to bind against LDAP with given user object and password.
-
- Returns None if the user does not seem to exist, False on invalid auth, True on valid auth.
-
- Raises AuthError.
- """
-
- # search
- try :
- user = self.ldap.users.get(username)
- except KeyError :
- log.info("%s: not found", username)
- return None
- else :
- log.info("%s: %s", username, user)
-
- # bind
- bind = self.bind(user, password)
-
- if bind :
- return user
- else :
- return False
-
- def bind (self, user, password) :
- """
- Attempt to bind against LDAP with given user object and password.
-
- Returns the bound connection, or
- None - if the user does not seem toe xist
- False - invalid auth
-
- Raises AuthError.
- """
-
- conn = self.ldap.open()
-
- try :
- conn.bind(user.dn, password)
-
- except ldap.INVALID_CREDENTIALS as ex :
- log.info("%s: INVALID_CREDENTIALS", user)
- return False
-
- except ldap.NO_SUCH_OBJECT as ex :
- log.info("%s: ldap.NO_SUCH_OBJECT", user)
- return None
-
- except ldap.LDAPError as ex :
- log.exception("%s", user)
- raise AuthError(ex)
-
- else :
- log.info("%s", user)
- return conn
-
- def access (self, user) :
- """
- Yield a list of access control tokens for the given auth username.
- """
-
- yield pvl.users.group.Group.fromldap(self.ldap.users.group(user))
-
- for group in self.ldap.users.groups(user) :
- yield pvl.users.group.Group.fromldap(group)
-
- def userdata (self, user) :
- """
- Yield arbitrary userdata for given auth state.
- """
-
- return user.get('cn')
-
- def renew (self, username) :
- """
- Re-lookup auth state for given username.
- """
-
- try :
- return self.ldap.users.get(username)
- except KeyError :
- return None
-
--- a/pvl/login/pubtkt.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,291 +0,0 @@
-import base64
-import calendar
-import datetime
-import ipaddr
-import hashlib
-import M2Crypto
-
-import logging; log = logging.getLogger('pvl.login.pubtkt')
-
-def datetime2unix (dt) :
- """
- datetime.datetime -> float
- """
-
- return calendar.timegm(dt.utctimetuple())
-
-def unix2datetime (unix) :
- return datetime.datetime.utcfromtimestamp(unix)
-
-class Error (Exception) :
- """
- Error
- """
-
- def __init__ (self, error) :
- self.error = error
-
- def __unicode__ (self) :
- return u"{doc}: {self.error}".format(self=self, doc=self.__doc__.strip())
-
-class ParseError (Error) :
- """
- Unable to parse PubTkt from cookie
- """
-
-class VerifyError (Error) :
- """
- Invalid login token sigunature
- """
-
- def __init__ (self, pubtkt, error) :
- self.pubtkt = pubtkt
- self.error = error
-
-class ExpiredError (Error) :
- """
- Login token has expired
- """
-
- def __init__ (self, pubtkt, expire) :
- self.pubtkt = pubtkt
- self.error = expire
-
-class RenewError (Error) :
- """
- Unable to renew login token
- """
-
-class ServerError (Error) :
- """
- Login request from invalid server
- """
-
-class ServerKeys (object) :
- @classmethod
- def config (cls, public_key, private_key) :
- return cls(
- public = M2Crypto.RSA.load_pub_key(public_key),
- private = M2Crypto.RSA.load_key(private_key),
- )
-
- def __init__ (self, public, private) :
- self.public = public
- self.private = private
-
-class PubTkt (object) :
- @staticmethod
- def now () :
- return datetime.datetime.utcnow()
-
- @classmethod
- def load (cls, cookie, public_key) :
- """
- Load and verify a pubtkt from a cookie.
-
- Raise ParseError, VerifyError.
- """
-
- pubtkt, hash, sig = cls.parse(cookie)
-
- log.debug("parsed %s hash=%s sig=%s", pubtkt, hash.encode('hex'), sig.encode('hex'))
-
- try :
- if not public_key.verify(hash, sig, 'sha1') :
- raise VerifyError(pubtkt, "Unable to verify signature")
- except M2Crypto.RSA.RSAError as ex :
- raise VerifyError(pubtkt, str(ex))
-
-
- log.debug("checking expiry %s", pubtkt.validuntil)
-
- if not pubtkt.valid() :
- raise ExpiredError(pubtkt, pubtkt.validuntil)
-
- return pubtkt
-
- @classmethod
- def parse (cls, cookie) :
- """
- Load a pubtkt from a cookie
-
- Raises ParseError.
- """
-
- if ';sig=' in cookie :
- data, sig = cookie.rsplit(';sig=', 1)
- else :
- raise ParseError("Missing signature")
-
- try :
- sig = base64.b64decode(sig)
- except (ValueError, TypeError) as ex :
- raise ParseError("Invalid signature")
-
- hash = hashlib.sha1(data).digest()
-
- try :
- attrs = dict(field.split('=', 1) for field in data.split(';'))
- except ValueError as ex :
- raise ParseError(str(ex))
-
- if 'uid' not in attrs or 'validuntil' not in attrs :
- raise ParseError("Missing parameters in cookie (uid, validuntil)")
-
- try :
- return cls.build(**attrs), hash, sig
- except TypeError as ex :
- raise ParseError("Invalid or missing parameters in cookie")
- except ValueError as ex :
- raise ParseError(str(ex))
-
- @classmethod
- def build (cls, uid, validuntil, cip=None, tokens=None, udata=None, graceperiod=None, bauth=None) :
- """
- Build a pubtkt from items.
-
- Raises TypeError or ValueError..
- """
-
- return cls(uid,
- validuntil = unix2datetime(int(validuntil)),
- cip = ipaddr.IPAddress(cip) if cip else None,
- tokens = tokens.split(',') if tokens else (),
- udata = udata,
- graceperiod = unix2datetime(int(graceperiod)) if graceperiod else None,
- bauth = bauth,
- )
-
- @classmethod
- def new (cls, uid, valid, grace=None, **opts) :
- now = cls.now()
-
- return cls(uid, now + valid,
- graceperiod = now + grace if grace else None,
- **opts
- )
-
- def update (self, valid, grace, cip=None, tokens=None, udata=None, bauth=None) :
- now = self.now()
-
- return type(self)(self.uid, now + valid,
- graceperiod = now + grace if grace else None,
- cip = self.cip if cip is None else cip,
- tokens = self.tokens if tokens is None else tokens,
- udata = self.udata if udata is None else udata,
- bauth = self.bauth if bauth is None else bauth,
- )
-
- def __init__ (self, uid, validuntil, cip=None, tokens=(), udata=None, graceperiod=None, bauth=None) :
- self.uid = uid
- self.validuntil = validuntil
- self.cip = cip
- self.tokens = tokens
- self.udata = udata
- self.graceperiod = graceperiod
- self.bauth = bauth
-
- def iteritems (self) :
- yield 'uid', self.uid
- yield 'validuntil', int(datetime2unix(self.validuntil))
-
- if self.cip :
- yield 'cip', self.cip
-
- if self.tokens :
- yield 'tokens', ','.join(str(token) for token in self.tokens)
-
- if self.udata :
- yield 'udata', self.udata
-
- if self.graceperiod :
- yield 'graceperiod', int(datetime2unix(self.graceperiod))
-
- if self.bauth :
- yield 'bauth', self.bauth
-
- def __str__ (self) :
- """
- The (unsigned) pubtkt
- """
-
- return ';'.join('%s=%s' % (key, value) for key, value in self.iteritems())
-
- def sign (self, private_key) :
- data = str(self)
- hash = hashlib.sha1(data).digest()
- sign = private_key.sign(hash, 'sha1')
-
- return '%s;sig=%s' % (self, base64.b64encode(sign))
-
- def valid (self) :
- """
- Return remaining ticket validity.
- """
-
- now = self.now()
-
- if self.validuntil > now :
- return self.validuntil - now
- else :
- return False
-
- def grace (self) :
- """
- Return remaining grace period.
- """
-
- now = self.now()
-
- if not self.graceperiod :
- return None
-
- elif now < self.graceperiod :
- # still valid
- return None
-
- elif now < self.validuntil :
- # positive
- return self.validuntil - now
-
- else :
- # expired
- return False
-
- def remaining (self) :
- """
- Return remaining validity before grace.
- """
-
- now = self.now()
-
- if not self.graceperiod :
- return self.valid()
-
- elif now < self.graceperiod :
- return self.graceperiod - now
-
- else :
- # expired
- return False
-
- def grace_period (self) :
- """
- Return the length of the grace period.
- """
-
- if self.graceperiod :
- return self.validuntil - self.graceperiod
- else :
- return None
-
- def renew (self, valid, grace=None) :
- if not self.valid() :
- raise ExpiredError(self, "Unable to renew expired pubtkt")
-
- now = self.now()
-
- self.validuntil = now + valid
- self.graceperiod = now + grace if grace else None
-
-
--- a/pvl/login/server.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,701 +0,0 @@
-# encoding: utf-8
-
-import datetime
-import urlparse
-import werkzeug
-import werkzeug.urls
-
-import pvl.login.auth
-import pvl.web
-import pvl.web.response
-
-from pvl.login import pubtkt
-from pvl.web import urls
-from pvl.web import html5 as html
-
-import logging; log = logging.getLogger('pvl.login.server')
-
-class Handler (pvl.web.Handler) :
- # Bootstrap
- DOCTYPE = 'html'
- HTML_XMLNS = None
- HTML_LANG = 'en'
- CSS = (
- '//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css',
- )
- JS = (
- '//code.jquery.com/jquery.js',
- '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js',
- )
-
- STYLE = """
-body {
- padding-top: 2em;
- text-align: center;
-}
-
-.container {
- padding: 2em 1em;
- text-align: left;
-}
- """
-
- def redirect (self, *url, **params) :
- return pvl.web.response.redirect(self.url(*url, **params))
-
- pubtkt = None
- invalid_pubtkt = None
- valid_pubtkt = None
-
- def init (self) :
- self.alerts = []
-
- def alert (self, type, alert, icon=None) :
- log.info(u"%s: %s", type, alert)
-
- self.alerts.append((type, icon, unicode(alert)))
-
- def process_cookie (self) :
- """
- Reverse the urlencoding used for the cookie...
- """
-
- log.debug("cookies: %s", self.request.cookies)
-
- cookie = self.request.cookies.get(self.app.cookie_name)
-
- if not cookie :
- return
-
- log.debug("cookie %s=%s", self.app.cookie_name, cookie)
-
- cookie = werkzeug.urls.url_unquote(cookie)
-
- log.debug("cookie decoded: %s", cookie)
-
- if not cookie :
- return
-
- try :
- self.pubtkt = self.app.load(cookie)
-
- except pubtkt.ParseError as ex :
- self.alert('danger', ex, icon='compare')
-
- except pubtkt.VerifyError as ex :
- self.alert('danger', ex, icon='warning-sign')
-
- self.invalid_pubtkt = ex.pubtkt
-
- except pubtkt.ExpiredError as ex :
- self.alert('warning', ex, icon='clock')
-
- # store it anyways, but not as valid
- self.pubtkt = ex.pubtkt
-
- else :
- # it's a parsed, verified and valid pubtkt
- self.valid_pubtkt = self.pubtkt
-
- return self.pubtkt
-
- def process_back (self) :
- self.server = None
- self.back = urlparse.urlunparse((self.app.login_scheme, self.app.login_server, '/', '', '', ''))
-
- back = self.request.args.get('back')
-
- if back :
- url = urlparse.urlparse(back, self.app.login_scheme)
-
- if not self.app.login_scheme :
- scheme = url.scheme
-
- elif url.scheme == self.app.login_scheme :
- scheme = url.scheme
-
- else :
- self.alert('info', "Using SSL for application URL", icon='lock')
- scheme = self.app.login_scheme
-
- if url.hostname :
- self.server = self.app.check_server(url.hostname)
- else :
- self.server = self.app.login_server
-
- self.back = urlparse.urlunparse((scheme, self.server, url.path, url.params, url.query, url.fragment))
-
- def render_alerts (self) :
- for type, icon, alert in self.alerts :
- yield html.div(class_='alert alert-{type}'.format(type=type))(
- html.span(class_='glyphicon glyphicon-{glyphicon}'.format(glyphicon=icon)) if icon else None,
- alert
- )
-
-
-
-class Index (Handler) :
- TITLE = u"Päivölä Network Login"
-
- STYLE = Handler.STYLE + """
-.pubtkt {
- width: 30em;
- margin: 1em auto;
-}
-
-.pubtkt form {
- display: inline;
-}
-
-.pubtkt .panel-heading {
- line-height: 20px;
-}
-
-.pubtkt .panel-body .glyphicon {
- width: 1em;
- float: left;
- line-height: 20px;
-}
-
-.pubtkt .panel-body .progress {
- margin-bottom: 0;
- margin-left: 2em;
-}
- """
-
- JS = Handler.JS + (
- '/static/pubtkt-expire.js',
- )
-
- def process (self) :
- self.process_cookie()
-
- if not self.pubtkt :
- return self.redirect(Login)
-
- def render_valid (self, valid) :
- seconds = valid.seconds + valid.days * (24 * 60 * 60)
-
- minutes, seconds = divmod(seconds, 60)
- hours, minutes = divmod(minutes, 60)
-
- return "%2d:%02d:%02d" % (hours, minutes, seconds)
-
- def render_status (self, pubtkt) :
- valid = pubtkt.valid()
- grace = pubtkt.grace()
-
- if grace :
- return 'warning'
- elif valid :
- return 'success'
- else :
- return 'danger'
-
- def render_pubtkt_valid (self, pubtkt) :
- """
- Yield HTML for ticket validity.
- """
-
-
- lifetime = self.app.login_valid
- valid = pubtkt.valid()
- grace = pubtkt.grace()
- grace_period = pubtkt.grace_period()
- remaining = pubtkt.remaining()
-
- if valid :
- progress = float(valid.seconds) / float(lifetime.seconds)
- else :
- progress = None
-
- if grace :
- title = "Remaining renewal period"
- label = "{grace} (Renew)".format(grace=self.render_valid(grace))
- status = 'warning'
- elif valid :
- title = "Remaining validity period"
- label = "{valid}".format(valid=self.render_valid(valid))
- status = 'success'
- else :
- title = "Expired"
- label = "Expired"
- status = 'danger'
-
- if progress :
- return html.div(class_='panel-body', title=title)(
- html.span(class_='glyphicon glyphicon-time'),
- html.div(class_='progress pubtkt-progress',
- data_start=valid.seconds,
- data_refresh=grace_period.seconds if remaining else None,
- data_end=lifetime.seconds,
- )(
- html.div(class_='progress-bar progress-bar-{status}'.format(status=status),
- role='progressbar',
- style='width: {pp:.0f}%'.format(pp=progress*100),
- )(
- html.span(class_='pubtkt-progress-label')(label)
- )
- )
- )
- else :
- return None # html.p(label)
-
- def render_pubtkt_fields (self, pubtkt) :
- """
- Yield (glyphicon, text) to render as fields for ticket.
- """
-
- if pubtkt.cip :
- yield 'cloud', None, "Network address", pubtkt.cip
-
- if pubtkt.udata :
- yield 'comment', None, "User data", pubtkt.udata
-
- for token in pubtkt.tokens :
- yield 'flag', None, "Access token", token
-
- if pubtkt.bauth :
- yield 'keys', None, "Authentication token", pubtkt.bauth
-
- def render_pubtkt (self, pubtkt) :
- status = self.render_status(pubtkt)
- domain = self.app.login_domain
-
- return html.div(class_='pubtkt panel panel-{status}'.format(status=status))(
- html.div(class_='panel-heading')(
- html.span(class_='glyphicon glyphicon-user'),
- html.strong(pubtkt.uid),
- html.span("@", domain),
- ),
- self.render_pubtkt_valid(pubtkt),
- html.ul(class_='list-group')(
- html.li(class_='list-group-item {status}'.format(status=('alert-'+status if status else '')), title=title)(
- html.span(class_='glyphicon glyphicon-{glyphicon}'.format(glyphicon=icon)) if icon else None,
- data,
- ) for icon, status, title, data in self.render_pubtkt_fields(pubtkt)
- ),
- html.div(class_='panel-footer')(
- #html.div(class_='btn-toolbar', role='toolbar')(
- (
- html.form(action=self.url(Login), method='post', class_='form-inline')(
- html.button(type='submit', class_='btn btn-success')(
- html.span(class_='glyphicon glyphicon-time'), "Renew"
- )
- )
- ) if pubtkt.valid() else (
- html.form(action=self.url(Login), method='get', class_='form-inline')(
- html.button(type='submit', class_='btn btn-info')(
- html.span(class_='glyphicon glyphicon-log-in'), "Login"
- )
- ),
- ),
-
- html.form(action=self.url(Logout), method='post', class_='form-inline pull-right')(
- html.button(type='submit', class_='btn btn-warning')(
- html.span(class_='glyphicon glyphicon-log-out'), "Logout"
- )
- ),
- #),
- ),
- )
-
- def render (self) :
- return html.div(class_='container')(
- self.render_alerts(),
- self.render_pubtkt(self.pubtkt) if self.pubtkt else None,
- )
-
-class Login (Handler) :
- TITLE = "Login"
-
- STYLE = Handler.STYLE + """
-form#login {
- max-width: 50%;
- padding: 1em;
- margin: 0 auto;
-}
-
- """
-
- login_failure = None
-
- def process (self) :
- self.process_cookie()
-
- try :
- self.process_back()
- except pubtkt.Error as ex :
- self.alert('danger', ex)
-
- if self.pubtkt :
- self.username = self.pubtkt.uid
- else :
- self.username = None
-
- # update cookie?
- set_pubtkt = None
-
- if self.request.method == 'POST' :
- username = self.request.form.get('username')
- password = self.request.form.get('password')
-
- if username :
- # preprocess
- username = username.strip().lower()
-
- if username and password :
- self.username = username
-
- try :
- set_pubtkt = self.app.auth(username, password)
-
- except pvl.login.auth.AuthError as ex :
- self.alert('danger', "Internal authentication error, try again later?")
-
- else :
- if not set_pubtkt :
- self.alert('danger', "Invalid authentication credentials, try again.")
-
- elif self.pubtkt and self.pubtkt.valid() :
- # renew manually if valid
- set_pubtkt = self.app.renew(self.pubtkt)
-
- # a POST request that does not modify state is a failure
- if not set_pubtkt :
- self.login_failure = True
-
- elif 'renew' in self.request.args :
- # renew automatically if in grace period
- if self.pubtkt and self.pubtkt.grace() :
- set_pubtkt = self.app.renew(self.pubtkt)
-
- if set_pubtkt :
- signed = self.app.sign(set_pubtkt)
-
- self.pubtkt = set_pubtkt
-
- # browsers and mod_pubtkt seem to be very particular about quoting ;'s in cookie values...
- # this follows PHP's setcookie() encoding, without any quoting of the value..
- cookie = '{cookie}={value}; Domain={domain}; Secure; HttpOnly'.format(
- cookie = self.app.cookie_name,
- value = werkzeug.urls.url_quote(signed),
- domain = self.app.cookie_domain,
- )
-
- # redirect with cookie
- response = pvl.web.response.redirect(self.back)
- response.headers.add('Set-Cookie', cookie)
-
- return response
-
- def status (self) :
- if self.login_failure :
- return 400
- else :
- return 200
-
- def render (self) :
- domain = self.app.login_domain
-
- if 'logout' in self.request.args :
- self.alert('info', "You have been logged out.", icon='log-out')
-
- if self.pubtkt and self.pubtkt.valid() :
- renew = True
-
- # within validity period...
- self.alert('info', "Login or renew ticket.", icon='log-in')
-
- else :
- renew = False
-
- return html.div(class_='container')(
- html.form(action=self.url(back=self.back), method='POST', id='login')(
- self.render_alerts(),
-
- html.fieldset(
- html.legend(
- (
- "Login @ ",
- html.a(href=self.back)(self.server),
- ) if self.server else (
- "Login"
- )
- ),
-
- html.div(class_='form-group')(
- html.div(class_='input-group')(
- html.label(for_='username', class_='sr-only')("Username"),
- html.input(name='username', type='text', class_='form-control', placeholder="username", required=True, autofocus=(not self.username), value=self.username),
- html.span(class_='input-group-addon')("@{domain}".format(domain=domain)),
- ),
-
- html.label(for_='password', class_='sr-only')("Password"),
- html.input(name='password', type='password', class_='form-control', placeholder="Password", required=(not renew), autofocus=bool(self.username)),
- ),
-
- html.button(type='submit', class_='btn btn-primary')(
- html.span(class_='glyphicon glyphicon-log-in'), "Login"
- ),
-
- html.button(type='submit', class_='btn btn-success')(
- html.span(class_='glyphicon glyphicon-time'), "Renew"
- ) if renew else None,
- )
- )
- )
-
-class Logout (Handler) :
- TITLE = "Logout"
-
- def process (self) :
- self.process_cookie()
-
- if not self.pubtkt :
- return self.redirect(Login)
-
- if self.request.method == 'POST' :
- response = pvl.web.response.redirect(self.url(Login, logout=1))
-
- response.set_cookie(self.app.cookie_name, '',
- expires = 0,
- domain = self.app.cookie_domain,
- secure = self.app.cookie_secure,
- httponly = self.app.cookie_httponly,
- )
-
- return response
-
- def render (self) :
- return html.div(class_='container')(
- html.form(action=self.url(), method='post')(
- self.render_alerts(),
-
- html.fieldset(
- html.legend("Logout {pubtkt.uid}".format(pubtkt=self.pubtkt)),
-
- html.button(type='submit', class_='btn btn-warning')(
- html.span(class_='glyphicon glyphicon-log-out'), "Logout"
- ),
- )
- )
- )
-
-class SSL (Handler) :
- TITLE = "SSL"
-
- OUT = 'tmp/spkac'
-
- def render_cert (self) :
- return html.div(class_='container')(
- self.render_alerts(),
- html.div(class_='alert alert-success')(
- "Your new SSL client cert has been signed, and should shortly be installed within your browser."
- )
- )
-
- def respond_cert (self, cert) :
- """
- Generate a response for a signed cert, showing the user an informational page, and redirecting to the cert itself..
- """
-
- location = self.url(SSL, cert=cert)
-
- return pvl.web.Response(
- self.render_html(
- body = self.render_cert(),
- extrahead = html.meta(http_equiv='refresh', content='0;{location}'.format(location=location)),
- ),
- status = 200,
- #headers = {
- # 'Location': location
- #},
- mimetype = 'text/html',
- )
-
- def process_spkac (self, spkac) :
- log.info("SPKAC: %s", spkac)
-
- try :
- cert = self.app.ssl_sign(self.pubtkt, spkac)
- except pvl.login.ssl.Error as ex :
- self.alert('danger', ex)
- return
-
- log.info("Redirecting to client cert: %s", cert)
- return self.respond_cert(cert)
-
- def process_cert (self, cert) :
- """
- Return user cert as download.
-
- Uses the application/x-x509-user-cert mimetype per
- https://developer.mozilla.org/en-US/docs/NSS_Certificate_Download_Specification
- """
-
- try :
- file = self.app.ssl_open(self.pubtkt, cert)
- except pvl.login.ssl.Error as ex :
- self.alert('danger', ex)
- return
-
- log.info("Returning client cert: %s", file)
-
- return pvl.web.Response(self.response_file(file), mimetype='application/x-x509-user-cert')
-
- def process (self, cert=None) :
- if not self.process_cookie() :
- return self.redirect(Login, back=self.url())
-
- self.sslcert_dn = self.request.headers.get('X-Forwarded-SSL-DN')
-
- if cert :
- return self.process_cert(cert)
-
- if self.request.method == 'POST' :
- spkac = self.request.form.get('spkac')
-
- if spkac:
- return self.process_spkac(spkac)
-
- def render (self) :
- if self.sslcert_dn :
- self.alert('info', "You are currently using a client SSL cert: {self.sslcert_dn}".format(self=self))
-
- return html.div(class_='container')(
- html.form(action=self.url(), method='post')(
- self.render_alerts(),
- html.fieldset(
- html.legend("SSL Login"),
-
- html.keygen(name='spkac', challenge='foo', keytype='RSA'),
-
- html.button(type='submit', class_='btn')(
- "Generate Certificate"
- ),
- )
- )
- )
-
-class LoginApplication (pvl.web.Application) :
- URLS = urls.Map((
- urls.rule('/', Index),
- urls.rule('/login', Login),
- urls.rule('/logout', Logout),
-
- # proto
- urls.rule('/ssl', SSL),
- urls.rule('/ssl/<cert>', SSL),
- ))
-
- PUBLIC_KEY = 'etc/login/public.pem'
- PRIVATE_KEY = 'etc/login/private.pem'
-
- login_domain = 'test.paivola.fi'
- login_server = 'login.test.paivola.fi'
- login_valid = datetime.timedelta(minutes=60)
- login_grace = datetime.timedelta(minutes=15)
- login_scheme = 'https'
-
- cookie_name = 'auth_pubtkt'
- cookie_domain = 'test.paivola.fi'
- cookie_secure = True
- cookie_httponly = True
-
- def __init__ (self, auth, ssl=None, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) :
- super(LoginApplication, self).__init__(**opts)
-
- self._auth = auth
- self._ssl = ssl
- self.server_keys = pubtkt.ServerKeys.config(
- public_key = public_key,
- private_key = private_key,
- )
-
- def check_server (self, server) :
- """
- Check that the given target server is valid.
- """
-
- server = server.lower()
-
- if server == self.login_domain or server.endswith('.' + self.login_domain) :
- return server
- else :
- raise pubtkt.ServerError("Target server is not covered by our authentication domain: {domain}".format(domain=self.login_domain))
-
- def load (self, cookie) :
- """
- Load a pubtkt from a cookie, and verify it.
- """
-
- return pubtkt.PubTkt.load(cookie, self.server_keys.public)
-
- def auth (self, username, password) :
- """
- Perform authentication, returning a PubTkt (unsiigned) or None.
-
- Raises auth.AuthError.
- """
-
- auth = self._auth.auth(username, password)
-
- if not auth :
- return None
-
- return pubtkt.PubTkt.new(username,
- valid = self.login_valid,
- grace = self.login_grace,
- tokens = list(self._auth.access(auth)),
- udata = self._auth.userdata(auth),
- )
-
- def sign (self, pubtkt) :
- """
- Create a cookie by signing the given pubtkt.
- """
-
- return pubtkt.sign(self.server_keys.private)
-
- def renew (self, pubtkt) :
- """
- Renew and re-sign the given pubtkt.
- """
-
- auth = self._auth.renew(pubtkt.uid)
-
- if not auth :
- raise pubtkt.RenewError("Unable to re-authenticate")
-
- return pubtkt.update(
- valid = self.login_valid,
- grace = self.login_grace,
- tokens = list(self._auth.access(auth)),
- udata = self._auth.userdata(auth),
- )
-
- def ssl_sign (self, pubtkt, spkac) :
- """
- Generate a SSL client cert for the given user.
-
- Returns the redirect token for downloading it.
-
- Raises pvl.login.ssl.Error
- """
-
- if not self._ssl :
- raise pvl.login.ssl.Error("No ssl CA available for signing")
-
- return self._ssl.sign_user(pubtkt.uid, spkac,
- userinfo = pubtkt.udata,
- )
-
- def ssl_open (self, pubtkt, cert) :
- """
- Open and return an SSL cert file.
-
- Raises pvl.login.ssl.Error
- """
-
- return self._ssl.open_cert(pubtkt.uid, cert)
--- a/pvl/login/ssl.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-# encoding: utf-8
-
-import base64
-import datetime
-import hashlib
-import os
-import os.path
-import string
-
-import pvl.invoke
-
-import logging; log = logging.getLogger('pvl.login.ssl')
-
-class Error (Exception) :
- pass
-
-class UsersCA (object) :
- OPENSSL = '/usr/bin/openssl'
-
- SIGN_DAYS = 1
-
- VALID_USER = set(string.letters + string.digits + '-.')
-
- O = u"Päivölän Kansanopisto"
- OU = u"People"
- DC = ('paivola', 'fi')
-
- def __init__ (self, ca, users) :
- self.ca = ca
- self.users = users
-
- self.ca_config = os.path.join(ca, 'openssl.cnf')
-
- def sign_spkac (self, out, spkac, days=SIGN_DAYS) :
- """
- Sign given request file (path).
-
- Creates the given output file (path). Empty file on errors..
- """
-
- pvl.invoke.invoke(self.OPENSSL, ('ca',
- '-config', self.ca_config,
- '-spkac', spkac,
- '-out', out,
- '-policy', 'policy_user',
- '-days', str(days),
- '-utf8',
- ),
- setenv={
- 'CA': self.ca,
- },
- )
-
- def generate_dn (self, uid, cn=None) :
- """
- Generate OpenSSL (rdn, value) pairs for given user.
- """
-
- if self.O :
- yield 'O', self.O
-
- elif self.DC :
- for index, dc in enumerate(self.DC, 1) :
- yield '{index}.DC'.format(index=index), dc
-
- yield 'OU', self.OU
-
- yield 'UID', uid
-
- if cn :
- yield 'CN', cn
-
- def write_spkac (self, path, spkac, dn) :
- """
- Write out a spkac file to the given path, containing the given base64-encoded spkac and DN.
- """
-
- # roundtrip the spkac for consistent formatting
- spkac = base64.b64encode(base64.b64decode(spkac))
-
- file = open(path, 'w')
-
- file.write('SPKAC=')
- file.write(spkac)
- file.write('\n')
-
- for rdn, value in dn :
- file.write(u'{rdn}={value}\n'.format(rdn=rdn, value=value).encode('utf-8'))
-
- file.close()
-
- def sign_user (self, user, spkac, userinfo=None) :
- """
- Sign given spkac string (base64-encoded) for given user.
-
- Returns a name for the signed cert.
- """
-
- if not set(user).issubset(self.VALID_USER) :
- raise Error("Invalid username: {user}".format(user=user))
-
- dir = os.path.join(self.users, user)
-
- if not os.path.exists(dir) :
- os.mkdir(dir)
-
- name = hashlib.sha1(user + spkac).hexdigest()
- spkac_file = os.path.join(dir, name) + '.spkac'
- cert_file = os.path.join(dir, name)
- tmp_file = os.path.join(dir, name) + '.tmp'
-
- # the req to sign
- if os.path.exists(spkac_file) :
- log.warning("spkac already exists: %s", spkac_file)
- else :
- log.info("%s: write spkac: %s", user, spkac_file)
- self.write_spkac(os.path.join(dir, name) + '.spkac', spkac, self.generate_dn(user, userinfo))
-
- # sign it
- if os.path.exists(cert_file) :
- log.warning("cert already exists: %s", cert_file)
- return name
-
- if os.path.exists(tmp_file) :
- log.warning("cleaning out previous tmp file: %s", tmp_file)
- os.unlink(tmp_file)
-
- log.info("%s: sign cert: %s", user, cert_file)
- self.sign_spkac(tmp_file, spkac_file)
-
- log.debug("%s: rename %s -> %s", user, tmp_file, cert_file)
- os.rename(tmp_file, cert_file)
-
- return name
-
- def open_cert (self, user, name) :
- """
- Return an opened cert file by username / cert name.
- """
-
- if not set(user).issubset(self.VALID_USER) :
- raise Error("Invalid username: {user}".format(user=user))
-
- path = os.path.join(self.users, user, name)
-
- if not os.path.exists(path) :
- raise Error("No cert found on server")
-
- return open(path)
--- a/pvl/login/static/pubtkt-expire.js Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-function pad2digits (i) {
- if (i < 10) {
- return "0" + i;
- } else {
- return "" + i;
- }
-}
-
-function seconds2interval (s) {
- var h, m;
-
- s = Math.floor(s);
-
- m = Math.floor(s / 60);
- s = s % 60;
-
- h = Math.floor(m / 60);
- m = m % 60;
-
- return h + ":" + pad2digits(m) + ":" + pad2digits(s);
-}
-
-$(function () {
- $('.pubtkt-progress').each(function () {
- var item = $(this);
-
- var progress_bar = item.find('.progress-bar');
- var progress_label = item.find('.pubtkt-progress-label');
-
- var progress_start = item.attr('data-start');
- var progress_refresh = item.attr('data-refresh');
- var progress_end = item.attr('data-end');
-
- var start_time = Date.now();
-
- var reload = false;
-
- function update_progress () {
- var duration = (Date.now() - start_time) / 1000;
-
- var progress = progress_start - duration;
-
- if (reload) {
- // delayed reload
- window.location.reload(true);
- }
-
- if (progress <= 0 || (progress_refresh && progress < progress_refresh)) {
- // done
- reload = true;
- } else {
- // update
- progress_bar.css('width', (progress * 100 / progress_end) + '%');
-
- progress_label.html(seconds2interval(progress));
- }
- }
-
- window.setInterval(update_progress, 1000);
- });
-});
-
--- a/pvl/rrd/__init__.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,15 +0,0 @@
-"""
- RRD Graphing
-
- Requires:
- python-rrdtool
-"""
-from pvl.rrd.rrds import (
- RRDDatabase,
- RRDCache,
-)
-
-from pvl.rrd import (
- api as rrd,
- graph,
-)
--- a/pvl/rrd/api.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,128 +0,0 @@
-import rrdtool
-
-import pvl.invoke
-import tempfile
-
-import logging; log = logging.getLogger('pvl.rrd.api')
-
-"""
- Wrapper around the rrdtool python interface
-"""
-
-def timestamp (time=None) :
- """
- Format datetime value for rrdtool.
- """
-
- if not time :
- return None
-
- elif isinstance(time, datetime.datetime) :
- return int(time.mktime(dt.timetuple()))
-
- elif isinstance(time, datetime.timedelta) :
- raise NotImplementedError("pvl.rrd.api.timestamp: timedelta")
-
- else :
- # dunno
- return str(dt)
-
-def cmd (func, pre, opts, post) :
- """
- Run the given rrdtool.* function, formatting the given positional arguments and options.
-
- Returns the return value, which varies...
- """
-
- log.debug("%s: %s: %s: %s", func, pre, opts, post)
-
- # { opt: arg } -> [ '--opt', arg ]
- opts = pvl.invoke.optargs(**opts)
-
- # positional arguments
- pre = pvl.invoke.optargs(*pre)
- post = pvl.invoke.optargs(*post)
-
- return func(*(pre + opts + post))
-
-def graph (out=None, *defs, **opts) :
- """
- Render a graph image and/or print a report from data stored in one or several RRDs.
-
- out - None -> tempfile
- - False -> stdout
- - path -> write to file
-
- Returns:
- (width, height) - pixel dimensions of the resulting graph image
- report_output - any PRINT'd output (?)
- graph_file - file-like object containing graph image, unless out=False -> stdout
-
- With out=None, the returned graph_file is a tempfile which will be cleaned up by Python once close()'d!
-
- XXX: tempfile suffix as .png?
- """
-
- if out is None :
- # tempfile
- out_file = tempfile.NamedTemporaryFile(suffix='.png', delete=True) # python2.6
- out_path = out_file.name
-
- elif out is False :
- out_file = None
- out_path = '-'
-
- else :
- # for reading
- out_path = out
- out_file = True # open later
-
- log.debug("%s", out_path)
-
- # XXX: handle tempfile close?
- width, height, out_lines = cmd(rrdtool.graph, (out_path, ), opts, defs)
-
- if out_file is True :
- out_file = open(out_path)
-
- return (width, height), out_lines, out_file
-
-def report (*defs, **opts) :
- """
- Render an output-less graph with only PRINT statements as a textual report.
-
- Returns output lines.
- """
-
- # exec
- width, height, out_lines = cmd(rrdtool.graph, ('/dev/null', ), opts, defs)
-
- return out_lines
-
-def fetch (rrd, cf, **opts) :
- """
- Fetch values from RRD.
-
- Returns
- (start, end, step) - xrange(...) for row timestamps
- (ds, ...) - columns (ds name)
- ((value, ...), ...) - rows (data by ds)
- """
-
- return cmd(rrdtool.fetch, (rrd, cf), opts, ())
-
-def _fetch (rrd, cf='AVERAGE', resolution=None, start=None, end=None, **opts) :
- """
- Yields (timestamp, { ds: value }) for given ds-values, or all.
- """
-
- steps, sources, rows = fetch(rrd, cf,
- resolution = resolution,
- start = timestamp(start),
- end = timestamp(end),
- **opts
- )
-
- for ts, row in zip(xrange(*steps), rows) :
- yield datetime.fromtimestamp(ts), dict(zip(sources, row))
-
--- a/pvl/rrd/args.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,46 +0,0 @@
-import optparse
-
-import pvl.rrd.graph # interface
-from pvl.rrd import RRDDatabase, RRDCache
-
-import logging; log = logging.getLogger('pvl.rrd.args')
-import sys
-
-def parser (parser) :
- """
- optparse OptionGroup.
- """
-
- parser = optparse.OptionGroup(parser, "RRD options")
-
- parser.add_option('--rrd-type', metavar='TYPE', default='collectd',
- help="mrtg/collectd")
-
- parser.add_option('--rrd', metavar='PATH',
- help="Find RRD files")
-
- parser.add_option('--rrd-cache', metavar='PATH',
- help="Cache RRD graphs")
-
- return parser
-
-def apply (options) :
- """
- Return RRDDatabase from options.
- """
- # path
- if not options.rrd :
- log.error("no --rrd given")
- sys.exit(2)
-
- # type
- graph_type = pvl.rrd.graph.interface_type(options.rrd_type)
-
- # cache
- if options.rrd_cache :
- cache = RRDCache(options.rrd_cache)
- else :
- cache = None
-
- return RRDDatabase(options.rrd, graph_type, cache)
-
--- a/pvl/rrd/graph.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,299 +0,0 @@
-from pvl.rrd import api as rrd
-import time
-
-from pvl.invoke import merge # XXX
-
-import logging; log = logging.getLogger('pvl.rrd.graph')
-
-"""
- RRDTool graph builders.
-
- Includes support for Smokeping-inspired interface graphs from MRTG/Collectd.
-"""
-
-def timestamp () :
- return time.strftime("%Y/%m/%d %H:%M:%S %Z")
-
-class Graph (object) :
- """
- Render an RRD graph from definitions/options.
-
- Acts as a semi-magical object, with immutable state, and methods that return updated copies of our state.
-
- >>> Graph()
- Graph()
- >>> Graph('foo')
- Graph('foo')
- >>> Graph(bar='bar')
- Graph(bar='bar')
- >>> Graph('foo', bar='bar')
- Graph('foo', bar='bar')
- >>> Graph('foo')(bar='bar')
- Graph('foo', bar='bar')
- """
-
- def __init__ (self, *defs, **opts) :
- self.defs = defs
- self.opts = opts
-
- def __call__ (self, *defs, **opts) :
- return type(self)(*(self.defs + defs), **merge(self.opts, opts))
-
- def __getitem__ (self, key) :
- return self.opts[key]
-
- def graph (self, out) :
- """
- Render completed graph using pvl.rrd.api.graph()
- """
-
- return rrd.graph(out, *self.defs, **self.opts)
-
- def __repr__ (self) :
- return "{type}({args})".format(
- type = self.__class__.__name__,
- args = ', '.join([repr(def_) for def_ in self.defs] + [str(opt) + '=' + repr(value) for opt, value in self.opts.iteritems()]),
- )
-
-class Interface (Graph) :
- """
- An RRD graph showing in/out traffic in bits/s on an interface, with style/interval support.
- """
-
- @classmethod
- def build (cls, title, rrd, style, interval) :
- """
- Return a simple Graph using the given title, RRD and style/interval
-
- title - common(title=...)
- rrd - data(rrd=...)
- style - style(style=...)
- interval - interval(interval=...)
- """
-
- return cls().common(title).source(rrd).style(style).interval(interval)
-
- # builders
- def common (self, title) :
- """
- Common options for all views
- """
-
- return self(
- # output
- imgformat = "PNG",
- # lazy = True,
-
- color = [
- # disable border
- # border = 0,
- "SHADEA#ffffff00",
- "SHADEB#ffffff00",
-
- # keep background transparent
- "BACK#ffffff00",
- "SHADEB#ffffff00",
- ],
-
- # labels
- vertical_label = "bits/s",
- title = title,
- units = "si",
-
- # use logarithmic scaling?
- # logarithmic = True,
-
- # smooth out lines
- slope_mode = True,
- )
-
- ## data
- # DS names; in/out bytes
- IN = OUT = None
-
- def source (self, rrd) :
- """
- Abstract: rrd -> in/out
- """
-
- if not (self.IN and self.OUT) :
- raise TypeError("No IN/OUT DS names for %s" % (self, ))
-
- log.debug("%s", rrd)
-
- return self(
- # data sources, bytes/s
- r'DEF:in0={rrd}:{ds_in}:AVERAGE'.format(rrd=rrd, ds_in=self.IN),
- r'DEF:out0={rrd}:{ds_out}:AVERAGE'.format(rrd=rrd, ds_out=self.OUT),
-
- # data, bits/s
- 'CDEF:in=in0,8,*',
- 'CDEF:out=out0,8,*',
- )
-
- ## style
- def style_overview (graph) :
- """
- in/out bps -> overview graph
- """
-
- return graph(
- "CDEF:all=in,out,+",
-
- "VDEF:max=all,MAXIMUM",
- "VDEF:avg=all,AVERAGE",
- "VDEF:min=all,MINIMUM",
-
- "LINE1:in#0000FF:In",
- "LINE1:out#00CC00:Out",
-
- "GPRINT:max:%6.2lf %Sbps max",
- "GPRINT:avg:%6.2lf %Sbps avg",
- "GPRINT:min:%6.2lf %Sbps min\\l",
-
- # dimensions
- width = 600,
- height = 50,
- )
-
- def style_detail (graph) :
- """
- in/out bps -> detail graph
- """
-
- return graph(
- # values
- 'VDEF:in_max=in,MAXIMUM',
- 'VDEF:in_avg=in,AVERAGE',
- 'VDEF:in_min=in,MINIMUM',
- 'VDEF:in_cur=in,LAST',
- 'VDEF:out_max=out,MAXIMUM',
- 'VDEF:out_avg=out,AVERAGE',
- 'VDEF:out_min=out,MINIMUM',
- 'VDEF:out_cur=out,LAST',
-
- # legend/graph
- "COMMENT:%4s" % "",
- "COMMENT:%11s" % "Maximum",
- "COMMENT:%11s" % "Average",
- "COMMENT:%11s" % "Minimum",
- "COMMENT:%11s\\l" % "Current",
-
- "LINE1:in#0000FF:%4s" % "In",
- 'GPRINT:in_max:%6.2lf %Sbps',
- 'GPRINT:in_avg:%6.2lf %Sbps',
- 'GPRINT:in_min:%6.2lf %Sbps',
- 'GPRINT:in_cur:%6.2lf %Sbps\\l',
-
- "LINE1:out#00CC00:%4s" % "Out",
- 'GPRINT:out_max:%6.2lf %Sbps',
- 'GPRINT:out_avg:%6.2lf %Sbps',
- 'GPRINT:out_min:%6.2lf %Sbps',
- 'GPRINT:out_cur:%6.2lf %Sbps\\l',
-
- # mark
- "COMMENT:Generated {now}\\r".format(now=timestamp().replace(':', '\\:')),
-
- # dimensions
- width = 600,
- height = 200,
- )
-
- STYLE = {
- 'overview': style_overview,
- 'detail': style_detail,
- }
-
- def style (self, style) :
- return self.STYLE[style](self)
-
- ## interval
- def interval_daily (graph) :
- """
- Common options for the 'daily' view
- """
-
- return graph(
- # labels
- x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
-
- # general info
- title = "Daily {graph[title]}".format(graph=graph),
-
- # interval
- start = "-24h",
- )
-
- def interval_weekly (graph) :
- return graph(
- # labels
- # x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
-
- # general info
- title = "Weekly {graph[title]}".format(graph=graph),
-
- # interval
- start = "-7d",
- )
-
- def interval_yearly (graph) :
- return graph(
- # labels
- # x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M",
-
- # general info
- title = "Yearly {graph[title]}".format(graph=graph),
-
- # interval
- start = "-1y",
- )
-
- INTERVAL = {
- 'daily': interval_daily,
- 'weekly': interval_weekly,
- 'yearly': interval_yearly,
- }
-
- def interval (self, interval) :
- return self.INTERVAL[interval](self)
-
-# specific types
-class Mrtg (Interface) :
- """
- MRTG -> in/out
- """
-
- IN = 'ds0'
- OUT = 'ds1'
-
-class CollectdIfOctets (Interface) :
- """
- Collectd if_octets -> in/out
- """
-
- IN = 'rx'
- OUT = 'tx'
-
-INTERFACE = {
- 'mrtg': Mrtg,
- 'collectd': CollectdIfOctets,
-}
-
-def interface_type (type) :
- """
- Lookup Interface -subclass for given type.
- """
-
- return INTERFACE[type]
-
-# XXX: legacy
-def mrtg (style, interval, title, rrd, out) :
- return MrtgInterface.build(title, rrd, style, interval).graph(out)
-
-def collectd_ifoctets (style, interval, title, rrd, out) :
- return CollectdIfOctets.build(title, rrd, style, interval).graph(out)
-
-if __name__ == '__main__':
- import doctest
- doctest.testmod()
-
--- a/pvl/rrd/hosts.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,121 +0,0 @@
-"""
- Handle host-interface mappings for stats.
-"""
-
-import shlex
-import os.path
-
-import logging; log = logging.getLogger('pvl.rrd.hosts')
-
-def hostjoin (*hosts) :
- """
- DNS hostname join.
- """
-
- return '.'.join(hosts)
-
-def hostreverse (host) :
- """
- Reverse hostname.
- """
-
- return '.'.join(reversed(host.split('.')))
-
-def load (file, domain=None) :
- """
- Parse hosts from file, yielding
- (host, node, iface, tag)
-
- file - read host/ifaces from file
- domain - append given domain to hostname
- """
-
- host = None
-
- for idx, line in enumerate(file, 1) :
- line = line.rstrip()
-
- if not line :
- continue
-
- # comment?
- if line.startswith('#') :
- continue
-
- # line
- parts = shlex.split(line)
-
- if not line[0].isspace() :
- host = parts.pop(0)
-
- # host-spec?
- if '=' in host :
- host, node = host.split('=')
- else :
- node = host
-
- if '@' in host :
- host, host_domain = host.split('@')
- else:
- host_domain = domain
-
- if '@' in node :
- node, node_domain = node.split('@')
- else:
- node_domain = domain
-
- # host has domain in collectd?
- if host_domain :
- host = hostjoin(host, host_domain)
-
- if not parts :
- # keep host for following lines
- continue
-
- instance = parts.pop(0)
-
- # possibly multiple tags..
- for tag in parts :
- yield host, instance, (node_domain, node, tag)
-
-def map_interfaces (options, file):
- """
- Read (hostname, instance}: (nodename, tag) pairs from file.
- """
-
- for host, instance, node_info in load(file) :
- log.debug("%s/%s -> %s", host, instance, node_info)
- yield (host, instance), node_info
-
-def collectd_interfaces (options, file, collectd_domain, collectd_plugin) :
- """
- Read collectd (host, instance, (domain, node, tag)) items, and yield (collectd-rrd, node_info) tuples.
-
- file - read host/ports from file
- collectd_domain - append given domain to collectd hostname
- collectd_plugin - use given collectd plugin's type-instances
- """
-
- log.info("%s/${host}.%s/%s/%s-${instance}.rrd", options.collectd_rrd, collectd_domain, collectd_plugin, options.collectd_type)
-
- for host, instance, node_info in load(file, domain=collectd_domain) :
- if options.collectd_instance == 'type' :
- type = options.collectd_type + '-' + instance
- else :
- type = options.collectd_type
-
- if options.collectd_instance == 'plugin' :
- plugin = collectd_plugin + '-' + instance
- else :
- plugin = collectd_plugin
-
- rrd = os.path.join(options.collectd_rrd, host, plugin, type) + '.rrd'
-
- if not os.path.exists(rrd) :
- log.warn("%s/%s: missing collectd rrd: %s", host, instance, rrd)
- continue
-
- # out
- log.debug("%s: %s", rrd, node_info)
-
- yield rrd, node_info
--- a/pvl/rrd/rrds.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,239 +0,0 @@
-"""
- RRD file on filesystem.
-"""
-
-import os, os.path, errno
-
-import logging; log = logging.getLogger('pvl.rrd.rrds')
-
-class RRDDatabase (object) :
- """
- A filesystem directory containing .rrd files.
- """
-
- def __init__ (self, path, graph_type, cache=None) :
- """
- path - path to rrd dirs
- graph_type - pvl.rrd.graph.InterfaceGraph type
- cache - optional RRDCache
- """
-
- if not path :
- raise ValueError("RRDDatabase: no path given")
-
- log.info("%s: graph_type=%s, cache=%s", path, graph_type, cache)
-
- self.graph_type = graph_type
- self._path = path
- self.cache = cache
-
- def path (self, *nodes) :
- """
- Lookup and full filesystem path to the given relative RRD/dir path.
-
- Raises ValueError if invalid path.
- """
-
- # relative dir (no leading slash) -> absolute path
- path = os.path.normpath(os.path.join(self._path, *nodes))
-
- log.debug("%s: %s -> %s", self, nodes, path)
-
- # check inside base path
- if not path.startswith(self._path.rstrip('/')) :
- # mask
- raise ValueError("%s: Invalid path: %s" % (self, nodes))
-
- # ok
- return path
-
- def tree (self, node=None) :
- """
- Lookup and return XXX:RRDTree for given node, or root tree.
-
- Raises ValueError if invalid path, or no such tree.
- """
-
- # lookup fs path
- if node :
- path = self.path(node)
- else :
- path = self.path()
-
- # found?
- if not os.path.isdir(path) :
- raise ValueError("%s: Invalid tree: %s: %s" % (self, node, path))
-
- if node :
- return node
- else :
- return '' # equivalent
-
- def rrd (self, node, tree=None) :
- """
- Lookup and return RRD for given node.
- """
-
- if tree :
- node = os.path.join(tree, node)
-
- path = self.path(node) + '.rrd'
-
- if not os.path.isfile(path) :
- raise ValueError("%: Invalid rrd: %s: %s" % (self, node, path))
-
- return RRD(self, node, self.graph_type)
-
- def list (self, tree=None) :
- """
- List (trees, rrds) under given tree.
- """
-
- dirs = []
- rrds = []
-
- path = self.path(tree)
-
- for name in os.listdir(path) :
- if name.startswith('.') :
- continue
-
- path = self.path(tree, name)
-
- basename, extname = os.path.splitext(name)
-
- log.debug("%s: %s: %s: %s", self, tree, name, path)
-
- if os.path.isdir(path) :
- dirs.append(name)
-
- elif extname == '.rrd' :
- # without the .rrd
- rrds.append(basename)
-
- # return sorted lists
- return sorted(dirs), sorted(rrds)
-
- def graph (self, rrd, style, interval, cache=True) :
- """
- Cached RRD.graph(), returning outfile.
-
- Uses self.cache if cache, otherwise graphs to tempfile.
- """
-
- path = rrd.path()
-
- if self.cache and cache :
- # path in cache
- out = self.cache.path(style, interval, str(rrd))
-
- # hit/miss?
- cache = self.cache.lookup(path, out)
-
- else :
- # tempfile
- out = None
- cache = None
-
- log.debug("%s: %s: %s", self, rrd, out)
-
- if cache :
- # from cache
- outfile = open(out)
-
- else :
- # to cache/tempfile
- dimensions, lines, outfile = rrd.graph(style, interval).graph(out)
-
- return outfile
-
- def __str__ (self) :
- return self._path
-
-class RRD (object) :
- """
- An .rrd file on the filesystem, graphed using pvl.rrd.graph.Interface.
- """
-
- def __init__ (self, db, rrd, graph_type) :
- self.db = db
- self.rrd = rrd
- self.graph_type = graph_type
-
- def path (self) :
- return self.db.path(self.rrd) + '.rrd'
-
- def graph (self, style, interval) :
- """
- Build Graph given rrd using given style/interval.
- """
-
- path = self.path()
-
- title = str(self) # " / ".join(rrd.split('/'))
- return self.graph_type.build(title, path, style, interval)
-
- def __str__ (self) :
- return self.rrd
-
-class RRDCache (object) :
- """
- Cache graph output in the filesystem.
- """
-
- def __init__ (self, path) :
- log.info("%s", path)
-
- self._path = path
-
- def path (self, *key) :
- """
- Return cache path for key.
- """
-
- return os.path.join(self._path, *key) + '.png'
-
- def _stat (self, path) :
- """
- os.stat or None.
- """
-
- try :
- return os.stat(path)
-
- except OSError as ex :
- if ex.errno == errno.ENOENT :
- return None
- else :
- raise
-
- def lookup (self, source, path) :
- """
- Return hit from cache.
- """
-
- # create
- dir = os.path.dirname(path)
- if not os.path.isdir(dir) :
- log.warn("makedirs %s", dir)
- os.makedirs(dir)
-
- # stats's
- src = self._stat(source)
- dst = self._stat(path)
-
- if dst and src.st_mtime < dst.st_mtime :
- log.debug("%s: %s: %s: hit", self, source, path)
-
- return True
-
- elif not dst:
- log.debug("%s: %s: %s: miss", self, source, path)
- return False
-
- else :
- log.debug("%s: %s: %s: update", self, source, path)
- return None
-
- def __str__ (self) :
- return self._path
--- a/pvl/verkko/__init__.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-
-from pvl.verkko import db
-from pvl.verkko.db import Database
-
-__version__ = '0.7.3-1'
--- a/pvl/verkko/db.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,167 +0,0 @@
-"""
- SQLAlchemy Database tables model for pvl.verkko
-"""
-
-import sqlalchemy
-from sqlalchemy import *
-
-import logging; log = logging.getLogger('pvl.verkko.db')
-
-# schema
-metadata = MetaData()
-
-dhcp_hosts = Table('dhcp_hosts', metadata,
- Column('id', Integer, primary_key=True),
-
- # unique
- Column('ip', String, nullable=False),
- Column('mac', String, nullable=False),
- Column('gw', String, nullable=False),
-
- # updated
- Column('first_seen', DateTime, nullable=False),
- Column('last_seen', DateTime, nullable=False),
-
- # scalar; updated
- Column('name', String, nullable=True),
- Column('state', String, nullable=True),
- Column('error', String, nullable=True),
-
- # counters
- Column('count', Integer, default=1),
-
- UniqueConstraint('ip', 'mac', 'gw'),
-)
-
-dhcp_leases = Table('dhcp_leases', metadata,
- Column('id', Integer, primary_key=True),
-
- Column('ip', String, nullable=False),
- Column('mac', String, nullable=False),
- Column('hostname', String, nullable=True),
-
- Column('starts', DateTime, nullable=False),
- Column('ends', DateTime, nullable=True), # never
- # TODO: renewed
-
- Column('state', String, nullable=True),
- Column('next', String, nullable=True),
-)
-
-wlan_sta = Table('wlan_sta', metadata,
- Column('id', Integer, primary_key=True),
-
- Column('ap', String, nullable=False),
- Column('wlan', String, nullable=False),
- Column('sta', String, nullable=False),
-
- # updated
- Column('first_seen', DateTime, nullable=False),
- Column('last_seen', DateTime, nullable=False),
-
- Column('count', Integer, default=1),
- Column('msg', String, nullable=True),
-
- UniqueConstraint('ap', 'wlan', 'sta'),
-)
-
-# for ORM models
-from sqlalchemy.orm import mapper, sessionmaker
-
-Session = sessionmaker()
-
-class Database (object) :
- """
- Our underlying database.
- """
-
- def __init__ (self, database) :
- """
- database - sqlalchemy connection URI
- """
-
- self.engine = create_engine(database,
- echo = (log.isEnabledFor(logging.DEBUG)),
- )
-
- # ORM
- def session (self) :
- """
- Return a new ORM session bound to our engine.
-
- XXX: session lifetimes? Explicit close?
- """
-
- return Session(bind=self.engine)
-
- # SQL
- def connect (self) :
- return self.engine.connect()
-
- def execute (self, query) :
- return self.engine.execute(query)
-
- def select (self, query) :
- return self.engine.execute(query)
-
- def get (self, query) :
- """
- Fetch a single row.
-
- XXX: returning None if not found
- """
-
- return self.select(query).fetchone()
-
- def insert (self, insert) :
- """
- Execute given INSERT query, returning the inserted primary key.
- """
-
- result = self.engine.execute(insert)
-
- id, = result.inserted_primary_key
-
- return id
-
- def update (self, update) :
- """
- Execute given UPDATE query, returning the number of matched rows.
- """
-
- result = self.engine.execute(update)
-
- return result.rowcount
-
-# command-line
-import optparse
-import sys
-
-def parser (parser, table=None) :
- parser.set_defaults(
- create_table = table,
- )
- parser = optparse.OptionGroup(parser, "Verkko Database")
- parser.add_option('--database', metavar='URI',
- help="Connect to given database")
-
- if table is not None :
- parser.add_option('--create', action='store_true',
- help="Initialize database")
-
- return parser
-
-def apply (options, required=True) :
- # db
- if not options.database and required :
- log.error("No database given")
- sys.exit(1)
-
- log.info("Open up database: %s", options.database)
- db = Database(options.database)
-
- if options.create_table is not None and options.create :
- log.info("Creating database tables: %s", options.create_table)
- options.create_table.create(db.engine, checkfirst=True)
-
- return db
--- a/pvl/verkko/dhcp.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,79 +0,0 @@
-# encoding: utf-8
-import pvl.web
-import pvl.verkko.web
-
-from pvl.web import html, urls
-from pvl.verkko import hosts, leases
-
-import logging; log = logging.getLogger('pvl.verkko.dhcp')
-
-class Index (pvl.verkko.web.DatabaseHandler) :
- TITLE = u"Päivölä Verkko"
-
- CSS = pvl.verkko.web.DatabaseHandler.CSS + (
- '/static/dhcp/forms.css',
- )
-
- def render_link (self, title, **opts) :
- return html.a(href=self.url(hosts.ListHandler, **opts))(title)
-
- def render_links (self, attr, titlevalues) :
- for title, value in titlevalues :
- yield html.li(
- self.render_link(title, **{attr: value})
- )
-
- def render (self) :
- return (
- html.h2("Interval"),
- html.ul(
- self.render_links('seen', (
- ("Hour", '1h'),
- ("Day", '1d'),
- #("Month", '30d'),
- #("Year", '365d'),
- )),
- html.li(
- html.a(href=self.url(hosts.RealtimeHandler))("Realtime"),
- ),
- ),
- html.h2("State"),
- html.ul(
- self.render_links('state', (
- ("Valid", ('DHCPACK', 'DHCPRELEASE')),
- ("Incomplete", ('DHCPDISCOVER', 'DHCPOFFER', 'DHCPREQUEST')),
- ("Invalid", ('DHCPNAK', )),
- )),
- ),
-
- html.h2("IP/MAC"),
- html.form(action=self.url(hosts.ListHandler), method='get')(
- html.fieldset(
- html.ul(
- html.li(
- html.label(for_='ip')("IP"),
- html.input(type='text', name='ip'),
- ),
-
- html.li(
- html.label(for_='mac')("MAC"),
- html.input(type='text', name='mac'),
- ),
-
- html.li(
- html.input(type='submit', value="Search"),
- ),
- )
- )
- ),
- )
-
-class Application (pvl.verkko.web.Application) :
- URLS = urls.Map((
- urls.rule('/', Index),
- urls.rule('/hosts/', hosts.ListHandler),
- urls.rule('/hosts/<int:id>', hosts.ItemHandler),
- urls.rule('/hosts/realtime', hosts.RealtimeHandler),
- urls.rule('/leases/', leases.ListHandler),
- ))
-
--- a/pvl/verkko/hosts.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,412 +0,0 @@
-from pvl.verkko import web, db, table
-from pvl.verkko.utils import parse_timedelta, IPv4Network
-
-from pvl.web import html, response
-
-import re
-import datetime
-import socket # dns
-
-import logging; log = logging.getLogger('pvl.verkko.hosts')
-
-## Model
-import json
-import time
-
-def dt2ts (dt) :
- return int(time.mktime(dt.timetuple()))
-
-def ts2dt (ts) :
- return datetime.datetime.fromtimestamp(ts)
-
-# TODO: this should be DHCPHost
-class Host (object) :
- DATE_FMT = '%Y%m%d'
- TIME_FMT = '%H:%M:%S'
-
- MAC_HEX = r'([A-Za-z0-9]{2})'
- MAC_SEP = r'[-:.]?'
- MAC_RE = re.compile(MAC_SEP.join([MAC_HEX] * 6))
-
- @classmethod
- def normalize_mac (cls, mac) :
- match = cls.MAC_RE.search(mac)
-
- if not match :
- raise ValueError(mac)
-
- else :
- return ':'.join(hh.lower() for hh in match.groups())
-
- def __init__ (self, ip, mac, name=None) :
- self.ip = ip
- self.mac = mac
- self.name = name
-
- def render_mac (self) :
- if not self.mac :
- return None
-
- elif len(self.mac) > (6 * 2 + 5) :
- return u'???'
-
- else :
- return unicode(self.mac)
-
- def network (self) :
- return self.gw
-
- def render_name (self) :
- if self.name :
- return self.name.decode('ascii', 'replace')
- else :
- return None
-
- STATES = {
- 'DHCPACK': 'ack',
- 'DHCPNAK': 'nak',
- 'DHCPRELEASE': 'release',
- 'DHCPDISCOVER': 'search',
- 'DHCPREQUEST': 'search',
- 'DHCPOFFER': 'search',
- }
-
- def state_class (self) :
- if self.error :
- return 'dhcp-error'
-
- elif self.state in self.STATES :
- return 'dhcp-' + self.STATES[self.state]
-
- else :
- return None
-
- def state_title (self) :
- return self.error # or None
-
- def render_state (self) :
- if self.error :
- return "{self.state}: {self.error}".format(self=self)
- else :
- return self.state
-
- @classmethod
- def format_datetime (cls, dt) :
- if (datetime.datetime.now() - dt).days :
- return dt.strftime(cls.DATE_FMT)
-
- else :
- return dt.strftime(cls.TIME_FMT)
-
- def seen (self) :
- return (
- html.span(title=self.first_seen)(self.format_datetime(self.first_seen)),
- '-',
- html.span(title=self.last_seen)(self.format_datetime(self.last_seen))
- )
-
- def dns (self) :
- """
- Reverse-DNS lookup.
- """
-
- if not self.ip :
- return None
-
- sockaddrs = set(sockaddr for family, socktype, proto, canonname, sockaddr in socket.getaddrinfo(self.ip, 0, 0, 0, 0, socket.AI_NUMERICHOST))
-
- for sockaddr in sockaddrs :
- try :
- host, port = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
- except socket.gaierror :
- continue
-
- return host
-
- def __unicode__ (self) :
- return u"{host.ip} ({host.mac})".format(host=self)
-
-db.mapper(Host, db.dhcp_hosts, properties=dict(
- #id = db.dhcp_hosts.c.rowid,
- #state = db.dhcp_hosts.c.,
-))
-
-## Controller
-class HostsTable (table.Table) :
- """
- <table> of hosts.
- """
-
- ITEMS = "Hosts"
- COLUMNS = (
- table.Column('ip', "IP", Host.ip,
- rowfilter = True,
- ),
- table.Column('mac', "MAC", Host.mac, Host.render_mac,
- rowfilter = True,
- ),
- table.Column('name', "Hostname", Host.name, Host.render_name, ),
- table.Column('gw', "Network", Host.gw, Host.network, ),
- table.Column('seen', "Seen", Host.last_seen, Host.seen, ),
- table.Column('state', "State", Host.count,
- rowtitle = Host.state_title,
- rowcss = Host.state_class,
- ),
- )
-
- # XXX: have to set again
- ATTRS = dict((col.attr, col) for col in COLUMNS)
-
- # default
- SORT = Host.last_seen.desc()
- PAGE = 10
-
-class HostsHandler (table.TableHandler, web.DatabaseHandler) :
- """
- Combined database + <table>
- """
-
- CSS = web.DatabaseHandler.CSS + table.TableHandler.CSS + (
- "/static/dhcp/hosts.css",
- )
-
- # view
- TABLE = HostsTable
-
- def query (self) :
- """
- Database SELECT query.
- """
-
- return self.db.query(Host)
-
- def filter_seen (self, value) :
- """
- Return filter expression for given attr == value
- """
-
- column = Host.last_seen
-
- if value.isdigit() :
- # specific date
- date = datetime.datetime.strptime(value, Host.DATE_FMT).date()
-
- return db.between(date.strftime(Host.DATE_FMT),
- db.func.strftime(Host.DATE_FMT, Host.first_seen),
- db.func.strftime(Host.DATE_FMT, Host.last_seen)
- )
- else :
- # recent
- timedelta = parse_timedelta(value)
-
- return ((db.func.now() - Host.last_seen) < timedelta)
-
- # XXX: for sqlite, pgsql should handle this natively?
- # to seconds
- #timeout = timedelta.days * (24 * 60 * 60) + timedelta.seconds
-
- # WHERE strftime('%s', 'now') - strftime('%s', last_seen) < :timeout
- #filter = (db.func.strftime('%s', 'now') - db.func.strftime('%s', Host.last_seen) < timeout)
-
- def filter_ip (self, value) :
- column = Host.ip
-
- # column is IPv4 string literal format...
- if '/' in value :
- return (db.func.inet(Host.ip).op('<<')(db.func.cidr(value)))
- else :
- return (db.func.inet(Host.ip) == db.func.inet(value))
-
- def filter_mac (self, value) :
- return self.filter_attr('mac', Host.normalize_mac(value))
-
-class ItemHandler (HostsHandler) :
- """
- A specific DHCP host, along with a list of related hosts.
- """
-
- def process (self, id) :
- self.hosts = self.query()
- self.host = self.hosts.get(id)
-
- if not self.host :
- raise web.NotFound("No such host: {id}".format(id=id))
-
- self.sorts, self.hosts = self.sort(self.hosts.filter((Host.ip == self.host.ip) | (Host.mac == self.host.mac)))
-
- def title (self) :
- return u"DHCP Host: {self.host}".format(self=self)
-
- def render_host (self, host) :
- """
- Details for specific host.
- """
-
- attrs = (
- ('Network', host.gw),
- ('IP', host.ip),
- ('MAC', host.mac),
- ('Hostname', host.name),
- ('DNS', host.dns()),
- ('First seen', host.first_seen),
- ('Last seen', host.last_seen),
- ('Last state', host.render_state()),
- ('Total messages', host.count),
- )
-
- return (
- html.dl(
- (html.dt(title), html.dd(value)) for title, value in attrs
- )
- )
-
- def render (self) :
- return (
- html.h2('Host'),
- self.render_host(self.host),
-
- html.h2('Related'),
- self.render_table(self.hosts, sort=self.sorts, hilight=dict(ip=self.host.ip, mac=self.host.mac)),
-
- html.a(href=self.url(ListHandler))(html('«'), 'Back'),
- )
-
-class ListHandler (HostsHandler) :
- """
- List of DHCP hosts for given filter.
- """
-
- TABLE_ITEM_URL = ItemHandler
-
- def process (self) :
- # super
- table.TableHandler.process(self)
-
- def title (self) :
- if self.filters :
- return "DHCP Hosts: {filters}".format(filters=self.filters_title())
- else :
- return "DHCP Hosts"
-
- def render (self) :
- return (
- self.render_table(self.query, filters=self.filters, sort=self.sorts, page=self.page),
-
- #html.a(href=self.url())(html('«'), 'Back') if self.filters else None,
- )
-
-class RealtimeHandler (HostsHandler) :
- TITLE = "DHCP Hosts: Pseudo-Realtime.."
- CSS = HostsHandler.CSS + (
- 'http://code.jquery.com/ui/1.9.0/themes/base/jquery-ui.css',
- )
- JS = (
- #"/static/jquery/jquery.js",
- 'http://code.jquery.com/jquery-1.8.2.js',
- 'http://code.jquery.com/ui/1.9.0/jquery-ui.js',
- '/static/dhcp/spin.js',
- '/static/dhcp/table.js',
- '/static/dhcp/hosts.js',
- )
-
- COLUMNS = (
- ( 'ip', 'IP', lambda host: host.ip ),
- ( 'mac', 'MAC', lambda host: host.mac ),
- ( 'name', 'Hostname', lambda host: host.name ),
- ( 'gw', 'Network', lambda host: host.gw ),
- ( 'seen', 'Seen', Host.seen, ),
- ( 'state', 'State', lambda host: host.state ),
- )
-
- def process (self) :
- """
- Either return JSON (if ?t=...), or fetch hosts/t for rendering.
- """
-
- t = self.request.args.get('t')
-
- if t :
- # return json
- t = ts2dt(int(t))
-
- # query
- hosts = self.query()
-
- # always sorted by last_seen
- hosts = hosts.order_by(Host.last_seen.desc())
-
- # filter
- self.filters, hosts = self.filter(hosts)
-
- if t :
- # return json
- hosts = hosts.filter(Host.last_seen > t)
- hosts = list(hosts)
- hosts.reverse()
-
- if hosts :
- # update timestamp to most recent
- t = hosts[-1].last_seen
-
- # json
- data = dict(
- t = dt2ts(t),
- hosts = [dict(self.table.json(host)) for host in hosts],
- )
-
- return response.json(data)
-
- else :
- # render html
- hosts = hosts.limit(self.table.PAGE)
-
- # XXX: testing
- hosts = hosts.offset(1)
-
- # extract timestamp
- for host in hosts :
- self.t = host.last_seen
-
- break
-
- else :
- # no hosts :<
- self.t = datetime.datetime.now()
-
- # store
- self.hosts = hosts
-
- def title (self) :
- if self.filters :
- return "{title}: {filters}".format(title=self.TITLE, filters=self.filters_title())
- else :
- return self.TITLE
-
- def render (self) :
- """
- Render page HTML and initial <table>, along with bootstrap JS (t0, configuration).
- """
-
- return html.div(id='wrapper')(
- html.input(type='submit', id='refresh', value="Refresh"),
- html.input(type='reset', id='pause', value="Pause"),
-
- self.table.render(self.hosts)(id='hosts-realtime'),
-
- html.script(type='text/javascript')(
- """
-$(document).ready(HostsRealtime(Table($('#hosts-realtime'), {table_params}), {params}));
- """.format(
- table_params = json.dumps(dict(
- item_url = self.url(ItemHandler, id='0'),
- columns = [column.attr for column in self.table.columns],
- )),
- params = json.dumps(dict(
- url = self.url(),
- filters = self.filters,
- t = dt2ts(self.t),
- )),
- )
- )
- )
-
--- a/pvl/verkko/leases.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-from pvl.verkko import web, db, table
-from pvl.verkko.utils import parse_timedelta, format_timedelta
-
-from pvl.web import html
-
-import datetime
-
-import logging; log = logging.getLogger('pvl.verkko.leases')
-
-class DHCPLease (object) :
- """
- A DHCP lease with ip/mac and starts/ends
- """
-
- DATE_FMT = '%Y%m%d %H:%M'
- TIME_FMT = '%H:%M:%S'
-
- @classmethod
- def format_datetime (cls, dt) :
- if dt.date() == datetime.date.today() :
- return dt.strftime(cls.TIME_FMT)
- else :
- return dt.strftime(cls.DATE_FMT)
-
- def render_starts (self) :
- return self.format_datetime(self.starts)
-
- def render_ends (self) :
- if self.ends :
- return self.format_datetime(self.ends)
- else :
- return None
-
- def ends_class (self) :
- if self.ends and self.ends > datetime.datetime.now() :
- return 'active'
- else :
- return None
-
- @property
- def length (self) :
- if self.ends :
- return self.ends - self.starts
- else :
- return datetime.datetime.now() - self.starts
-
- def render_length (self) :
- return format_timedelta(self.length)
-
-db.mapper(DHCPLease, db.dhcp_leases, properties=dict(
-
-))
-
-class LeasesTable (table.Table) :
- """
- <table> of leases.
- """
-
- ITEMS = "Leases"
- COLUMNS = (
- table.Column('ip', "IP", DHCPLease.ip,
- rowfilter = True,
- ),
- table.Column('mac', "MAC", DHCPLease.mac,
- rowfilter = True,
- ),
- table.Column('hostname', "Hostname", DHCPLease.hostname),
- table.Column('starts', "Starts", DHCPLease.starts, DHCPLease.render_starts),
- table.Column('ends', "Ends", DHCPLease.ends, DHCPLease.render_ends,
- rowcss = DHCPLease.ends_class
- ),
- table.Column('length', "Lease", DHCPLease.ends - DHCPLease.starts, DHCPLease.render_length),
- )
-
- # XXX: have to set again
- ATTRS = dict((col.attr, col) for col in COLUMNS)
-
- # default
- SORT = DHCPLease.starts.desc()
- PAGE = 20
-
-class LeasesHandler (table.TableHandler, web.DatabaseHandler) :
- """
- Combined database + <table>
- """
-
- CSS = web.DatabaseHandler.CSS + table.TableHandler.CSS + (
- "/static/dhcp/hosts.css",
- )
-
- # view
- TABLE = LeasesTable
-
- def query (self) :
- """
- Database SELECT query.
- """
-
- return self.db.query(DHCPLease)
-
- def filter_starts (self, value) :
- return ((db.func.now() - DHCPLease.starts) < parse_timedelta(value))
-
- def filter_ends (self, value) :
- return ((DHCPLease.ends - db.func.now()) > parse_timedelta(value))
-
- def filter_length (self, value) :
- """
- Filter by leases valid on given date.
- """
-
- dt = datetime.datetime.strptime(value, DHCPLease.DATE_FMT)
-
- return db.between(dt, DHCPLease.starts, DHCPLease.ends)
-
- def filter_ip (self, value) :
- # column is IPv4 string literal format...
- if '/' in value :
- return (db.func.inet(DHCPLease.ip).op('<<')(db.func.cidr(value)))
- else :
- return (db.func.inet(DHCPLease.ip) == db.func.inet(value))
-
-
-class ListHandler (LeasesHandler) :
- """
- List of DHCP leases, using table.TableHandler -> LeasesTable.
- """
-
- #TABLE_ITEM_URL = ItemHandler
-
- def process (self) :
- # super
- table.TableHandler.process(self)
-
- def title (self) :
- if self.filters :
- return "DHCP Leases: {filters}".format(filters=self.filters_title())
- else :
- return "DHCP Leases"
-
- def render (self) :
- return (
- self.render_table(self.query, filters=self.filters, sort=self.sorts, page=self.page),
-
- #html.a(href=self.url())(html('«'), 'Back') if self.filters else None,
- )
-
-
-
--- a/pvl/verkko/rrd.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,262 +0,0 @@
-# encoding: utf-8
-"""
- http://verkko.paivola.fi/rrd
-"""
-
-from pvl import web
-from pvl.web import urls
-from pvl.web.html import tags as html
-
-import pvl.web.response
-import pvl.rrd
-
-import logging; log = logging.getLogger('pvl.verkko.rrd')
-
-# errors
-import os.path
-
-class RRDNotFound (pvl.web.response.NotFound) :
- """
- 404 Not Found for tree/rrd.
- """
-
- def __init__ (self, tree, rrd=None) :
- if tree and rrd :
- path = os.path.join(tree, rrd)
- elif tree :
- path = tree
- elif rrd :
- path = rrd
- else :
- path = ''
-
- pvl.web.response.NotFound.__init__(self, path)
-
-# View/Controller
-class Handler (web.Handler) :
- CSS = (
- "/static/rrd/rrd.css",
- )
-
- def title (self) :
- return u"Network RRD Traffic Graphs"
-
- def breadcrumb (self, _tree, target=None) :
- """
- Yield (title, url) navigation breadcrumbs
- """
-
- yield '/', self.url(Index)
-
- if _tree :
- tree = ''
-
- for part in _tree.split('/') :
- tree = urls.join(tree, part)
-
- yield part, self.url(Index, tree=tree)
-
- if target :
- # Target
- yield target, self.url(Target, tree=tree, target=self.target)
-
- def render_breadcrumb (self, tree, target=None) :
- """
- Render breadcrumb -> html.div
- """
-
- return html.div(id='breadcrumb')(html(" » ".join(
- str(html.a(href=url)(node)) for node, url in self.breadcrumb(tree, target)))
- )
-
-class Index (Handler) :
- """
- Browse trees, show overview graphs for targets.
- """
-
- def _title (self) :
- if self.tree :
- return html(" » ".join(self.tree.split('/')))
- else :
- return ""
-
- def url_tree (self, node) :
- """
- Return url for given sub-node.
- """
-
- if self.tree :
- path = urls.join(self.tree, node)
- else :
- path = node
-
- return self.url(tree=path)
-
- def process (self, tree=None) :
- """
- Lookup path -> self.tree.
- """
-
- if tree :
- try :
- # XXX: unicode?
- self.tree = self.app.rrd.tree(tree)
-
- except ValueError as ex :
- # mask
- raise RRDNotFound(tree)
- else :
- # root
- self.tree = self.app.rrd.tree()
-
- def render_list (self, items) :
- return (
- html.li(class_=('odd' if idx % 2 else 'even'))(item) for idx, item in enumerate(items)
- )
-
- def render_rrd (self, rrd) :
- """
- Render overview link/image for given rrd.
- """
-
- target_url = self.url(Target, tree=self.tree, target=rrd)
- graph_url = self.url(Graph, tree=self.tree, target=rrd)
-
- return html.a(href=target_url)(
- html.h3(rrd),
- html.img(src=graph_url),
- )
-
- def render (self) :
- """
- Render list of trees/rrds.
- """
-
- trees, rrds = self.app.rrd.list(self.tree)
-
- return self.render_breadcrumb(self.tree), html.div(id='overview')(
- html.ul(id='tree-list')(
- self.render_list(
- html.a(href=self.url_tree(subtree))(subtree)
- for subtree in trees)
- ) if trees else None,
-
- html.hr() if trees and rrds else None,
-
- html.ul(id='rrd-list')(
- self.render_list(
- self.render_rrd(rrd)
- for rrd in rrds)
- ) if rrds else None,
- )
-
-class Target (Handler) :
- """
- Show graphs for RRD file.
- """
-
- def _title (self) :
- return html(" » ".join(self.rrd.split('/')))
-
- def process (self, target, tree=None) :
- """
- Lookup tree/target -> self.target
- """
-
- try :
- self.tree = self.app.rrd.tree(tree)
- self.rrd = self.app.rrd.rrd(target, self.tree)
- self.target = target
-
- except ValueError as ex :
- raise RRDNotFound(tree, target)
-
- def render_interval (self, interval, style='detail') :
- """
- Render detail link/image.
- """
-
- graph_url = self.url(Graph, tree=self.tree, target=self.target, style=style, interval=interval)
-
- return (
- html.h2(interval.title()),
- html.img(src=graph_url)
- )
-
- INTERVALS = ('daily', 'weekly', 'yearly')
-
- def render (self) :
- return self.render_breadcrumb(self.tree, self.target), html.div(id='detail')(
- self.render_interval(interval) for interval in self.INTERVALS
- )
-
-from pvl.invoke import merge # XXX
-import werkzeug # wrap_file
-
-class Graph (Handler) :
- """
- Render graph for RRD.
- """
-
- ARGS = { 'interval': 'daily', 'style': 'overview' }
-
- def process (self, tree, target, style, interval) :
- """
- Return Graph for given options.
- """
-
- try :
- self.tree = self.app.rrd.tree(tree)
- self.rrd = self.app.rrd.rrd(target, self.tree)
-
- except ValueError as ex :
- raise RRDNotFound(tree, target)
-
- self.style = style
- self.interval = interval
-
- def render_png (self) :
- """
- Return PNG data as a file-like object for our graph.
- """
-
- return self.app.rrd.graph(self.rrd, self.style, self.interval)
-
- def respond (self) :
- """
- Return Response for our request.
- """
-
- # process params+args -> self.graph
- process = merge(self.params, dict((arg, self.request.args.get(arg, default)) for arg, default in self.ARGS.iteritems()))
- response = self.process(**process)
-
- if response :
- return response
-
- # PNG output
- file = self.render_png()
-
- # respond with file wrapper
- return pvl.web.response.image(self.response_file(file), type='png')
-
-# WSGI
-class Application (web.Application) :
- # dispatch
- URLS = urls.Map((
- urls.rule('/', Index),
- urls.rule('/<target>', Target),
- urls.rule('/<path:tree>/', Index),
- urls.rule('/<path:tree>/<target>', Target),
- urls.rule('/<path:tree>/<target>.png', Graph),
- ))
-
- def __init__ (self, rrd, **opts) :
- """
- rrd - pvl.rrd.RRDDatabase
- """
-
- super(Application, self).__init__(**opts)
-
- self.rrd = rrd
-
--- a/pvl/verkko/table.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,590 +0,0 @@
-from pvl.web import html
-from pvl.verkko import web, db
-
-import math
-
-import logging; log = logging.getLogger('pvl.verkko.table')
-
-class Column (object) :
- """
- web.Table column spec, representing a property of an SQLAlchemy ORM model class.
- """
-
- def __init__ (self, attr, title, column, rowhtml=None, sort=True, filter=True, colcss=True, rowfilter=False, rowtitle=None, rowcss=None) :
- """
- attr - name of column value, used for http query arg, html css class, model getattr()
- title - title of column
- column - the model column property, used to build queries
- rowhtml - function returning column value as HTML for model
- sort - allow sorting by column
- filter - allow filtering by column
- colcss - apply css class for column; True -> attr
- rowfilter - allow filtering by row value
- rowtitle - function returning column title for model
- rowcss - function returning column class for model
- """
-
- if colcss is True :
- colcss = attr
-
- self.attr = attr
- self.title = title
- self.column = column
-
- # column attrs
- self.sort = sort
- self.filter = filter
- self.colcss = colcss
-
- # row attrs
- self.rowhtml = rowhtml
- self.rowfilter = rowfilter
- self.rowtitle = rowtitle
- self.rowcss = rowcss
-
- def render_header (self, sorturl) :
- """
- Render <th> for <thead> in given Table.
- """
-
- header = self.title
-
- if self.sort :
- header = html.a(href=sorturl)(header)
-
- return html.th(header)
-
- def render_filter_input (self, filters) :
- """
- Render filter <input> in given Table.
- """
-
- value = filters.get(self.attr)
-
- if value :
- # XXX: multi-valued filters?
- value = value[0]
- else :
- value = None
-
- return html.input(type='text', name=self.attr, value=value)
-
- def render_header_filter (self, filters) :
- """
- Render <td><input> for <thead> in given Table.
- """
-
- if self.filter :
- input = self.render_filter_input(filters)
- else :
- input = None
-
- return html.td(class_=self.colcss)(input)
-
- def cell_value (self, item) :
- """
- Return value for cell.
- """
-
- # XXX: this is sometimes broken, figure out how to index by column
- return getattr(item, self.attr)
-
- def cell_html (self, item, value) :
- """
- Render contents for <td>.
- """
-
- if self.rowhtml :
- return self.rowhtml(item)
- else :
- return value
-
- def cell_css (self, item, value=None, hilight=None) :
- """
- Return CSS classes for <td>.
- """
-
- if self.colcss :
- yield self.colcss
-
- if self.rowcss :
- yield self.rowcss(item)
-
- if hilight :
- # lookup attr/value
- hilight = self.attr in hilight and value in hilight[self.attr]
-
- if hilight :
- yield 'hilight'
-
- def cell_title (self, item, value=None) :
- """
- Return title= for <td>
- """
-
- if self.rowtitle :
- return self.rowtitle(item)
-
- def render_cell (self, item, table, filters=None, hilight=None) :
- """
- Render <td> for item in <tbody> in given Table.
-
- hilight - optionally higlight given { attr: value }'s using CSS.
- """
-
- value = self.cell_value(item)
-
- if self.rowfilter and filters is not None :
- # filter-link by row-value
- filters = { self.attr: value }
- else :
- filters = None
-
- yield table.render_cell(self.cell_html(item, value),
- css = tuple(self.cell_css(item, value, hilight=hilight)),
- filters = filters,
- title = self.cell_title(item),
- )
-
-class Table (object) :
- """
- Render <table> with Columns from SQLAlchemy ORM model class.
- """
-
- COLUMNS = ()
- ITEMS = None
-
- # attr -> column
- ATTRS = dict((col.attr, col) for col in COLUMNS)
-
- # items per page
- PAGE = 10
-
- def __init__ (self, url, columns=None, table_url=None, item_url=None, caption=None, page=None) :
- """
- url - pvl.web.Handler.url()
- table_url - ListHandler, or self?
- item_url - ItemHandler, or self#id
- columns - sequence of Columns
- caption - optional <caption>
- page - items per page
- """
-
- self.url = url
-
- self.columns = columns or self.COLUMNS
- self.table_url = table_url
- self.item_url = item_url
-
- self.caption = caption
- self.page = page or self.PAGE
-
- def tableurl (self, filters=None, **opts) :
- """
- URL for table with given opts, keeping our sorting/filtering unless overriden.
- """
-
- args = dict()
-
- # apply
- if filters :
- args.update(filters)
-
- if opts :
- args.update(opts)
-
- return self.url(self.table_url, **args)
-
- def sorturl (self, attr, sort=None, **opts) :
- """
- URL for table sorted by given column, reversing direction if already sorting by given column.
- """
-
- if not sort :
- sort = attr
- elif sort.lstrip('+-') != attr :
- sort = attr
- elif sort.startswith('-') :
- sort = "+" + attr
- else :
- sort = "-" + attr
-
- return self.tableurl(sort=sort, **opts)
-
- def itemurl (self, item) :
- """
- URL for given item, by id.
- """
-
- if self.item_url :
- # separate page
- return self.url(self.item_url, id=item.id)
- else :
- # to our table
- return '#{id}'.format(id=item.id)
-
- def render_head (self, filters=None, sort=None) :
- """
- Yield header columns in table header.
- """
-
- # id
- yield html.th('#')
-
- for column in self.columns :
- yield column.render_header(sorturl=self.sorturl(column.attr, sort=sort, filters=filters))
-
- def render_head_filters (self, filters=None) :
- """
- Yield filter columns in table header.
- """
-
- # id
- yield html.td(html.input(type='submit', value=u'\u00BF'))
-
- for column in self.columns :
- yield column.render_header_filter(filters)
-
- def render_cell (self, rowhtml, css=(), filters=None, title=None) :
- """
- Render a single cell.
-
- htmlvalue - rendered value
- css - css classes to apply
- filters - render filter link for filter-values?
- title - mouseover title for cell
- """
-
- if filters :
- cell = html.a(href=self.tableurl(**filters))(rowhtml)
- else :
- cell = rowhtml
-
- css = ' '.join(cls for cls in css if cls)
-
- return html.td(class_=css, title=title)(cell)
-
- def render_row (self, item, **opts) :
- """
- Yield columns for row.
- """
-
- yield html.th(
- html.a(href=self.itemurl(item))("#")
- ),
-
- for column in self.columns :
- yield column.render_cell(item, self, **opts)
-
- def render_body (self, rows, **opts) :
- """
- Yield body rows.
- """
-
- for i, item in enumerate(rows) :
- yield html.tr(class_=('alternate' if i % 2 else None), id=item.id)(
- self.render_row(item, **opts)
- )
-
- def render_pagination (self, page, count=None, **opts) :
- """
- Render pagination links.
- """
-
- if count is not None :
- pages = int(math.ceil(count / self.page))
- else :
- pages = None
-
- if page > 0 :
- yield html.a(href=self.tableurl(page=0, **opts))(html("«« First"))
- yield html.a(href=self.tableurl(page=(page - 1), **opts))(html("« Prev"))
-
- if pages :
- yield html.span("Page {page} of {pages}".format(page=(page + 1), pages=pages))
- else :
- yield html.span("Page {page}".format(page=(page + 1)))
-
- yield html.a(href=self.tableurl(page=(page + 1), **opts))(html("» Next"))
-
- def render_foot (self, query, page, **opts) :
- """
- Render pagination/host count in footer.
- """
-
- # XXX: does separate SELECT count()
- count = query.count()
-
- if page is None :
- return "{count} {items}".format(count=count, items=self.ITEMS)
- else :
- # XXX: count is just the count we got..
- return self.render_pagination(page, count=None, **opts)
-
- def render (self, query, filters=None, sort=None, page=None, hilight=None) :
- """
- Return <table> element. Wrapped in <form> if filters.
- query - filter()'d sort()'d SELECT query()
- filters - None for no filtering ui, dict of filters otherwise.
- sort - None for no sorting ui, sort-attr otherwise.
- page - display pagination for given page
- hilight - { attr: value } cells to hilight
- """
-
- # render table
- table = html.table(
- html.caption(self.caption) if self.caption else None,
- html.thead(
- html.tr(
- self.render_head(filters=filters, sort=sort)
- ),
- (
- html.tr(class_='filter')(self.render_head_filters(filters=filters))
- ) if filters is not None else None,
- ),
- html.tbody(
- self.render_body(query,
- filters = filters,
- hilight = hilight,
- )
- ),
- html.tfoot(
- html.tr(
- html.td(colspan=(1 + len(self.columns)))(
- self.render_foot(query, page, filters=filters, sort=sort)
- )
- )
- )
- )
-
- # filters form?
- if filters is None :
- return table
- else :
- return html.form(method='get', action=self.tableurl())(
- html.input(type='hidden', name='sort', value=sort),
- table,
- )
-
- def json (self, item) :
- """
- Yield JSON params for given item.
- """
-
- yield 'id', item.id
-
- for column in self.columns :
- value = column.cell_value(item)
-
- yield column.attr, dict(
- html = unicode(html(column.cell_html(item, value))),
- css = tuple(column.cell_css(item, value)),
- title = column.cell_title(item, value),
- )
-
-class TableHandler (object) :
- """
- Mixin for handling Table args/rendering.
- """
-
- CSS = (
- "/static/dhcp/table.css",
- )
-
- TABLE = None
- DB_TABLE = None
-
- # target Handlers for table links
- TABLE_URL = None
- TABLE_ITEM_URL = None
-
- def init (self) :
- """
- Bind self.table
- """
-
- super(TableHandler, self).init()
-
- self.table = self.TABLE(self.url,
- table_url = self.TABLE_URL,
- item_url = self.TABLE_ITEM_URL,
- )
-
- def query (self) :
- """
- Database SELECT query.
- """
-
- if self.DB_CLASS is None :
- raise NotImplementedError()
-
- return self.db.query(self.DB_CLASS)
-
- def sort (self, query) :
- """
- Apply ?sort= from requset args to query.
-
- Return { attr: sort }, query
- """
-
- sort = self.request.args.get('sort')
-
- if sort :
- name = sort.lstrip('+-')
- else :
- name = None
-
- if name :
- order_by = self.TABLE.ATTRS[name].column
- else :
- order_by = self.TABLE.SORT # default
-
- # prefix -> ordering
- if not sort :
- pass
- elif sort.startswith('+') :
- order_by = order_by.asc()
- elif sort.startswith('-') :
- order_by = order_by.desc()
- else :
- pass
-
- # apply
- log.debug("sort: %s", order_by)
-
- query = query.order_by(order_by)
-
- return sort, query
-
- def filter_attr (self, attr, value) :
- """
- Return filter expression for given attr == value
- """
-
- # preprocess
- like = False
-
- if value.endswith('*') :
- like = value.replace('*', '%')
-
- # filter
- column = self.TABLE.ATTRS[attr].column
-
- if like :
- return (column.like(like))
- else :
- return (column == value)
-
- def _filter (self, attr, values) :
- """
- Apply filters for given attr -> (value, expression)
- """
-
- for value in values :
- value = value.strip()
-
- # ignore empty fields
- if not value :
- continue
-
- # lookup attr-specific filter
- filter = getattr(self, 'filter_{attr}'.format(attr=attr), None)
-
- if filter :
- filter = filter(value)
- else :
- # use generic
- filter = self.filter_attr(attr, value)
-
- log.debug("%s: %s: %s", attr, value, filter)
-
- yield value, filter
-
- def filter (self, query) :
- """
- Apply filters from request.args against given hosts.
-
- Returns (filters, hosts).
- """
-
- # filter?
- filters = {}
-
- for attr in self.TABLE.ATTRS :
- # from request args
- values = self.request.args.getlist(attr)
-
- # lookup attr filters as expressions
- value_filters = list(self._filter(attr, values))
-
- # ignore empty fields
- if not value_filters :
- continue
-
- # filtering values, and filter expressions
- values, expressions = zip(*value_filters)
-
- # apply
- query = query.filter(db.or_(*expressions))
- filters[attr] = values
-
- return filters, query
-
- def filters_title (self) :
- """
- Return a string representing the applied filters.
- """
-
- return ', '.join(value for values in self.filters.itervalues() for value in values)
-
- def title (self) :
- if self.filters :
- return "{title}: {filters}".format(title=self.TABLE.ITEMS, filters=self.filters_title())
- else :
- return self.TABLE.ITEMS
-
- def process (self) :
- """
- Process request args -> self.filters, self.sorts, self.page, self.query
- """
-
- query = self.query()
-
- # filter
- self.filters, query = self.filter(query)
-
- # sort
- # TODO: sort per filter column by default?
- self.sorts, query = self.sort(query)
-
- # page?
- self.page = self.request.args.get('page')
-
- if self.page :
- self.page = int(self.page)
-
- query = query.offset(self.page * self.TABLE.PAGE).limit(self.TABLE.PAGE)
-
- self.query = query
-
- def render_table (self, query, filters=None, sort=None, page=None, hilight=None) :
- """
- Render table
- query - SELECT query for rows
- filters - applied filters
- sort - applied sort
- page - applied page
- hilight - hilight given { attr: value } cells
- """
-
- return self.table.render(query,
- filters = filters,
- sort = sort,
- page = page,
- hilight = hilight,
- )
-
- def render (self) :
- return (
- self.render_table(self.query, filters=self.filters, sort=self.sorts, page=self.page),
- )
--- a/pvl/verkko/utils.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,184 +0,0 @@
-"""
- DHCP... stuff
-"""
-
-import re
-import functools
-from datetime import datetime, timedelta
-
-TIMEDELTA_RE = re.compile(r'(\d+)([a-z]*)', re.IGNORECASE)
-TIMEDELTA_UNITS = {
- 'd': 'days',
- 'h': 'hours',
- 'm': 'minutes',
- 's': 'seconds',
-}
-
-def parse_timedelta (expr) :
- """
- Parse timeout -> timedelta
-
- >>> parse_timedelta('1d')
- datetime.timedelta(1)
- >>> parse_timedelta('1h')
- datetime.timedelta(0, 3600)
- >>> parse_timedelta('15m')
- datetime.timedelta(0, 900)
- >>> parse_timedelta('1d1h1s')
- datetime.timedelta(1, 3601)
- """
-
- what = {}
-
- for (value, unit) in TIMEDELTA_RE.findall(expr) :
- unit = unit.lower()
- value = int(value)
-
- if unit in TIMEDELTA_UNITS :
- what[TIMEDELTA_UNITS[unit]] = value
- else :
- raise ValueError(unit)
-
- return timedelta(**what)
-
-def format_timedelta (td):
- """
- datetime.timedelta -> str
-
- >>> print timedelta_str(timedelta(days=1))
- 1d
- >>> print timedelta_str(timedelta(hours=6))
- 6h
- >>> print timedelta_str(timedelta(days=2, hours=6, seconds=120))
- 2d6h2m
- """
-
- # divmod
- days = td.days
-
- seconds = td.seconds
- minutes, seconds = divmod(seconds, 60)
- hours, minutes = divmod(minutes, 60)
-
- # format
- data = (
- (days, 'd'),
- (hours, 'h'),
- (minutes, 'm'),
- (seconds, 's'),
- )
-
- return ''.join('%d%s' % (count, unit) for count, unit in data if count)
-
-def parse_addr (addr, pad=False) :
- """
- Parse IPv4 addr -> int.
-
- partial - allow partial addrs; right-pad with .0
-
- >>> print "%#010x/%d" % parse_addr('1.2.3.4')
- 0x01020304/32
- >>> print "%#010x/%d" % parse_addr('1.2', pad=True)
- 0x01020000/16
-
- """
-
- # split net
- addr = [int(part) for part in addr.split('.') if part]
-
- addrlen = len(addr) * 8
-
- # fixup base?
- if len(addr) == 4 :
- # fine
- pass
-
- elif len(addr) > 4 :
- raise ValueError("Invalid IPv4 net: {0}".format(addr))
-
- elif len(addr) < 4 and pad :
- # pad
- addr += [0] * (4 - len(addr))
-
- else :
- raise ValueError("Incomplete IPv4 addr: {0}".format(addr))
-
- # pack to int
- return functools.reduce(lambda a, b: a * 256 + b, addr), addrlen
-
-def parse_net (expr) :
- """
- Parse given expr into (base, mask).
-
- >>> print "%#010x/%#010x" % parse_net('1.2.3.4')
- 0x01020304/0xffffffff
- >>> print "%#010x/%#010x" % parse_net('1.2.0.0/16')
- 0x01020000/0xffff0000
- >>> print "%#010x/%#010x" % parse_net('1.2')
- 0x01020000/0xffff0000
- """
-
- if '/' in expr :
- net, masklen = expr.split('/', 1)
-
- masklen = int(masklen)
-
- else :
- net = expr
- masklen = None
-
- base, baselen = parse_addr(net, pad=True)
-
- if not masklen :
- # implicit mask, by leaving off octets in the base
- masklen = baselen
-
- elif masklen > 32 :
- raise ValueError("Invalid IPv4 mask: /{0:d}".format(masklen))
-
- # pack
- mask = (0xffffffff << (32 - masklen)) & 0xffffffff
-
- # verify
- if base & ~mask :
- raise ValueError("Invalid IPv4 net base: {base:x} & {mask:x}".format(base=base, mask=mask))
-
- return base, mask, masklen
-
-def IPv4Address (addr) :
- """
- Parse IPv4 address to int.
- """
-
- addr, len = parse_addr(addr)
-
- return addr
-
-class IPv4Network (object) :
- """
- Parse and match network masks.
-
- XXX: is used as a dict key
- """
-
- def __init__ (self, expr) :
- self.expr = expr
- self.base, self.mask, self.masklen = parse_net(expr)
-
- def __contains__ (self, addr) :
- return (addr & self.mask) == self.base
-
- def __str__ (self) :
- return self.expr
-
- def __repr__ (self) :
- return "IPv4Network(%r)" % (self.expr, )
-
-if __name__ == '__main__' :
- import logging
-
- logging.basicConfig()
-
- import doctest
- doctest.testmod()
-
--- a/pvl/verkko/web.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,39 +0,0 @@
-# encoding: utf-8
-import pvl.web
-from pvl.web import html, urls
-
-import logging; log = logging.getLogger('pvl.verkko.web')
-
-class DatabaseHandler (pvl.web.Handler) :
- """
- Request handler with pvl.verkko.Database session
- """
-
- def __init__ (self, app, request, urls, params) :
- super(DatabaseHandler, self).__init__(app, request, urls, params)
-
- # new ORM session per request
- self.db = app.db.session()
-
- def cleanup (self) :
- """
- After request processing.
- """
-
- # XXX: SQLAlchemy doesn't automatically close these...?
- self.db.close()
-
-class Application (pvl.web.Application) :
- """
- Application with pvl.verkko.Database
- """
-
- def __init__ (self, db, **opts) :
- """
- db - pvl.verkko.Database
- """
-
- super(Application, self).__init__(**opts)
-
- self.db = db
-
--- a/pvl/verkko/wlan.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,75 +0,0 @@
-from pvl.verkko import web, db, table
-from pvl.web import html, urls
-
-import datetime
-import logging; log = logging.getLogger('pvl.verkko.wlan')
-
-class WLANSta (object) :
- """
- A WLAN STA (client) with ap/ssid/sta and starts/ends.
- """
-
- DATE_FMT = '%Y%m%d %H:%M'
- TIME_FMT = '%H:%M:%S'
-
- @classmethod
- def format_datetime (cls, dt) :
- if dt.date() == datetime.date.today() :
- return dt.strftime(cls.TIME_FMT)
- else :
- return dt.strftime(cls.DATE_FMT)
-
- def seen (self) :
- return (
- html.span(title=self.first_seen)(self.format_datetime(self.first_seen)),
- '-',
- html.span(title=self.last_seen)(self.format_datetime(self.last_seen))
- )
-
-db.mapper(WLANSta, db.wlan_sta, properties=dict(
-
-))
-
-class WLANTable (table.Table) :
- """
- <table> of WLAN STA
- """
-
- ITEMS = "WLAN Stations"
- COLUMNS = (
- table.Column('ap', "Access Point", WLANSta.ap,
- rowfilter = True,
- ),
- table.Column('wlan', "SSID", WLANSta.wlan,
- rowfilter = True,
- ),
- table.Column('sta', "MAC", WLANSta.sta,
- rowfilter = True,
- ),
- table.Column('seen', "Seen", WLANSta.last_seen, WLANSta.seen),
- )
-
- # XXX: have to set again
- ATTRS = dict((col.attr, col) for col in COLUMNS)
-
- # default
- SORT = WLANSta.last_seen.desc()
- PAGE = 20
-
-class WLANHandler (table.TableHandler, web.DatabaseHandler) :
- """
- Combined database + <table>
- """
-
- CSS = web.DatabaseHandler.CSS + table.TableHandler.CSS + (
- "/static/wlan.css",
- )
-
- # view
- TABLE = WLANTable
- DB_CLASS = WLANSta
-
-class Application (web.Application) :
- URLS = urls.Map((
- urls.rule('/', WLANHandler),
- ))
--- a/pvl/web/__init__.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-"""
- Werkzeug-based web application.
-"""
-
-# items
-from pvl.web.html import tags as html, html5
-from pvl.web.application import Application, Handler
-
-# werkzeug
-from werkzeug.wrappers import Request, Response
-
--- a/pvl/web/application.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,228 +0,0 @@
-"""
- WSGI Application with pvl.web.urls-mapped Handlers building pvl.web.html.
-"""
-
-import werkzeug
-from werkzeug.wrappers import Request, Response
-from werkzeug.exceptions import HTTPException
-
-import pvl.web.response
-
-import logging; log = logging.getLogger('pvl.web.application')
-
-class Application (object) :
- """
- Main wsgi entry point state.
- """
-
- URLS = None
-
- # TODO: charset
-
- def __init__ (self, urls=None, layout=None) :
- """
- urls - werkzeug.routing.Map -> Handler
- layout - template with {TITLE} and {HEAD} and {BODY}
- """
-
- if not urls :
- urls = self.URLS
-
- if not urls :
- raise ValueError("No URLS/urls=... given")
-
- self.layout = layout
- self.urls = urls
-
- def respond (self, request) :
- """
- Lookup Request -> web.Handler, params
- """
-
- # bind to request
- urls = self.urls.bind_to_environ(request)
-
- # lookup
- handler, params = urls.match()
-
- # handler instance
- handler = handler(self, request, urls, params)
-
- try :
- # apply
- return handler.respond()
-
- finally :
- handler.cleanup()
-
- @Request.application
- def __call__ (self, request) :
- """
- WSGI entry point, werkzeug Request -> Response
- """
-
- try :
- return self.respond(request)
-
- except HTTPException as ex :
- return ex
-
-from pvl.invoke import merge # XXX
-from pvl.web.html import tags as html
-
-class Handler (object) :
- """
- Per-Request controller/view, containing the request context and generating the response.
- """
-
- DOCTYPE = 'html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"'
- HTML_XMLNS = 'http://www.w3.org/1999/xhtml'
- HTML_LANG = None
-
- TITLE = None
- STYLE = None
- SCRIPT = None
- CSS = (
- #"/static/...css",
- )
- JS = (
- #"/static/....js"
- )
-
- STATUS = 200
-
- def __init__ (self, app, request, urls, params) :
- """
- app - wsgi.Application
- request - werkzeug.Request
- urls - werkzeug.routing.Map.bind_to_environ()
- """
-
- self.app = app
- self.request = request
- self.urls = urls
- self.params = params
-
- self.init()
-
- def init (self) :
- """
- Initialize on request.
- """
-
- def url (self, handler=None, **params) :
- """
- Return an URL for given endpoint, with parameters,
- """
-
- if not handler :
- # XXX: just generate a plain-relative '?foo=...' url instead?
- handler = self.__class__
- params = merge(self.params, params)
-
- return self.urls.build(handler, params)
-
- def status (self) :
- """
- Return HTTP status code for response.
- """
-
- return self.STATUS
-
- def title (self) :
- """
- Render site/page title as text.
- """
-
- return self.TITLE
-
- def render (self) :
- """
- Render page content (as <body>...</body>).
- """
-
- raise NotImplementedError()
-
- def render_html (self, body=None, extrahead=None) :
- """
- Render page layout (as <html>).
- """
-
- title = self.title()
- head = html(
- (
- html.link(rel='Stylesheet', type="text/css", href=src) for src in self.CSS
- ),
- (
- html.script(src=src, type='text/javascript', _selfclosing=False) for src in self.JS
- ),
- html.style(type='text/css')(self.STYLE) if self.STYLE else None,
- html.script(type='text/javascript')(self.SCRIPT) if self.SCRIPT else None,
- extrahead,
- )
-
- if body is None :
- body = html(self.render())
-
- if not title :
- raise Exception("%s: no page title!" % (self, ))
-
- if self.app.layout :
- return self.app.layout.format(
- TITLE = unicode(title),
- HEAD = unicode(head),
- BODY = unicode(body),
- )
- else :
- return html.document(html.html(
- html.head(
- html.title(title),
- head,
- ),
- html.body(
- html.h1(title),
- body,
- )
- ), doctype=self.DOCTYPE, html_xmlns=self.HTML_XMLNS, html_lang=self.HTML_LANG)
-
- def response_file (self, file) :
- """
- Wrap a file for returning in a response with direct_passthrough=True.
- """
-
- return werkzeug.wrap_file(self.request.environ, file)
-
- def process (self, **params) :
- """
- Process request args to build internal request state.
- """
-
- pass
-
- def respond (self) :
- """
- Generate a response, or raise an HTTPException
-
- Does an HTML layout'd response per default.
- """
-
- # returning e.g. redirect?
- response = self.process(**self.params)
-
- if response :
- return response
-
- # render as html per default
- # XXX: might also be string?
- text = unicode(self.render_html())
-
- return pvl.web.response.html(text,
- status = self.status(),
- )
-
- def cleanup (self) :
- """
- After request processing. Do not fail :)
- """
-
- pass
--- a/pvl/web/args.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-import codecs
-import werkzeug.serving
-import optparse
-
-import logging; log = logging.getLogger('pvl.web.args')
-
-def parser (parser) :
- """
- Command-line args for pvl.web
- """
-
- parser = optparse.OptionGroup(parser, 'pvl.web')
-
- parser.add_option('--web-layout', metavar='TEMPLATE',
- help="Use template from given file for layout")
-
- parser.add_option('--web-static', metavar='PATH', default='static',
- help="Path to static files")
-
- return parser
-
-def apply (options, application, *args, **opts) :
- """
- Build given pvl.web.Application subclass from options.
- """
-
- if options.web_layout :
- layout = codecs.open(options.web_layout, 'r', 'utf-8').read()
- else :
- layout = None
-
- return application(*args,
- layout = layout,
- **opts
- )
-
-def main (options, wsgi) :
- """
- Run the werkzeug development server.
- """
-
- static_files = { }
-
- if options.web_static :
- static_files['/static'] = options.web_static
-
- werkzeug.serving.run_simple('0.0.0.0', 8080, wsgi,
- #use_reloader = True,
- use_debugger = (options.loglevel == logging.DEBUG),
- static_files = static_files,
- )
-
- return 0
-
--- a/pvl/web/html.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,641 +0,0 @@
-"""
- Generate XHTML output from python code.
-
- >>> from html import tags
- >>> unicode(tags.a(href="http://www.google.com")("Google <this>!"))
- u'<a href="http://www.google.com">\\n\\tGoogle <this>!\\n</a>'
-"""
-
-# XXX: needs some refactoring for Text vs Tag now
-# XXX: not all tags work in self-closing form, e.g. empty html.title() breaks badly
-
-import itertools as itertools
-import types as types
-from xml.sax import saxutils
-
-class Renderable (object) :
- """
- Structured data that's flattened into indented lines of text.
- """
-
- # types of nested items to flatten
- CONTAINER_TYPES = (types.TupleType, types.ListType, types.GeneratorType)
-
- @classmethod
- def process_contents (cls, *args) :
- """
- Yield the HTML tag's contents from the given sequence of positional arguments as a series of flattened
- items, eagerly converting them to unicode.
-
- If no arguments are given, we don't have any children:
-
- >>> bool(list(Tag.process_contents()))
- False
-
- Items that are None will be ignored:
-
- >>> list(Tag.process_contents(None))
- []
-
- Various Python container types are recursively flattened:
-
- >>> list(Tag.process_contents([1, 2]))
- [u'1', u'2']
- >>> list(Tag.process_contents([1], [2]))
- [u'1', u'2']
- >>> list(Tag.process_contents([1, [2]]))
- [u'1', u'2']
- >>> list(Tag.process_contents(n + 1 for n in xrange(2)))
- [u'1', u'2']
- >>> list(Tag.process_contents((1, 2)))
- [u'1', u'2']
- >>> list(Tag.process_contents((1), (2, )))
- [u'1', u'2']
-
- Our own HTML-aware objects are returned as-is:
-
- >>> list(Tag.process_contents(Tag.build('foo')))
- [tag('foo')]
- >>> list(Tag.process_contents(Text(u'bar')))
- [Text(u'bar')]
-
- All other objects are converted to unicode:
-
- >>> list(Tag.process_contents('foo', u'bar', 0.123, False))
- [u'foo', u'bar', u'0.123', u'False']
-
- """
-
- for arg in args :
- if arg is None :
- # skip null: None
- continue
-
- elif isinstance(arg, cls.CONTAINER_TYPES) :
- # flatten nested container: tuple/list/generator
- for node in arg :
- # recurse
- for item in cls.process_contents(node) :
- yield item
-
- elif isinstance(arg, Renderable) :
- # yield item: Renderable
- yield arg
-
- else :
- # as unicode
- yield unicode(arg)
-
-
- def flatten (self) :
- """
- Flatten this object into a series of (identlevel, line) tuples.
- """
-
- raise NotImplementedError()
-
- def iter (self, indent='\t') :
- """
- Yield a series of lines for this render.
- """
-
- for indent_level, line in self.flatten() :
- yield (indent * indent_level) + line
-
- def unicode (self, newline=u'\n', **opts) :
- """
- Render as a single unicode string.
-
- No newline is returned at the end of the string.
-
- >>> Tag.build('a', 'b').unicode(newline='X', indent='Y')
- u'<a>XYbX</a>'
- """
-
- return newline.join(self.iter(**opts))
-
- # required for print
- def str (self, newline='\n', encoding='ascii', **opts) :
- """
- Render as a single string.
- """
-
- # XXX: try and render as non-unicode, i.e. binary data in the tree?
- return newline.join(line.encode(encoding) for line in self.iter(**opts))
-
- # formal interface using defaults
- __iter__ = iter
- __unicode__ = unicode
- __str__ = str
-
-class Text (Renderable) :
- """
- Plain un-structured/un-processed HTML text for output
-
- >>> Text(u'foo')
- Text(u'foo')
- >>> list(Text('<foo>'))
- [u'<foo>']
- >>> list(Text('<foo>', tag('p', 'test')))
- [u'<foo>', u'<p>', u'\\ttest', u'</p>']
- >>> list(tag('a', Text('<foo>')))
- [u'<a>', u'\\t<foo>', u'</a>']
- >>> list(Text(range(2)))
- [u'0', u'1']
-
- """
-
- def __init__ (self, *contents) :
- self.contents = self.process_contents(*contents)
-
- def flatten (self, indent=0) :
- for item in self.contents :
- if isinstance(item, Renderable) :
- # recursively flatten items
- for line_indent, line in item.flatten() :
- # indented
- yield indent + line_indent, line
-
- else :
- # render raw value
- yield indent, unicode(item)
-
- def __repr__ (self) :
- return "Text(%s)" % (', '.join(repr(item) for item in self.contents))
-
-class Tag (Renderable) :
- """
- An immutable HTML tag structure, with the tag's name, attributes and contents.
- """
-
- @classmethod
- def process_attrs (cls, **kwargs) :
- """
- Yield the HTML tag attributes from the given set of keyword arguments as a series of (name, value) tuples.
-
- Keyword-only options (`_key=value`) are filtered out:
-
- >>> dict(Tag.process_attrs(_opt=True))
- {}
-
- Attributes with a value of None/False are filtered out:
-
- >>> dict(Tag.process_attrs(foo=None, bar=False))
- {}
-
- A value given as True is returned as the key's value:
-
- >>> dict(Tag.process_attrs(quux=True))
- {'quux': u'quux'}
-
- A (single) trailing underscore in the attribute name is removed:
-
- >>> dict(Tag.process_attrs(class_='foo'))
- {'class': u'foo'}
- >>> dict(Tag.process_attrs(data__='foo'))
- {'data_': u'foo'}
- """
-
- for key, value in kwargs.iteritems() :
- # keyword arguments are always pure strings
- assert type(key) is str
-
- if value is None or value is False:
- # omit
- continue
-
- if key.startswith('_') :
- # option
- continue
-
- if key.endswith('_') :
- # strip underscore
- key = key[:-1]
-
- if '_' in key :
- key = key.replace('_', '-')
-
- if value is True :
- # flag attr
- value = key
-
- yield key, unicode(value)
-
- @classmethod
- def process_opts (cls, **kwargs) :
- """
- Return a series of of the keyword-only _options, extracted from the given dict of keyword arguments, as
- (k, v) tuples.
-
- >>> Tag.process_opts(foo='bar', _bar=False)
- (('bar', False),)
- """
-
- return tuple((k.lstrip('_'), v) for k, v in kwargs.iteritems() if k.startswith('_'))
-
- @classmethod
- def build (cls, _name, *args, **kwargs) :
- """
- Factory function for constructing Tags by directly passing in contents/attributes/options as Python function
- arguments/keyword arguments.
-
- The first positional argument is the tag's name:
-
- >>> Tag.build('foo')
- tag('foo')
-
- Further positional arguments are the tag's contents:
-
- >>> Tag.build('foo', 'quux', 'bar')
- tag('foo', u'quux', u'bar')
-
- All the rules used by process_contents() are available:
-
- >>> Tag.build('foo', [1, None], None, (n for n in xrange(2)))
- tag('foo', u'1', u'0', u'1')
-
- The special-case for a genexp as the only argument works:
-
- >>> f = lambda *args: Tag.build('foo', *args)
- >>> f('hi' for n in xrange(2))
- tag('foo', u'hi', u'hi')
-
- Attributes are passed as keyword arguments, with or without contents:
-
- >>> Tag.build('foo', id=1)
- tag('foo', id=u'1')
- >>> Tag.build('foo', 'quux', bar=5)
- tag('foo', u'quux', bar=u'5')
- >>> Tag.build('foo', class_='ten')
- tag('foo', class=u'ten')
-
- The attribute names don't conflict with positional argument names:
-
- >>> Tag.build('bar', name='foo')
- tag('bar', name=u'foo')
-
- Options are handled as the 'real' keyword arguments:
-
- >>> print Tag.build('foo', _selfclosing=False)
- <foo></foo>
- >>> print Tag.build('foo', _foo='bar')
- Traceback (most recent call last):
- ...
- TypeError: __init__() got an unexpected keyword argument 'foo'
- """
-
- # pre-process incoming user values
- contents = list(cls.process_contents(*args))
- attrs = dict(cls.process_attrs(**kwargs))
-
- # XXX: use Python 2.6 keyword-only arguments instead?
- options = dict(cls.process_opts(**kwargs))
-
- return cls(_name, contents, attrs, **options)
-
- def __init__ (self, name, contents=None, attrs=None, selfclosing=None, whitespace_sensitive=None, escape=True) :
- """
- Initialize internal Tag state with the given tag identifier, flattened list of content items, dict of
- attributes and dict of options.
-
- selfclosing - set to False to render empty tags as <foo></foo> instead of <foo />
- (for XHTML -> HTML compatibility)
-
- whitespace_sensitive - do not indent tag content onto separate rows, render the full tag as a single
- row
-
- escape - html-escape non-Renderable's (text)
-
- Use the build() factory function to build Tag objects using Python's function call argument semantics.
-
- The tag name is used a pure string identifier:
-
- >>> Tag(u'foo', [], {})
- tag('foo')
- >>> Tag(u'\\xE4', [], {})
- Traceback (most recent call last):
- ...
- UnicodeEncodeError: 'ascii' codec can't encode character u'\\xe4' in position 0: ordinal not in range(128)
-
- Contents have their order preserved:
-
- >>> Tag('foo', [1, 2], {})
- tag('foo', 1, 2)
- >>> Tag('foo', [2, 1], {})
- tag('foo', 2, 1)
-
- Attributes can be given:
-
- >>> Tag('foo', [], dict(foo='bar'))
- tag('foo', foo='bar')
-
- Options can be given:
-
- >>> print Tag('foo', [], {}, selfclosing=False)
- <foo></foo>
- """
-
- self.name = str(name)
- self.contents = contents or []
- self.attrs = attrs or {}
-
- # options
- self.selfclosing = selfclosing
- self.whitespace_sensitive = whitespace_sensitive
- self.escape = escape
-
- def __call__ (self, *args, **kwargs) :
- """
- Return a new Tag as a copy of this tag, but with the given additional attributes/contents.
-
- The same rules for function positional/keyword arguments apply as for build()
-
- >>> Tag.build('foo')('bar')
- tag('foo', u'bar')
- >>> Tag.build('a', href='index.html')("Home")
- tag('a', u'Home', href=u'index.html')
-
- New contents and attributes can be given freely, using the same rules as for Tag.build:
-
- >>> Tag.build('bar', None)(5, foo=None, class_='bar')
- tag('bar', u'5', class=u'bar')
-
- Tag contents accumulate in order:
-
- >>> Tag.build('a')('b', ['c'])('d')
- tag('a', u'b', u'c', u'd')
-
- Each Tag is immutable, so the called Tag isn't changed, but rather a copy is returned:
-
- >>> t1 = Tag.build('a'); t2 = t1('b'); t1
- tag('a')
-
- Attribute values are replaced:
-
- >>> Tag.build('foo', a=2)(a=3)
- tag('foo', a=u'3')
-
- Options are also supported:
-
- >>> list(Tag.build('foo')(bar='quux', _selfclosing=False))
- [u'<foo bar="quux"></foo>']
- """
-
- # accumulate contents
- contents = self.contents + list(self.process_contents(*args))
-
- # merge attrs
- attrs = dict(self.attrs)
- attrs.update(self.process_attrs(**kwargs))
-
- # options
- opts = dict(
- selfclosing = self.selfclosing,
- whitespace_sensitive = self.whitespace_sensitive,
- )
- opts.update(self.process_opts(**kwargs))
-
- # build updated tag
- return Tag(self.name, contents, attrs, **opts)
-
- def render_attrs (self) :
- """
- Return the HTML attributes string
-
- >>> Tag.build('x', foo=5, bar='<', quux=None).render_attrs()
- u'foo="5" bar="<"'
- >>> Tag.build('x', foo='a"b').render_attrs()
- u'foo=\\'a"b\\''
- """
-
- return " ".join(
- (
- u'%s=%s' % (name, saxutils.quoteattr(value))
- ) for name, value in self.attrs.iteritems()
- )
-
- def flatten_items (self, indent=1) :
- """
- Flatten our content into a series of indented lines.
-
- >>> list(Tag.build('tag', 5).flatten_items())
- [(1, u'5')]
- >>> list(Tag.build('tag', 'line1', 'line2').flatten_items())
- [(1, u'line1'), (1, u'line2')]
-
- Nested :
- >>> list(Tag.build('tag', 'a', Tag.build('b', 'bb'), 'c').flatten_items())
- [(1, u'a'), (1, u'<b>'), (2, u'bb'), (1, u'</b>'), (1, u'c')]
- >>> list(Tag.build('tag', Tag.build('hr'), Tag.build('foo')('bar')).flatten_items())
- [(1, u'<hr />'), (1, u'<foo>'), (2, u'bar'), (1, u'</foo>')]
- """
-
- for item in self.contents :
- if isinstance(item, Renderable) :
- # recursively flatten items
- for line_indent, line in item.flatten() :
- # indented
- yield indent + line_indent, line
-
- elif self.escape :
- # render HTML-escaped raw value
- # escape raw values
- yield indent, saxutils.escape(item)
-
- else :
- # render raw value
- yield indent, unicode(item)
-
- def flatten (self) :
- """
- Render the tag and all content as a flattened series of indented lines.
-
- Empty tags collapse per default:
-
- >>> list(Tag.build('foo').flatten())
- [(0, u'<foo />')]
- >>> list(Tag.build('bar', id=5).flatten())
- [(0, u'<bar id="5" />')]
-
- Values are indented inside the start tag:
-
- >>> list(Tag.build('foo', 'bar', a=5).flatten())
- [(0, u'<foo a="5">'), (1, u'bar'), (0, u'</foo>')]
-
- Nested tags are further indented:
-
- >>> list(Tag.build('1', '1.1', Tag.build('1.2', '1.2.1'), '1.3', a=5).flatten())
- [(0, u'<1 a="5">'), (1, u'1.1'), (1, u'<1.2>'), (2, u'1.2.1'), (1, u'</1.2>'), (1, u'1.3'), (0, u'</1>')]
-
- Empty tags are rendered with a separate closing tag on the same line, if desired:
-
- >>> list(Tag.build('foo', _selfclosing=False).flatten())
- [(0, u'<foo></foo>')]
- >>> list(Tag.build('foo', src='asdf', _selfclosing=False).flatten())
- [(0, u'<foo src="asdf"></foo>')]
-
- Tags that are declared as whitespace-sensitive are collapsed onto the same line:
-
- >>> list(Tag.build('foo', _whitespace_sensitive=True).flatten())
- [(0, u'<foo />')]
- >>> list(Tag.build('foo', _whitespace_sensitive=True, _selfclosing=False).flatten())
- [(0, u'<foo></foo>')]
- >>> list(Tag.build('foo', 'bar', _whitespace_sensitive=True).flatten())
- [(0, u'<foo>bar</foo>')]
- >>> list(Tag.build('foo', 'bar\\nasdf\\tx', _whitespace_sensitive=True).flatten())
- [(0, u'<foo>bar\\nasdf\\tx</foo>')]
- >>> list(Tag.build('foo', 'bar', Tag.build('quux', 'asdf'), 'asdf', _whitespace_sensitive=True).flatten())
- [(0, u'<foo>bar<quux>asdf</quux>asdf</foo>')]
-
- Embedded HTML given as string values is escaped:
-
- >>> list(Tag.build('foo', '<asdf>'))
- [u'<foo>', u'\\t<asdf>', u'</foo>']
-
- Embedded quotes in attribute values are esacaped:
-
- >>> list(Tag.build('foo', style='ok;" onload="...'))
- [u'<foo style=\\'ok;" onload="...\\' />']
- >>> list(Tag.build('foo', style='ok;\\'" onload=..."\\''))
- [u'<foo style="ok;\\'" onload=..."\\'" />']
- """
-
- # optional attr spec
- if self.attrs :
- attrs = " " + self.render_attrs()
-
- else :
- attrs = ""
-
- if not self.contents and self.selfclosing is False :
- # empty tag, but don't use the self-closing syntax..
- yield 0, u"<%s%s></%s>" % (self.name, attrs, self.name)
-
- elif not self.contents :
- # self-closing xml tag
- # do note that this is invalid HTML, and the space before the / is relevant for parsing it as HTML
- yield 0, u"<%s%s />" % (self.name, attrs)
-
- elif self.whitespace_sensitive :
- # join together each line for each child, discarding the indent
- content = u''.join(line for indent, line in self.flatten_items())
-
- # render full tag on a single line
- yield 0, u"<%s%s>%s</%s>" % (self.name, attrs, content, self.name)
-
- else :
- # start tag
- yield 0, u"<%s%s>" % (self.name, attrs)
-
- # contents, indented one level below the start tag
- for indent, line in self.flatten_items(indent=1) :
- yield indent, line
-
- # close tag
- yield 0, u"</%s>" % (self.name, )
-
- def __repr__ (self) :
- return 'tag(%s)' % ', '.join(
- [
- repr(self.name)
- ] + [
- repr(c) for c in self.contents
- ] + [
- '%s=%r' % (name, value) for name, value in self.attrs.iteritems()
- ]
- )
-
-# factory function for Tag
-tag = Tag.build
-
-
-class Document (Renderable) :
- """
- A full XHTML 1.0 document with optional XML header, doctype, html[@xmlns].
-
- >>> list(Document(tags.html('...')))
- [u'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', u'<html xmlns="http://www.w3.org/1999/xhtml">', u'\\t...', u'</html>']
- """
-
- DOCTYPE = 'html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"'
- HTML_XMLNS = 'http://www.w3.org/1999/xhtml'
- HTML_LANG = None
-
- def __init__ (self, root,
- doctype=DOCTYPE,
- html_xmlns=HTML_XMLNS,
- html_lang=HTML_LANG,
- xml_version=None, xml_encoding=None,
- ) :
- # add xmlns attr to root node
- self.root = root(xmlns=html_xmlns, lang=html_lang)
-
- # store
- self.doctype = doctype
- self.xml_declaration = {}
-
- if xml_version :
- self.xml_declaration['version'] = xml_version
-
- if xml_encoding :
- self.xml_declaration['encoding'] = xml_encoding
-
- def flatten (self) :
- """
- Return the header lines along with the normally formatted <html> tag
- """
-
- if self.xml_declaration :
- yield 0, u'<?xml %s ?>' % (' '.join('%s="%s"' % kv for kv in self.xml_declaration.iteritems()))
-
- if self.doctype :
- yield 0, u'<!DOCTYPE %s>' % (self.doctype)
-
- # <html>
- for indent, line in self.root.flatten() :
- yield indent, line
-
-class TagFactory (object) :
- """
- Build Tags with names give as attribute names
-
- >>> list(TagFactory().a(href='#')('Yay'))
- [u'<a href="#">', u'\\tYay', u'</a>']
-
- >>> list(TagFactory()("><"))
- [u'><']
- """
-
- # full XHTML document
- document = Document
-
- def __getattr__ (self, name) :
- """
- Get a Tag object with the given name, but no contents
-
- >>> TagFactory().a
- tag('a')
- """
-
- return Tag(name)
-
- def __call__ (self, *values) :
- """
- Raw HTML.
- """
-
- return Text(*values)
-
-class HTML5TagFactory (TagFactory) :
- span = Tag('span', selfclosing=False)
-
-# static instance
-tags = TagFactory()
-html5 = HTML5TagFactory()
-
-# testing
-if __name__ == '__main__' :
- import doctest
-
- doctest.testmod()
-
--- a/pvl/web/response.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-from werkzeug.wrappers import Response
-from werkzeug.exceptions import (
- HTTPException,
- BadRequest, # 400
- NotFound, # 404
-)
-from werkzeug.utils import redirect
-
-
-import json as _json
-
-def html (tag, **opts) :
- """
- Return text/html response for given pvl.web.html
- """
-
- return Response(unicode(tag), mimetype='text/html', **opts)
-
-def json (data, **opts) :
- """
- Return text/json response for given object.
- """
-
- return Response(_json.dumps(data), mimetype='text/json', **opts)
-
-def image (file, type='png') :
- """
- Return image/{type} response for given file-like object.
- """
-
- # respond with file wrapper
- return Response(file, mimetype='image/{type}'.format(type=type), direct_passthrough=True)
-
--- a/pvl/web/urls.py Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,8 +0,0 @@
-from werkzeug.routing import Map, Rule
-
-# url_join
-from os.path import join
-
-def rule (string, endpoint, **opts) :
- return Rule(string, endpoint=endpoint, defaults=opts)
-
--- a/setup.py Tue Feb 24 12:47:09 2015 +0200
+++ b/setup.py Tue Feb 24 14:50:31 2015 +0200
@@ -14,16 +14,16 @@
# TODO: fix to use PEP-396 once available:
# https://www.python.org/dev/peps/pep-0396/#classic-distutils
-for line in open('pvl/verkko/__init__.py'):
+for line in open('pvl/hosts.py'):
if '__version__' in line:
_, line_version = line.split('=')
__version__ = line_version.strip().strip("''")
setup(
- name = 'pvl-verkko',
+ name = 'pvl-hosts',
version = __version__,
- description = "verkko.paivola.fi WSGI",
- url = 'http://verkko.paivola.fi/hg/pvl-verkko',
+ description = "DNS/DHCP hosts management",
+ url = 'http://verkko.paivola.fi/hg/pvl-hosts',
author = "Tero Marttila",
author_email = "terom@paivola.fi",
@@ -34,39 +34,37 @@
# pvl.invoke
'pvl-common',
- # pvl.hosts-import
- 'pvl-ldap',
-
- # pvl.verkko.db ->
- 'sqlalchemy',
-
# pvl.hosts
+ # TODO: replace with ipaddress for py3 forward-compat
'ipaddr',
],
+ extras_require = {
+ # pvl.hosts-import
+ 'import': [
+ 'pvl-ldap',
+ ],
+
+ # pvl.dhcp-leases/syslog
+ 'db': [
+ 'sqlalchemy',
+ ],
+ },
- # lib
+ # pvl/
namespace_packages = [ 'pvl' ],
py_modules = [
'pvl.hosts',
],
packages = [
- 'pvl',
- 'pvl.web',
'pvl.dhcp',
'pvl.dns',
- 'pvl.rrd',
- 'pvl.verkko',
],
- # bin
- scripts = globs('bin/pvl.*-*'),
-
- # etc, static
- data_files = [
- ( 'etc/pvl/verkko', [ ] ),
- ( 'share/pvl/verkko/static/dhcp', globs('static/dhcp/*.css', 'static/dhcp/*.js')),
- ( 'share/pvl/verkko/static/rrd', globs('static/rrd/*.css', 'static/rrd/*.js')),
- ( 'share/pvl/verkko/static', globs('static/*.css')),
- ],
+ # bin/
+ scripts = globs(
+ 'bin/pvl.dhcp-*',
+ 'bin/pvl.dns-*',
+ 'bin/pvl.hosts-*',
+ ),
)
--- a/static/dhcp/forms.css Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,144 +0,0 @@
-/*
- * Form layout
- *
- * Inspiration taken from http://articles.sitepoint.com/article/fancy-form-design-css
- * Since turned into a book? :)
- */
-
-/* General form errors, and field-specific error lists will display in red */
-form ul.errors,
-form ul.errors a
-{
- list-style-type: disc;
- color: #C00;
-
- margin: 0em 1em 1em;
-}
-
-fieldset
-{
- /* A fieldset will not be completely full-width, and will be vertically separated from adjacent fieldsets*/
- margin: 1em 20% 1em 1em;
-
- /* A fieldset shall not have any padding of its own, as we position the elements iniside it */
- padding: 0em;
-
- border: thin solid #aaa;
-}
-
-/* A fieldset's legend will look similar to a h2 element, except included as part of the frame */
-fieldset legend
-{
- width: 100%;
-
- font-size: large;
- font-weight: bold;
-
- margin: 0em 1em;
- padding: 0.5em;
-
- background-color: #e5e5e5;
-
- border: 1px dashed #c5c5c5;
-}
-
-/* A fieldset will be internally structured using a list, that is not itself visible */
-fieldset ul
-{
- list-style-type: none;
-
- margin: 0px;
-
- /* Recreate padding removed from fieldset */
- padding: 0em 1em;
-}
-
-/* Fields inside a fieldset will be vertically separated */
-fieldset ul > li
-{
- padding: 0.5em 1em;
-
- border-top: 1px solid #c5c5c5;
-}
-
-fieldset ul > li:first-child
-{
- border-top: none;
-}
-
-/* The field identifier displays above the field itself, and is visually weighted, but smaller than the fieldset legend */
-fieldset label,
-fieldset h3 /* used in place of labels for non-input fields... */
-{
- display: block;
-
- margin: 0.2em 0em;
-
- font-weight: bold;
- font-size: small;
-}
-
-/* A <strong> inside the label is an error message */
-fieldset label strong
-{
- color: #C00;
-
- text-transform: upppercase;
-}
-
-/* The field element are consistently aligned */
-form input,
-form textarea
-{
- width: 40%;
-
- border: thin solid #444;
- padding: 4px;
-}
-
-/* A multi-line text edit widget is wide enough for lots of text */
-form textarea
-{
- width: 80%;
-}
-
-/* A field that failed validation is highlighted */
-form .failed input,
-form .failed textarea
-{
- border: thin solid red;
-}
-
-form select,
-form input[type=submit]
-{
- width: 30%;
-
- border: auto;
-}
-
-form input[type=reset]
-{
- width: auto;
-
- font-size: x-small;
-
- float: right;
-}
-
-/* Field's descriptive text */
-fieldset p
-{
- margin: 0.8em;
-
- font-style: italic;
- font-size: x-small;
-}
-
-/* Static fields */
-fieldset div.value
-{
- /* Value is visually indented */
- margin-left: 2em;
-}
-
--- a/static/dhcp/hosts.css Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,77 +0,0 @@
-/*
- * General
- */
-a
-{
- color: inherit;
-}
-
-/*
- * Host details
- */
-div.info {
- width: 80%;
-
- border: 1px solid #aaa;
-
- padding: 1em;
- margin: 1em;
-}
-
-dl {
- width: 80%;
-
- margin: 1em;
-}
-
-dl dt {
- display: block;
- clear: both;
-
- font-weight: bold;
-
- margin-top: 0.5em;
-}
-
-dl dd {
-
-}
-
-/*
- * Hosts
- */
-.id
-{
- font-weight: bold;
- text-align: right;
-}
-
-.ip,
-.mac
-{
- font-family: monospace;
-}
-
-td.name
-{
- /* Wrap */
- white-space: normal;
-}
-
-
-.seen
-{
-}
-
-.dhcp-search { background-color: #444488; }
-.dhcp-ack { background-color: #448844; }
-.dhcp-nak { background-color: #884444; }
-.dhcp-release { background-color: #335533; }
-.dhcp-error { background-color: #ff4444; }
-
-/*
- * Leases
- */
-.ends.active {
- font-weight: bold;
-}
--- a/static/dhcp/hosts.js Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,114 +0,0 @@
-/* http://fgnass.github.com/spin.js/ */
-$.fn.spin = function(opts) {
- this.each(function() {
- var $this = $(this),
- data = $this.data();
-
- if (data.spinner) {
- data.spinner.stop();
- delete data.spinner;
- }
- if (opts !== false) {
- data.spinner = new Spinner($.extend({color: $this.css('color')}, opts)).spin(this);
- }
- });
- return this;
-};
-
-$.fn.disabled = function (disabled) {
- if (disabled)
- this.attr('disabled', 'disabled');
- else
- this.removeAttr('disabled');
-
- return this;
-};
-
-var Timer = function (interval, cb) {
- var handle = null;
-
- this.enable = function () {
- if (handle) return;
-
- handle = window.setInterval(cb, interval);
- };
-
- this.disable = function () {
- if (!handle) return;
-
- window.clearInterval(handle);
- handle = null;
- };
-
- return this;
-};
-
-/*
- * Initialize the realtime host table.
- *
- * table - Table to manipulate
- * url - base URL for ?t=...
- * filters - { attr: value } for ?t=...
- * t - ?t=... to poll for
- *
- * $(document).ready(HostsRealtime(Table(...), ...));
- *
- */
-function HostsRealtime (table, params) {
- var t = params.t;
- var refreshTimer = Timer(2 * 1000, refresh);
-
- // XXX: refresh > interval?
- function refresh () {
- console.log("refresh: " + t);
-
- var refresh = $('#refresh').disabled(true);
- var spinner = $('#wrapper').spin();
-
- var url = params.url;
- var query = $.param($.extend(params.filters, {t: t }), true); // using traditional encoding for multi-values
-
- $.getJSON(url, query, function (data) {
- var t1 = data.t;
- var hosts = data.hosts;
-
- console.log("refresh: " + t + " -> " + t1 + ": " + hosts.length);
-
- $.each(hosts, function (i, host) {
- table.update(host);
- });
-
- // update
- t = t1;
-
- }).error(function () {
- alert("Error :(");
-
- // disable auto-refresh
- refreshTimer.disable();
-
- }).complete(function () {
- spinner.spin(false);
- refresh.disabled(false);
- });
- }
-
- return function () {
- // init
- $("#refresh").click(function () {
- // in case diabled on error
- refreshTimer.enable();
- refresh();
- $("#pause").disabled(false);
- });
-
- $("#pause").click(function () {
- console.log("pause");
- refreshTimer.disable();
- $("#pause").disabled(true);
- });
-
- // start auto-refresh
- refreshTimer.enable();
- }
-}
--- a/static/dhcp/spin.js Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,320 +0,0 @@
-//fgnass.github.com/spin.js#v1.2.7
-!function(window, document, undefined) {
-
- /**
- * Copyright (c) 2011 Felix Gnass [fgnass at neteye dot de]
- * Licensed under the MIT license
- */
-
- var prefixes = ['webkit', 'Moz', 'ms', 'O'] /* Vendor prefixes */
- , animations = {} /* Animation rules keyed by their name */
- , useCssAnimations
-
- /**
- * Utility function to create elements. If no tag name is given,
- * a DIV is created. Optionally properties can be passed.
- */
- function createEl(tag, prop) {
- var el = document.createElement(tag || 'div')
- , n
-
- for(n in prop) el[n] = prop[n]
- return el
- }
-
- /**
- * Appends children and returns the parent.
- */
- function ins(parent /* child1, child2, ...*/) {
- for (var i=1, n=arguments.length; i<n; i++)
- parent.appendChild(arguments[i])
-
- return parent
- }
-
- /**
- * Insert a new stylesheet to hold the @keyframe or VML rules.
- */
- var sheet = function() {
- var el = createEl('style', {type : 'text/css'})
- ins(document.getElementsByTagName('head')[0], el)
- return el.sheet || el.styleSheet
- }()
-
- /**
- * Creates an opacity keyframe animation rule and returns its name.
- * Since most mobile Webkits have timing issues with animation-delay,
- * we create separate rules for each line/segment.
- */
- function addAnimation(alpha, trail, i, lines) {
- var name = ['opacity', trail, ~~(alpha*100), i, lines].join('-')
- , start = 0.01 + i/lines*100
- , z = Math.max(1 - (1-alpha) / trail * (100-start), alpha)
- , prefix = useCssAnimations.substring(0, useCssAnimations.indexOf('Animation')).toLowerCase()
- , pre = prefix && '-'+prefix+'-' || ''
-
- if (!animations[name]) {
- sheet.insertRule(
- '@' + pre + 'keyframes ' + name + '{' +
- '0%{opacity:' + z + '}' +
- start + '%{opacity:' + alpha + '}' +
- (start+0.01) + '%{opacity:1}' +
- (start+trail) % 100 + '%{opacity:' + alpha + '}' +
- '100%{opacity:' + z + '}' +
- '}', sheet.cssRules.length)
-
- animations[name] = 1
- }
- return name
- }
-
- /**
- * Tries various vendor prefixes and returns the first supported property.
- **/
- function vendor(el, prop) {
- var s = el.style
- , pp
- , i
-
- if(s[prop] !== undefined) return prop
- prop = prop.charAt(0).toUpperCase() + prop.slice(1)
- for(i=0; i<prefixes.length; i++) {
- pp = prefixes[i]+prop
- if(s[pp] !== undefined) return pp
- }
- }
-
- /**
- * Sets multiple style properties at once.
- */
- function css(el, prop) {
- for (var n in prop)
- el.style[vendor(el, n)||n] = prop[n]
-
- return el
- }
-
- /**
- * Fills in default values.
- */
- function merge(obj) {
- for (var i=1; i < arguments.length; i++) {
- var def = arguments[i]
- for (var n in def)
- if (obj[n] === undefined) obj[n] = def[n]
- }
- return obj
- }
-
- /**
- * Returns the absolute page-offset of the given element.
- */
- function pos(el) {
- var o = { x:el.offsetLeft, y:el.offsetTop }
- while((el = el.offsetParent))
- o.x+=el.offsetLeft, o.y+=el.offsetTop
-
- return o
- }
-
- var defaults = {
- lines: 12, // The number of lines to draw
- length: 7, // The length of each line
- width: 5, // The line thickness
- radius: 10, // The radius of the inner circle
- rotate: 0, // Rotation offset
- corners: 1, // Roundness (0..1)
- color: '#000', // #rgb or #rrggbb
- speed: 1, // Rounds per second
- trail: 100, // Afterglow percentage
- opacity: 1/4, // Opacity of the lines
- fps: 20, // Frames per second when using setTimeout()
- zIndex: 2e9, // Use a high z-index by default
- className: 'spinner', // CSS class to assign to the element
- top: 'auto', // center vertically
- left: 'auto', // center horizontally
- position: 'relative' // element position
- }
-
- /** The constructor */
- var Spinner = function Spinner(o) {
- if (!this.spin) return new Spinner(o)
- this.opts = merge(o || {}, Spinner.defaults, defaults)
- }
-
- Spinner.defaults = {}
-
- merge(Spinner.prototype, {
- spin: function(target) {
- this.stop()
- var self = this
- , o = self.opts
- , el = self.el = css(createEl(0, {className: o.className}), {position: o.position, width: 0, zIndex: o.zIndex})
- , mid = o.radius+o.length+o.width
- , ep // element position
- , tp // target position
-
- if (target) {
- target.insertBefore(el, target.firstChild||null)
- tp = pos(target)
- ep = pos(el)
- css(el, {
- left: (o.left == 'auto' ? tp.x-ep.x + (target.offsetWidth >> 1) : parseInt(o.left, 10) + mid) + 'px',
- top: (o.top == 'auto' ? tp.y-ep.y + (target.offsetHeight >> 1) : parseInt(o.top, 10) + mid) + 'px'
- })
- }
-
- el.setAttribute('aria-role', 'progressbar')
- self.lines(el, self.opts)
-
- if (!useCssAnimations) {
- // No CSS animation support, use setTimeout() instead
- var i = 0
- , fps = o.fps
- , f = fps/o.speed
- , ostep = (1-o.opacity) / (f*o.trail / 100)
- , astep = f/o.lines
-
- ;(function anim() {
- i++;
- for (var s=o.lines; s; s--) {
- var alpha = Math.max(1-(i+s*astep)%f * ostep, o.opacity)
- self.opacity(el, o.lines-s, alpha, o)
- }
- self.timeout = self.el && setTimeout(anim, ~~(1000/fps))
- })()
- }
- return self
- },
-
- stop: function() {
- var el = this.el
- if (el) {
- clearTimeout(this.timeout)
- if (el.parentNode) el.parentNode.removeChild(el)
- this.el = undefined
- }
- return this
- },
-
- lines: function(el, o) {
- var i = 0
- , seg
-
- function fill(color, shadow) {
- return css(createEl(), {
- position: 'absolute',
- width: (o.length+o.width) + 'px',
- height: o.width + 'px',
- background: color,
- boxShadow: shadow,
- transformOrigin: 'left',
- transform: 'rotate(' + ~~(360/o.lines*i+o.rotate) + 'deg) translate(' + o.radius+'px' +',0)',
- borderRadius: (o.corners * o.width>>1) + 'px'
- })
- }
-
- for (; i < o.lines; i++) {
- seg = css(createEl(), {
- position: 'absolute',
- top: 1+~(o.width/2) + 'px',
- transform: o.hwaccel ? 'translate3d(0,0,0)' : '',
- opacity: o.opacity,
- animation: useCssAnimations && addAnimation(o.opacity, o.trail, i, o.lines) + ' ' + 1/o.speed + 's linear infinite'
- })
-
- if (o.shadow) ins(seg, css(fill('#000', '0 0 4px ' + '#000'), {top: 2+'px'}))
-
- ins(el, ins(seg, fill(o.color, '0 0 1px rgba(0,0,0,.1)')))
- }
- return el
- },
-
- opacity: function(el, i, val) {
- if (i < el.childNodes.length) el.childNodes[i].style.opacity = val
- }
-
- })
-
- /////////////////////////////////////////////////////////////////////////
- // VML rendering for IE
- /////////////////////////////////////////////////////////////////////////
-
- /**
- * Check and init VML support
- */
- ;(function() {
-
- function vml(tag, attr) {
- return createEl('<' + tag + ' xmlns="urn:schemas-microsoft.com:vml" class="spin-vml">', attr)
- }
-
- var s = css(createEl('group'), {behavior: 'url(#default#VML)'})
-
- if (!vendor(s, 'transform') && s.adj) {
-
- // VML support detected. Insert CSS rule ...
- sheet.addRule('.spin-vml', 'behavior:url(#default#VML)')
-
- Spinner.prototype.lines = function(el, o) {
- var r = o.length+o.width
- , s = 2*r
-
- function grp() {
- return css(
- vml('group', {
- coordsize: s + ' ' + s,
- coordorigin: -r + ' ' + -r
- }),
- { width: s, height: s }
- )
- }
-
- var margin = -(o.width+o.length)*2 + 'px'
- , g = css(grp(), {position: 'absolute', top: margin, left: margin})
- , i
-
- function seg(i, dx, filter) {
- ins(g,
- ins(css(grp(), {rotation: 360 / o.lines * i + 'deg', left: ~~dx}),
- ins(css(vml('roundrect', {arcsize: o.corners}), {
- width: r,
- height: o.width,
- left: o.radius,
- top: -o.width>>1,
- filter: filter
- }),
- vml('fill', {color: o.color, opacity: o.opacity}),
- vml('stroke', {opacity: 0}) // transparent stroke to fix color bleeding upon opacity change
- )
- )
- )
- }
-
- if (o.shadow)
- for (i = 1; i <= o.lines; i++)
- seg(i, -2, 'progid:DXImageTransform.Microsoft.Blur(pixelradius=2,makeshadow=1,shadowopacity=.3)')
-
- for (i = 1; i <= o.lines; i++) seg(i)
- return ins(el, g)
- }
-
- Spinner.prototype.opacity = function(el, i, val, o) {
- var c = el.firstChild
- o = o.shadow && o.lines || 0
- if (c && i+o < c.childNodes.length) {
- c = c.childNodes[i+o]; c = c && c.firstChild; c = c && c.firstChild
- if (c) c.opacity = val
- }
- }
- }
- else
- useCssAnimations = vendor(s, 'animation')
- })()
-
- if (typeof define == 'function' && define.amd)
- define(function() { return Spinner })
- else
- window.Spinner = Spinner
-
-}(window, document);
--- a/static/dhcp/table.css Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,130 +0,0 @@
-/*
- * Tables
- */
-table
-{
- width: 80%;
-
- margin: auto;
- padding: 0px;
-
- border-collapse: collapse;
- border: 1px solid #aaa;
-
- font-size: small;
-}
-
-/* A caption looks similar to a h2 */
-table caption
-{
- font-size: large;
- font-weight: bold;
-
- padding: 0.5em;
- margin: 0em 0em 0.5em;
-
- background-color: #e5e5e5;
- border: 1px dashed #c5c5c5;
-}
-
-/* Table header */
-thead tr
-{
- background-color: #d0d0d0;
-}
-
-thead
-{
- border-bottom: 2px solid #aaa;
-}
-
-/* Filter */
-tr.filter
-{
- height: 1em;
-}
-
-tr.filter td
-{
- padding: 0em 1em;
-}
-
-tr.filter td input
-{
- display: block;
- width: 100%;
-
- border: 0px;
- padding: 0em;
-
- background-color: #d0d0d0;
-
- font-family: inherit;
- font-size: inherit;
-}
-
-tr.filter td input:hover
-{
- background-color: #e0e0e0;
-}
-
-tr.filter td input:focus
-{
- background-color: #ffffff;
-}
-
-/* Rows */
-tr
-{
- background-color: #e0e0e0;
-}
-
-tr.alternate
-{
- background-color: #c8c8c8;
-}
-
-/* Link to tr with URL #foo fragment */
-tr:target
-{
- background-color: #F5F3B8;
-}
-
-/* Cells */
-td, th
-{
- border: thin solid #ffffff;
-
- padding: 0.25em 1em;
-
- /* Do not normally wrap */
- white-space: nowrap;
-}
-
-thead a,
-tbody a
-{
- display: block;
-}
-
-th
-{
- text-align: center;
-}
-
-td.hilight
-{
- font-weight: bold;
-}
-
-/* Footer */
-tfoot
-{
- border-top: 2px solid #aaa;
-}
-
-tfoot tr
-{
- text-align: center;
-}
-
--- a/static/dhcp/table.js Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,70 +0,0 @@
-/*
- * Dynamic pvl.verkko.table frontend.
- */
-function html (tag, params, html) {
- return $("<" + tag + " />", $.extend({html: html}, params));
-};
-
-/*
- * item_url - base URL path to /items/0 by id=0
- * columns - [ column_name ]
- * animate - attempt to animate table..
- */
-function Table (table, params) {
- var tbody = table.children('tbody');
- var animate = params.animate || false;
-
- if (animate)
- table.css('display', 'block');
-
- /*
- * Render <tr> for item.
- */
- function render_tr (item) {
- var columns = [
- html("th", {},
- html("a", { href: params.item_url.replace('0', item.id) }, "#" )
- ),
- ];
-
- $.each(params.columns, function (i, column) {
- var col = item[column];
- var td = html("td", { title: col.title }, col.html);
-
- $.each(col.css, function (i, css) {
- td.addClass(css);
- });
-
- columns.push(td);
- });
-
- return html("tr", { id: item.id }, columns);
- }
-
- return {
- animate: animate,
-
- /*
- * Update given item into our <table>
- */
- update: function (item) {
- var tr = $('#' + item.id);
-
- if (tr.length) {
- if (animate) tr.slideUp();
- } else {
- tr = render_tr(item)
-
- if (animate) tr.hide();
- }
-
- // move to top
- tr.prependTo(tbody);
-
- if (animate)
- tr.slideDown();
- else
- tr.effect("highlight", {}, 'slow');
- }
- }
-};
--- a/static/rrd/rrd.css Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,61 +0,0 @@
-/* Nagivation breadcrumb */
-div#breadcrumb
-{
- padding: 5pt;
-
- border-bottom: 1px dotted #aaa;
-}
-
-div#breadcrumb a
-{
- color: #444;
- font-weight: bold;
- text-decoration: none;
-}
-
-div#breadcrumb a:hover
-{
- text-decoration: underline;
-}
-
-/* Directory overview */
-#overview ul
-{
- list-style-type: none;
-}
-
-#overview ul li
-{
- margin: 5pt;
- padding: 5pt;
-}
-
-#overview ul li.even
-{
- background-color: #fff;
-}
-
-#overview ul li.odd
-{
- background-color: #eee;
-}
-
-#overview ul li h3
-{
- margin: 0px;
- padding: 5pt;
-}
-
-#overview ul a
-{
- color: #444;
- font-weight: bold;
- font-size: large;
- text-decoration: none;
-}
-
-#overview ul a:hover
-{
- text-decoration: underline;
-}
-
--- a/static/wlan.css Tue Feb 24 12:47:09 2015 +0200
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-/*
- * General
- */
-a
-{
- color: inherit;
-}
-
-/*
- * Hosts
- */
-.id
-{
- font-weight: bold;
- text-align: right;
-}
-
-.ap,
-.sta
-{
- font-family: monospace;
-}
-
-.seen
-{
-}