# HG changeset patch # User Tero Marttila # Date 1424782231 -7200 # Node ID d45fc43c6073fe5fde4b48a1a3ba37ff85ac387c # Parent 5100b359906c2f30f4af36061d1615091f841598 split out pvl-hosts from pvl-verkko diff -r 5100b359906c -r d45fc43c6073 MANIFEST.in --- 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 diff -r 5100b359906c -r d45fc43c6073 README --- 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 diff -r 5100b359906c -r d45fc43c6073 bin/pvl.login-server --- 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) diff -r 5100b359906c -r d45fc43c6073 bin/pvl.rrd-graph --- 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)) diff -r 5100b359906c -r d45fc43c6073 bin/pvl.rrd-interfaces --- 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] [ [...]]', - 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: -.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: .txt -> ") - - # output - parser.add_option('--rrd', metavar='PATH', - help="Output directory for .rrd symlinks: .txt -> /") - 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 : - # /-.txt -> /- - if '.txt' in path: - basepath, _ = os.path.splitext(path) - else: - basepath = path - - # / -> - _, basename = os.path.split(basepath) - - # /- -> /, - 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)) diff -r 5100b359906c -r d45fc43c6073 bin/pvl.switches-traps --- 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) - diff -r 5100b359906c -r d45fc43c6073 bin/pvl.verkko-dhcp --- 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] [ [...]]', - 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)) - - diff -r 5100b359906c -r d45fc43c6073 bin/pvl.verkko-rrd --- 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] [ [...]]', - 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)) - - diff -r 5100b359906c -r d45fc43c6073 bin/pvl.wlan-syslog --- 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] [ [...]]', - 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.+?): STA (?P.+?) (?P.+)') - - 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)) diff -r 5100b359906c -r d45fc43c6073 etc/snmptrapd.conf --- 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 diff -r 5100b359906c -r d45fc43c6073 pvl/hosts.py --- 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', diff -r 5100b359906c -r d45fc43c6073 pvl/login/auth.py --- 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 - diff -r 5100b359906c -r d45fc43c6073 pvl/login/pubtkt.py --- 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 - - diff -r 5100b359906c -r d45fc43c6073 pvl/login/server.py --- 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/', 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) diff -r 5100b359906c -r d45fc43c6073 pvl/login/ssl.py --- 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) diff -r 5100b359906c -r d45fc43c6073 pvl/login/static/pubtkt-expire.js --- 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); - }); -}); - diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/__init__.py --- 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, -) diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/api.py --- 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)) - diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/args.py --- 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) - diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/graph.py --- 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() - diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/hosts.py --- 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 diff -r 5100b359906c -r d45fc43c6073 pvl/rrd/rrds.py --- 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 diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/__init__.py --- 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' diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/db.py --- 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 diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/dhcp.py --- 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/', hosts.ItemHandler), - urls.rule('/hosts/realtime', hosts.RealtimeHandler), - urls.rule('/leases/', leases.ListHandler), - )) - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/hosts.py --- 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) : - """ - 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 +
- """ - - 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
, 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), - )), - ) - ) - ) - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/leases.py --- 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) : - """ -
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 +
- """ - - 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, - ) - - - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/rrd.py --- 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), - urls.rule('//', Index), - urls.rule('//', Target), - urls.rule('//.png', Graph), - )) - - def __init__ (self, rrd, **opts) : - """ - rrd - pvl.rrd.RRDDatabase - """ - - super(Application, self).__init__(**opts) - - self.rrd = rrd - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/table.py --- 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 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 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 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 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
for
for
. - """ - - if self.rowhtml : - return self.rowhtml(item) - else : - return value - - def cell_css (self, item, value=None, hilight=None) : - """ - Return CSS classes for . - """ - - 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 - """ - - if self.rowtitle : - return self.rowtitle(item) - - def render_cell (self, item, table, filters=None, hilight=None) : - """ - Render for item in
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
- 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 element. Wrapped in 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), - ) diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/utils.py --- 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() - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/web.py --- 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 - diff -r 5100b359906c -r d45fc43c6073 pvl/verkko/wlan.py --- 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) : - """ -
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 +
- """ - - 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), - )) diff -r 5100b359906c -r d45fc43c6073 pvl/web/__init__.py --- 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 - diff -r 5100b359906c -r d45fc43c6073 pvl/web/application.py --- 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 ...). - """ - - raise NotImplementedError() - - def render_html (self, body=None, extrahead=None) : - """ - Render page layout (as ). - """ - - 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 diff -r 5100b359906c -r d45fc43c6073 pvl/web/args.py --- 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 - diff -r 5100b359906c -r d45fc43c6073 pvl/web/html.py --- 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 !")) - u'\\n\\tGoogle <this>!\\n' -""" - -# 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'XYbX' - """ - - 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('')) - [u''] - >>> list(Text('', tag('p', 'test'))) - [u'', u'

', u'\\ttest', u'

'] - >>> list(tag('a', Text(''))) - [u'', u'\\t', u''] - >>> 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) - - >>> 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 instead of - (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) - - """ - - 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''] - """ - - # 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''), (2, u'bb'), (1, u''), (1, u'c')] - >>> list(Tag.build('tag', Tag.build('hr'), Tag.build('foo')('bar')).flatten_items()) - [(1, u'
'), (1, u''), (2, u'bar'), (1, u'')] - """ - - 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'')] - >>> list(Tag.build('bar', id=5).flatten()) - [(0, u'')] - - Values are indented inside the start tag: - - >>> list(Tag.build('foo', 'bar', a=5).flatten()) - [(0, u''), (1, u'bar'), (0, u'')] - - 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, u'1.3'), (0, u'')] - - Empty tags are rendered with a separate closing tag on the same line, if desired: - - >>> list(Tag.build('foo', _selfclosing=False).flatten()) - [(0, u'')] - >>> list(Tag.build('foo', src='asdf', _selfclosing=False).flatten()) - [(0, u'')] - - Tags that are declared as whitespace-sensitive are collapsed onto the same line: - - >>> list(Tag.build('foo', _whitespace_sensitive=True).flatten()) - [(0, u'')] - >>> list(Tag.build('foo', _whitespace_sensitive=True, _selfclosing=False).flatten()) - [(0, u'')] - >>> list(Tag.build('foo', 'bar', _whitespace_sensitive=True).flatten()) - [(0, u'bar')] - >>> list(Tag.build('foo', 'bar\\nasdf\\tx', _whitespace_sensitive=True).flatten()) - [(0, u'bar\\nasdf\\tx')] - >>> list(Tag.build('foo', 'bar', Tag.build('quux', 'asdf'), 'asdf', _whitespace_sensitive=True).flatten()) - [(0, u'barasdfasdf')] - - Embedded HTML given as string values is escaped: - - >>> list(Tag.build('foo', '')) - [u'', u'\\t<asdf>', u''] - - Embedded quotes in attribute values are esacaped: - - >>> list(Tag.build('foo', style='ok;" onload="...')) - [u''] - """ - - # 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>" % (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" % (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"" % (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'', u'', u'\\t...', u''] - """ - - 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 tag - """ - - if self.xml_declaration : - yield 0, u'' % (' '.join('%s="%s"' % kv for kv in self.xml_declaration.iteritems())) - - if self.doctype : - yield 0, u'' % (self.doctype) - - # - 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'', u'\\tYay', u''] - - >>> 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() - diff -r 5100b359906c -r d45fc43c6073 pvl/web/response.py --- 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) - diff -r 5100b359906c -r d45fc43c6073 pvl/web/urls.py --- 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) - diff -r 5100b359906c -r d45fc43c6073 setup.py --- 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-*', + ), ) diff -r 5100b359906c -r d45fc43c6073 static/dhcp/forms.css --- 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 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; -} - diff -r 5100b359906c -r d45fc43c6073 static/dhcp/hosts.css --- 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; -} diff -r 5100b359906c -r d45fc43c6073 static/dhcp/hosts.js --- 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(); - } -} diff -r 5100b359906c -r d45fc43c6073 static/dhcp/spin.js --- 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> 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); diff -r 5100b359906c -r d45fc43c6073 static/dhcp/table.css --- 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; -} - diff -r 5100b359906c -r d45fc43c6073 static/dhcp/table.js --- 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
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
- */ - 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'); - } - } -}; diff -r 5100b359906c -r d45fc43c6073 static/rrd/rrd.css --- 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; -} - diff -r 5100b359906c -r d45fc43c6073 static/wlan.css --- 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 -{ -}