diff options
author | Rob Austein <sra@hactrn.net> | 2012-04-15 04:42:40 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2012-04-15 04:42:40 +0000 |
commit | fd695c2371824c1952510bab9fbe0e05b52b9e9d (patch) | |
tree | 60b9836b9d24055d900be3335856ec4e0091cec2 | |
parent | b5eb637d68bd8387cfff7cb06945f6654d1192db (diff) | |
parent | f4d381b2ead3a3fab4b7b0c73cdc8d3a6b4cb12d (diff) |
Merge branches/tk161 to trunk.
svn path=/trunk/; revision=4415
139 files changed, 8603 insertions, 4164 deletions
diff --git a/buildtools/html2textrc b/buildtools/html2textrc new file mode 100644 index 00000000..96dee581 --- /dev/null +++ b/buildtools/html2textrc @@ -0,0 +1,60 @@ +# $Id$ +# +# html2text configuration settings for postprocessing Trac Wiki HTML. +# +A.attributes.external_link = NONE +A.attributes.internal_link = NONE +B.attributes = NONE +BLOCKQUOTE.vspace.after = 1 +BLOCKQUOTE.vspace.before = 1 +CODE.vspace.after = 0 +CODE.vspace.before = 0 +DD.indent = 6 +DIR.indents = 2 +DIR.vspace.before = 1 +DL.vspace.after = 1 +DL.vspace.before = 1 +DT.indent = 2 +DT.vspace.before = 1 +EM.attributes = NONE +H1.attributes = NONE +H1.vspace.after = 1 +H1.vspace.before = 0 +H2.attributes = NONE +H2.vspace.after = 1 +H2.vspace.before = 1 +H3.attributes = NONE +H3.vspace.after = 1 +H3.vspace.before = 1 +H4.attributes = NONE +H4.vspace.after = 1 +H4.vspace.before = 1 +H5.attributes = NONE +H5.vspace.after = 1 +H5.vspace.before = 1 +H6.attributes = NONE +H6.vspace.after = 1 +H6.vspace.before = 1 +HR.marker = = +IMG.alt.prefix = \ +IMG.alt.suffix = \ +IMG.replace.noalt = +MENU.vspace.after = 1 +MENU.vspace.before = 1 +OL.TYPE = 1 +OL.indents = 5 +OL.vspace.after = 1 +OL.vspace.before = 1 +P.vspace.after = 1 +P.vspace.before = 0 +PRE.indent.left = 2 +PRE.vspace.after = 1 +PRE.vspace.before = 1 +STRIKE.attributes = NONE +STRONG.attributes = NONE +TABLE.vspace.after = 1 +TABLE.vspace.before = 1 +U.attributes = NONE +UL.indents = 2 +UL.vspace.after = 1 +UL.vspace.before = 1 diff --git a/buildtools/make-relaxng.py b/buildtools/make-relaxng.py index 62decbae..0058ade5 100644 --- a/buildtools/make-relaxng.py +++ b/buildtools/make-relaxng.py @@ -3,7 +3,7 @@ Script to generate rpki/relaxng.py. $Id$ -Copyright (C) 2009 Internet Systems Consortium ("ISC") +Copyright (C) 2009-2011 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -32,7 +32,7 @@ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -schemas = ("left_right", "up_down", "publication") +import sys format_1 = """\ # Automatically generated, do not edit. @@ -46,9 +46,15 @@ format_2 = """\ %(name)s = lxml.etree.RelaxNG(lxml.etree.fromstring('''%(rng)s''')) """ +def filename_to_symbol(s): + for suffix in (".rng", "-schema"): + if s.endswith(suffix): + s = s[:-len(suffix)] + return s.replace("-", "_") + print format_1 -for name in schemas: +for filename in sys.argv[1:]: print format_2 % { - "name" : name, - "rng" : open(name.replace("_", "-") + "-schema.rng").read() } + "name" : filename_to_symbol(filename), + "rng" : open(filename).read() } diff --git a/buildtools/make-sql-schemas.py b/buildtools/make-sql-schemas.py index 700d2b9c..3ecde014 100644 --- a/buildtools/make-sql-schemas.py +++ b/buildtools/make-sql-schemas.py @@ -32,7 +32,7 @@ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -schemas = ("rpkid", "irdbd", "pubd") +schemas = ("rpkid", "pubd") format_1 = """\ # Automatically generated, do not edit. diff --git a/buildtools/pull-doc-from-wiki.py b/buildtools/pull-doc-from-wiki.py new file mode 100644 index 00000000..946b01ae --- /dev/null +++ b/buildtools/pull-doc-from-wiki.py @@ -0,0 +1,337 @@ +""" +Pull HTML pages from a Trac Wiki, feed the useful bits to htmldoc and +html2text to generate PDF and flat text documentation. + +Assumes you're using the TracNav plugin for the Wiki pages, and uses +the same list as the TracNav plugin does to determine the set of pages +to convert and the order in which they appear in the PDF file. + +Most of the work of massaging the HTML is done using XSL transforms, +because the template-driven style makes that easy. There's probably +some clever way to use lxml's XPath code to do the same thing in a +more pythonic way with ElementTrees, but I already had the XSL +transforms and there's a point of diminishing returns on this sort of +thing. + +$Id$ + +Copyright (C) 2012 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import sys +import os +import getopt +import lxml.etree +import urllib +import urlparse +import subprocess + +# Main program, up front so it doesn't get lost under all the XSL + +def main(): + + base = "https://trac.rpki.net" + toc = base + "/wiki/doc/RPKI/TOC" + pdf = "manual.pdf" + dir = "." + h2trc = os.path.join(os.path.dirname(sys.argv[0]), "html2textrc") + + options = ["base_url=", "directory=", "help", "html2textrc", "pdf_file=", "toc="] + + def usage(msg = 0): + sys.stderr.write("Usage: %s %s\n" % ( + sys.argv[0], " ".join("[%s value]" % o[:-1] if o.endswith("=") else "[%s]" % o + for o in options))) + sys.stderr.write(__doc__) + sys.exit(msg) + + opts, argv = getopt.getopt(sys.argv[1:], "b:d:hp:r:t:?", options) + for o, a in opts: + if o in ("-h", "--help", "-?"): + usage() + elif o in ("-b", "--base_url"): + base = a + elif o in ("-d", "--directory"): + dir = a + elif o in ("-p", "--pdf_file"): + pdf = a + elif o in ("-r", "--html2textrc"): + h2trc = a + elif o in ("-t", "--toc"): + toc = a + if argv: + usage("Unexpected arguments %s" % argv) + + urls = str(xsl_get_toc(lxml.etree.parse(urllib.urlopen(toc)).getroot(), + basename = repr(base))).splitlines() + + assert all(urlparse.urlparse(url).path.startswith("/wiki/") for url in urls) + + htmldoc = subprocess.Popen( + ("htmldoc", "--book", "--title", "--outfile", pdf, "--format", "pdf", + "--firstpage", "p1", "--size", "Universal", "--no-duplex", + "--fontsize", "11.0", "--fontspacing", "1.1", "--headfootsize", "11.0", + "--headingfont", "Helvetica", "--bodyfont", "Times", "--headfootfont", "Helvetica-Oblique", + "-"), stdin = subprocess.PIPE) + + lxml.etree.ElementTree(xml_title).write(htmldoc.stdin) + + for url in urls: + path = urlparse.urlparse(url).path + page = xsl_get_page(lxml.etree.parse(urllib.urlopen(url)).getroot(), + basename = repr(base), + path = repr(path)) + + page.write(htmldoc.stdin) + + html2text = subprocess.Popen(("html2text", "-rcfile", h2trc, "-nobs", "-ascii"), + stdin = subprocess.PIPE, + stdout = subprocess.PIPE) + page.write(html2text.stdin) + html2text.stdin.close() + lines = html2text.stdout.readlines() + html2text.stdout.close() + html2text.wait() + + while lines and lines[0].isspace(): + del lines[0] + + fn = os.path.join(dir, path[len("/wiki/"):].replace("/", ".")) + f = open(fn, "w") + want_blank = False + for line in lines: + blank = line.isspace() + if want_blank and not blank: + f.write("\n") + if not blank: + f.write(line) + want_blank = blank + f.close() + sys.stderr.write("Wrote %s\n" % fn) + + htmldoc.stdin.close() + htmldoc.wait() + sys.stderr.write("Wrote %s\n" % pdf) + + +# HTMLDOC title page. At some point we might want to generate this +# dynamically as an ElementTree, but static content will do for the +# moment. + +xml_title = lxml.etree.HTML('''\ + <html> + <head> + <meta name="author" content="http://rpki.net"> + <title>RPKI Tools Manual</title> + </head> + <body> + </body> + </html> +''') + +# XSL transform to extract list of Wiki page URLs from the TOC Wiki page + +xsl_get_toc = lxml.etree.XSLT(lxml.etree.XML('''\ + <xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + version="1.0"> + + <xsl:output method="text" encoding="us-ascii"/> + + <xsl:param name="basename"/> + + <xsl:template match="/"> + <xsl:for-each select="//div[@id = 'wikipage']/ul//a"> + <xsl:value-of select="concat($basename, @href, ' ')"/> + </xsl:for-each> + </xsl:template> + + </xsl:transform> +''')) + +# XSL transform to extract useful content of a Wiki page. + +# Django generates weird HTML for ordered lists: it sometimes breaks +# up a single ordered list into multiple adjacent <ol/> elements, +# using the @start attribute to try to make the result look like a +# single ordered list. This looks OK in Firefox but confuses the +# bejesus out of both html2text and htmldoc. In some cases this is +# probably unavoidable, but most of the uses of this I've seen look +# gratuitous, and are probably the result of code modulararity issues +# in Django. +# +# So we try to clean this up, by merging adjacent <ol/> elements where +# we can. The merge incantation is an adaptation of: +# +# http://stackoverflow.com/questions/1806123/merging-adjacent-nodes-of-same-type-xslt-1-0 +# +# There may be a more efficient way to do this, but I don't think +# we care, and this seems to work. +# +# Original author's explanation: +# +# The rather convoluted XPath expression for selecting the following +# sibling aaa nodes which are merged with the current one: +# +# following-sibling::aaa[ # following 'aaa' siblings +# not(preceding-sibling::*[ # if they are not preceded by +# not(self::aaa) and # a non-'aaa' node +# not(following-sibling::aaa = current()) # after the current node +# ]) +# ] + +xsl_get_page = lxml.etree.XSLT(lxml.etree.XML('''\ + <xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"> + + <xsl:output method="xml" encoding="us-ascii" omit-xml-declaration="yes" /> + + <xsl:param name="basename"/> + <xsl:param name="path"/> + + <xsl:template match="/"> + <xsl:message><xsl:value-of select="concat('Got path: ', $path)"/></xsl:message> + <xsl:variable name="id"> + <xsl:call-template name="path-to-id"> + <xsl:with-param name="p" select="$path"/> + </xsl:call-template> + </xsl:variable> + <xsl:message><xsl:value-of select="concat('Got id: ', $id)"/></xsl:message> + <xsl:comment>NEW PAGE</xsl:comment> + <html> + <body> + <div id="{$id}"> + <xsl:apply-templates select="//div[@id = 'wikipage']/*"/> + </div> + </body> + </html> + </xsl:template> + + <xsl:template match="//div[contains(@class, 'wiki-toc')]"/> + + <xsl:template match="//span[@class = 'icon' and not(*)]"/> + + <xsl:template match="a[contains(@class, 'wiki') and + starts-with(@href, '/wiki/')]"> + <xsl:variable name="href"> + <xsl:call-template name="path-to-id"> + <xsl:with-param name="p" select="@href"/> + </xsl:call-template> + </xsl:variable> + <a href="#{$href}"> + <xsl:apply-templates select="@*[name() != 'href']"/> + <xsl:apply-templates/> + </a> + </xsl:template> + + <xsl:template match="a[starts-with(@href, '/attachment/wiki/')]"> + <a href="{concat($basename, @href)}"> + <xsl:apply-templates select="@*[name() != 'href']"/> + <xsl:apply-templates/> + </a> + </xsl:template> + + <xsl:template match="img[starts-with(@src, '/raw-attachment/wiki/')]"> + <img src="{concat($basename, @src)}"> + <xsl:apply-templates select="@*[name() != 'src']"/> + <xsl:apply-templates/> + </img> + </xsl:template> + + <xsl:template match="text()[contains(., '​')]"> + <xsl:call-template name="remove-zero-width-spaces"> + <xsl:with-param name="s" select="."/> + </xsl:call-template> + </xsl:template> + + <xsl:template match="@*|node()"> + <xsl:copy> + <xsl:copy-of select="@*"/> + <xsl:apply-templates/> + </xsl:copy> + </xsl:template> + + <xsl:template name="path-to-id"> + <xsl:param name="p"/> + <xsl:text>_</xsl:text> + <xsl:call-template name="replace"> + <xsl:with-param name="s" select="$p"/> + <xsl:with-param name="old">/</xsl:with-param> + <xsl:with-param name="new">.</xsl:with-param> + </xsl:call-template> + </xsl:template> + + <xsl:template name="remove-zero-width-spaces"> + <xsl:param name="s"/> + <xsl:call-template name="replace"> + <xsl:with-param name="s" select="$s"/> + <xsl:with-param name="old">​</xsl:with-param> + <xsl:with-param name="new"/> + </xsl:call-template> + </xsl:template> + + <xsl:template name="replace"> + <xsl:param name="s"/> + <xsl:param name="old"/> + <xsl:param name="new"/> + <xsl:choose> + <xsl:when test="contains($s, $old)"> + <xsl:call-template name="replace"> + <xsl:with-param name="s" select="concat(substring-before($s, $old), + $new, + substring-after($s, $old))"/> + <xsl:with-param name="old" select="$old"/> + <xsl:with-param name="new" select="$new"/> + </xsl:call-template> + </xsl:when> + <xsl:otherwise> + <xsl:value-of select="$s"/> + </xsl:otherwise> + </xsl:choose> + </xsl:template> + + <xsl:template match="ol"> + <xsl:if test="not(preceding-sibling::*[1]/self::ol)"> + <xsl:variable name="following" + select="following-sibling::ol[ + not(preceding-sibling::*[ + not(self::ol) and + not(following-sibling::ol = current()) + ]) + ]"/> + <xsl:copy> + <xsl:apply-templates select="$following/@*[name() != 'start']"/> + <xsl:apply-templates select="@*"/> + <xsl:apply-templates select="node()"/> + <xsl:apply-templates select="$following/node()"/> + </xsl:copy> + </xsl:if> + </xsl:template> + + </xsl:transform> +''')) + +# All the files we want to parse are HTML, so make HTML the default +# parser. In theory the HTML produced by Trac is XHTML thus should +# parse correctly (in fact, better) as XML, but in practice this seems +# not to work properly at the moment, while parsing as HTML does. +# Haven't bothered to figure out why, life is too short. +# +# If you're reading this comment because this script stopped working +# after a Trac upgrade, try commenting out this line to see whether +# things have changed and Trac's HTML now parses better as XML. + +lxml.etree.set_default_parser(lxml.etree.HTMLParser()) + +# Run the main program. +main() @@ -640,7 +640,7 @@ LIBOBJS DJANGO_ADMIN DJANGO_DIR SECRET_KEY -WEBUSER +VIRTUAL_ENV OPENSSL_SO_GLOB OPENSSL_CONFIG_COMMAND RPKID_SUBDIRS @@ -4929,21 +4929,17 @@ fi if test $build_django = yes then - # the user that the apache process is running as - if test "x$WEBUSER" = "x"; then - for u in apache www-data www; do - if $GREP $u /etc/passwd >/dev/null 2>&1; then - WEBUSER=$u - break - fi - done - if test "x$WEBUSER" = "x"; then - { { $as_echo "$as_me:$LINENO: error: Could not determine which user the apache process runs as. Please specify WEBUSER=<USERNAME>." >&5 -$as_echo "$as_me: error: Could not determine which user the apache process runs as. Please specify WEBUSER=<USERNAME>." >&2;} - { (exit 1); exit 1; }; } - fi - fi + { $as_echo "$as_me:$LINENO: checking if running under virtualenv" >&5 +$as_echo_n "checking if running under virtualenv... " >&6; } + if test x$VIRTUAL_ENV != x; then + VIRTUAL_ENV=$VIRTUAL_ENV + { $as_echo "$as_me:$LINENO: result: $VIRTUAL_ENV" >&5 +$as_echo "$VIRTUAL_ENV" >&6; } + else + { $as_echo "$as_me:$LINENO: result: no" >&5 +$as_echo "no" >&6; } + fi # source: http://blog.leosoto.com/2008/04/django-secretkey-generation.html SECRET_KEY=`$PYTHON -c 'import random; print "".join(random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50))'` diff --git a/configure.ac b/configure.ac index 03fe8c4d..b1508796 100644 --- a/configure.ac +++ b/configure.ac @@ -394,19 +394,13 @@ fi if test $build_django = yes then - # the user that the apache process is running as - if test "x$WEBUSER" = "x"; then - for u in apache www-data www; do - if $GREP $u /etc/passwd >/dev/null 2>&1; then - WEBUSER=$u - break - fi - done - if test "x$WEBUSER" = "x"; then - AC_MSG_ERROR([Could not determine which user the apache process runs as. Please specify WEBUSER=<USERNAME>.]) - fi + AC_MSG_CHECKING([if running under virtualenv]) + if test x$VIRTUAL_ENV != x; then + AC_SUBST(VIRTUAL_ENV, [$VIRTUAL_ENV]) + AC_MSG_RESULT([$VIRTUAL_ENV]) + else + AC_MSG_RESULT(no) fi - AC_SUBST(WEBUSER) # source: http://blog.leosoto.com/2008/04/django-secretkey-generation.html AC_SUBST(SECRET_KEY, `$PYTHON -c 'import random; print "".join([random.choice("abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*(-_=+)") for i in range(50)])'`) diff --git a/rpkid/Makefile.in b/rpkid/Makefile.in index 1ce31177..bb177cc5 100644 --- a/rpkid/Makefile.in +++ b/rpkid/Makefile.in @@ -4,8 +4,8 @@ SUBDIRS = @RPKID_SUBDIRS@ PYTHON = @PYTHON@ SECRET_KEY = @SECRET_KEY@ -WEBUSER = @WEBUSER@ DJANGO_DIR = @DJANGO_DIR@ +VIRTUAL_ENV = @VIRTUAL_ENV@ CFLAGS = @CFLAGS@ LDFLAGS = @LDFLAGS@ @POW_LDFLAGS@ @@ -42,15 +42,18 @@ SETUP_PY = \ POW_SO = rpki/POW/_POW.so SCRIPTS = rpki-sql-backup rpki-sql-setup rpki-start-servers irbe_cli irdbd myrpki \ - pubd rootd rpkid portal-gui/scripts/rpkigui-load-csv \ - portal-gui/scripts/rpkigui-add-user portal-gui/scripts/rpkigui-response \ - portal-gui/scripts/rpkigui-rcynic + pubd rootd rpkic rpkid \ + portal-gui/scripts/rpkigui-rcynic \ + portal-gui/scripts/rpkigui-import-routes + +# scripts we build, but don't install +BUILD_SCRIPTS = portal-gui/scripts/rpkigui-reset-demo AUX_SCRIPTS = -SETTINGS = portal-gui/settings.py rpki/gui/app/settings.py portal-gui/apache/rpki.wsgi +SETTINGS = portal-gui/settings.py rpki/gui/app/settings.py portal-gui/rpki.wsgi -all:: ${POW_SO} rpki/relaxng.py myrpki.rng rpki/sql_schemas.py ${SCRIPTS} ${AUX_SCRIPTS} ${SETTINGS} +all:: ${POW_SO} rpki/relaxng.py myrpki.rng rpki/sql_schemas.py ${SCRIPTS} ${AUX_SCRIPTS} ${SETTINGS} ${BUILD_SCRIPTS} ${POW_SO}: ext/POW.c setup.py ${SETUP_PY} build_ext --inplace @@ -64,8 +67,10 @@ rpm deb:: all deb:: cd dist; for i in *.rpm; do case $$i in *.src.rpm) :;; *) (set -x; fakeroot alien -v $$i);; esac; done -rpki/relaxng.py: ${abs_top_srcdir}/buildtools/make-relaxng.py left-right-schema.rng up-down-schema.rng publication-schema.rng - ${PYTHON} ${abs_top_srcdir}/buildtools/make-relaxng.py >$@.tmp +RNGS = left-right-schema.rng up-down-schema.rng publication-schema.rng myrpki.rng + +rpki/relaxng.py: ${abs_top_srcdir}/buildtools/make-relaxng.py ${RNGS} + ${PYTHON} ${abs_top_srcdir}/buildtools/make-relaxng.py ${RNGS} >$@.tmp mv $@.tmp $@ left-right-schema.rng: left-right-schema.rnc @@ -80,7 +85,7 @@ publication-schema.rng: publication-schema.rnc myrpki.rng: myrpki.rnc trang myrpki.rnc myrpki.rng -rpki/sql_schemas.py: ${abs_top_srcdir}/buildtools/make-sql-schemas.py rpkid.sql irdbd.sql pubd.sql +rpki/sql_schemas.py: ${abs_top_srcdir}/buildtools/make-sql-schemas.py rpkid.sql pubd.sql ${PYTHON} ${abs_top_srcdir}/buildtools/make-sql-schemas.py >$@.tmp mv $@.tmp $@ @@ -121,7 +126,7 @@ tags: Makefile find . -type f \( -name '*.py' -o -name '*.sql' -o -name '*.rnc' -o -name '*.py.in' \) ! -name relaxng.py ! -name sql_schemas.py ! -name __doc__.py | etags - lint: - pylint --rcfile ${abs_top_srcdir}/buildtools/pylint.rc rpki/[a-z]*.py *d.py rpki-*.py myrpki.py irbe_cli.py tests/*.py + pylint --rcfile ${abs_top_srcdir}/buildtools/pylint.rc rpki/[a-z]*.py *d.py rpki-*.py myrpki.py rpkic.py irbe_cli.py tests/*.py # Documentation @@ -200,10 +205,8 @@ COMPILE_DJANGO = \ COMPILE_SETTINGS = \ rm -f $@; \ - AC_DATABASE_PATH='${DESTDIR}${localstatedir}/rpki/gui.db' \ AC_SECRET_KEY='${SECRET_KEY}' \ AC_LOCALSTATEDIR='${DESTDIR}${localstatedir}' \ - AC_WEBUSER='${WEBUSER}' \ AC_DATAROOTDIR='${DESTDIR}${datarootdir}' \ AC_DJANGO_DIR='${DJANGO_DIR}' \ AC_SYSCONFDIR='${DESTDIR}${sysconfdir}' \ @@ -233,24 +236,26 @@ pubd: pubd.py rootd: rootd.py ${COMPILE_PYTHON} -rpkid: rpkid.py +rpkic: rpkic.py ${COMPILE_PYTHON} -portal-gui/scripts/rpkigui-load-csv: portal-gui/scripts/load_csv.py - ${COMPILE_DJANGO} - -portal-gui/scripts/rpkigui-add-user: portal-gui/scripts/adduser.py - ${COMPILE_DJANGO} +rpkid: rpkid.py + ${COMPILE_PYTHON} portal-gui/scripts/rpkigui-rcynic: portal-gui/scripts/rpkigui-rcynic.py ${COMPILE_DJANGO} -portal-gui/scripts/rpkigui-response: portal-gui/scripts/rpkigui-response.py +portal-gui/scripts/rpkigui-import-routes: portal-gui/scripts/rpkigui-import-routes.py ${COMPILE_DJANGO} -portal-gui/apache/rpki.wsgi: ${srcdir}/portal-gui/apache/rpki.wsgi.in +portal-gui/scripts/rpkigui-reset-demo: portal-gui/scripts/rpkigui-reset-demo.py ${COMPILE_DJANGO} +portal-gui/rpki.wsgi: ${srcdir}/portal-gui/rpki.wsgi.in + sed -e "s|@VIRTUAL"_"ENV@|${VIRTUAL_ENV}|" \ + -e "s|@PYTHON""PATH@|${sysconfdir}/rpki|" \ + ${srcdir}/portal-gui/rpki.wsgi.in > portal-gui/rpki.wsgi + portal-gui/settings.py: ${srcdir}/portal-gui/settings.py.in ${COMPILE_SETTINGS} diff --git a/rpkid/examples/rpki.conf b/rpkid/examples/rpki.conf index 9ad7e3d0..84d7109c 100644 --- a/rpkid/examples/rpki.conf +++ b/rpkid/examples/rpki.conf @@ -308,7 +308,7 @@ rpki-root-cert-uri = rsync://${myrpki::publication_rsync_server}/${myrpki: # Private key corresponding to rootd's root RPKI certificate -rpki-root-key = ${myrpki::bpki_servers_directory}/ca.key +rpki-root-key = ${myrpki::bpki_servers_directory}/root.key # Filename (as opposed to rsync URI) of rootd's root RPKI certificate @@ -368,6 +368,16 @@ root_cert_manifest = rsync://${myrpki::publication_rsync_server}/${myrpki::publ ################################################################# +# Glue to allow the django application to pull user configuration +# from this file rather than directly editing settings.py + +[web_portal] +sql-database = ${myrpki::irdbd_sql_database} +sql-username = ${myrpki::irdbd_sql_username} +sql-password = ${myrpki::irdbd_sql_password} + +################################################################# + # Constants for OpenSSL voodoo portion of this file, to make them # easier to find. diff --git a/rpkid/irbe_cli.py b/rpkid/irbe_cli.py index 637ad720..520b186d 100644 --- a/rpkid/irbe_cli.py +++ b/rpkid/irbe_cli.py @@ -307,18 +307,39 @@ while argv: argv = q_pdu.client_getopt(argv[1:]) q_msg.append(q_pdu) +from django.conf import settings + +settings.configure( + DATABASES = { "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : cfg.get("sql-database", section = "irdbd"), + "USER" : cfg.get("sql-username", section = "irdbd"), + "PASSWORD" : cfg.get("sql-password", section = "irdbd"), + "HOST" : "", + "PORT" : "", + "OPTIONS" : { "init_command": "SET storage_engine=INNODB" }}}, + INSTALLED_APPS = ("rpki.irdb",), +) + +import rpki.irdb + +server_ca = rpki.irdb.ServerCA.objects.get() +irbe = server_ca.ee_certificates.get(purpose = "irbe") + if q_msg_left_right: class left_right_proto(object): cms_msg = left_right_cms_msg msg = left_right_msg + rpkid = server_ca.ee_certificates.get(purpose = "rpkid") + call_rpkid = rpki.async.sync_wrapper(rpki.http.caller( proto = left_right_proto, - client_key = rpki.x509.RSA( Auto_file = cfg.get("rpkid-irbe-key")), - client_cert = rpki.x509.X509(Auto_file = cfg.get("rpkid-irbe-cert")), - server_ta = rpki.x509.X509(Auto_file = cfg.get("rpkid-bpki-ta")), - server_cert = rpki.x509.X509(Auto_file = cfg.get("rpkid-cert")), + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = server_ca.certificate, + server_cert = rpkid.certificate, url = cfg.get("rpkid-url"), debug = verbose)) @@ -330,12 +351,14 @@ if q_msg_publication: msg = publication_msg cms_msg = publication_cms_msg + pubd = server_ca.ee_certificates.get(purpose = "pubd") + call_pubd = rpki.async.sync_wrapper(rpki.http.caller( proto = publication_proto, - client_key = rpki.x509.RSA( Auto_file = cfg.get("pubd-irbe-key")), - client_cert = rpki.x509.X509(Auto_file = cfg.get("pubd-irbe-cert")), - server_ta = rpki.x509.X509(Auto_file = cfg.get("pubd-bpki-ta")), - server_cert = rpki.x509.X509(Auto_file = cfg.get("pubd-cert")), + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = server_ca.certificate, + server_cert = pubd.certificate, url = cfg.get("pubd-url"), debug = verbose)) diff --git a/rpkid/left-right-schema.rng b/rpkid/left-right-schema.rng index 80beb1f5..0b0fa8d1 100644 --- a/rpkid/left-right-schema.rng +++ b/rpkid/left-right-schema.rng @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- - $Id: left-right-schema.rnc 4346 2012-02-17 01:11:06Z sra $ + $Id: left-right-schema.rnc 4403 2012-03-19 21:14:48Z sra $ RelaxNG Schema for RPKI left-right protocol. diff --git a/rpkid/myrpki.rnc b/rpkid/myrpki.rnc index 5b8aa450..8acb16cf 100644 --- a/rpkid/myrpki.rnc +++ b/rpkid/myrpki.rnc @@ -2,10 +2,15 @@ # # RelaxNG Schema for MyRPKI XML messages. # +# This message protocol is on its way out, as we're in the process of +# moving on from the user interface model that produced it, but even +# after we finish replacing it we'll still need the schema for a while +# to validate old messages when upgrading. +# # libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so # run the compact syntax through trang to get XML syntax. # -# Copyright (C) 2009-2010 Internet Systems Consortium ("ISC") +# Copyright (C) 2009-2011 Internet Systems Consortium ("ISC") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above diff --git a/rpkid/myrpki.rng b/rpkid/myrpki.rng index a86d51a6..5f59e114 100644 --- a/rpkid/myrpki.rng +++ b/rpkid/myrpki.rng @@ -4,10 +4,15 @@ RelaxNG Schema for MyRPKI XML messages. + This message protocol is on its way out, as we're in the process of + moving on from the user interface model that produced it, but even + after we finish replacing it we'll still need the schema for a while + to validate old messages when upgrading. + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so run the compact syntax through trang to get XML syntax. - Copyright (C) 2009-2010 Internet Systems Consortium ("ISC") + Copyright (C) 2009-2011 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/rpkid/portal-gui/Makefile.in b/rpkid/portal-gui/Makefile.in index fa4e1e46..d4b7c5dc 100644 --- a/rpkid/portal-gui/Makefile.in +++ b/rpkid/portal-gui/Makefile.in @@ -16,21 +16,13 @@ sbindir=@sbindir@ libexecdir=@libexecdir@ sysconfdir=@sysconfdir@ -WEBUSER=@WEBUSER@ -DJANGO_ADMIN=@DJANGO_ADMIN@ -PYTHON=@PYTHON@ -DJANGO_DIR=@DJANGO_DIR@ - INSTALL = @INSTALL@ -CONFDIR=${DESTDIR}$(localstatedir)/rpki/conf -DATABASE_PATH=${DESTDIR}$(localstatedir)/rpki/gui.db -INSTDIR=${DESTDIR}$(datarootdir)/rpki/gui -STATIC_DIR=${INSTDIR}/static -PYTHONPATH=${DESTDIR}${sysconfdir}/rpki +INSTDIR=${DESTDIR}$(datarootdir)/rpki +SYSCONFDIR=${DESTDIR}${sysconfdir}/rpki # automatically built sources -BUILD=apache/rpki.conf +BUILD=apache.conf all: $(BUILD) @@ -42,41 +34,25 @@ distclean: clean rm -f Makefile edit = sed \ - -e 's|@DJANGO_DIR[@]|$(DJANGO_DIR)|g' \ - -e 's|@INSTDIR[@]|$(INSTDIR)|g' \ - -e 's|@STATIC_DIR[@]|$(STATIC_DIR)|g' + -e 's|@INSTDIR[@]|$(INSTDIR)|g' -apache/rpki.conf: $(srcdir)/apache/rpki.conf.in Makefile +apache.conf: $(srcdir)/apache.conf.in Makefile $(edit) $@.in > $@ -.PHONY: install-perms install-data install - -install-perms: - chown $(WEBUSER) `dirname $(DATABASE_PATH)` - chown $(WEBUSER) $(DATABASE_PATH) - mkdir -p $(CONFDIR) - chown -R $(WEBUSER) $(CONFDIR) - -install-apache: - ${INSTALL} -d -m 755 $(INSTDIR)/apache - ${INSTALL} -m 644 apache/rpki.conf $(INSTDIR)/apache - ${INSTALL} -m 644 apache/rpki.wsgi $(INSTDIR)/apache - -install-data: $(BUILD) install-apache - mkdir -p `dirname $(DATABASE_PATH)` - mkdir -p ${PYTHONPATH} +install: $(BUILD) + ${INSTALL} -d $(SYSCONFDIR) + ${INSTALL} -d $(INSTDIR)/media/css + ${INSTALL} -d $(INSTDIR)/wsgi + ${INSTALL} -m 644 apache.conf $(SYSCONFDIR)/apache.conf + ${INSTALL} -m 644 $(srcdir)/media/css/bootstrap.min.css $(INSTDIR)/media/css/bootstrap.min.css + ${INSTALL} -m 644 rpki.wsgi $(INSTDIR)/wsgi/rpki.wsgi # FIXME should eventually try to merge new settings? - @if [ ! -f ${PYTHONPATH}/settings.py ]; then \ - ${INSTALL} -m 644 settings.py ${PYTHONPATH}; \ + @if [ ! -f ${SYSCONFDIR}/settings.py ]; then \ + ${INSTALL} -m 644 settings.py ${SYSCONFDIR}; \ else \ - echo "${PYTHONPATH}/settings.py already exists, installing settings.py as ${PYTHONPATH}/settings.py.new"; \ - ${INSTALL} -m 644 settings.py ${PYTHONPATH}/settings.py.new; \ + echo "${SYSCONFDIR}/settings.py already exists, installing settings.py as ${SYSCONFDIR}/settings.py.new"; \ + ${INSTALL} -m 644 settings.py ${SYSCONFDIR}/settings.py.new; \ fi - $(DJANGO_ADMIN) syncdb --pythonpath ${PYTHONPATH} --settings settings - #$(DJANGO_ADMIN) collectstatic --noinput --pythonpath ${PYTHONPATH} --settings settings - if [ ! -f $(INSTDIR)/rpki.conf.template ]; then ${INSTALL} -m 644 ../examples/rpki.conf $(INSTDIR)/rpki.conf.template; fi - -install: install-data install-perms deinstall uninstall: rm -rf $(INSTDIR) diff --git a/rpkid/portal-gui/apache/rpki.conf.in b/rpkid/portal-gui/apache.conf.in index 9318549a..b9ca7a36 100644 --- a/rpkid/portal-gui/apache/rpki.conf.in +++ b/rpkid/portal-gui/apache.conf.in @@ -13,20 +13,14 @@ # # Defines the URL to the portal-gui # -WSGIScriptAlias / @INSTDIR@/apache/rpki.wsgi -<Directory @INSTDIR@/rpkigui> +WSGIScriptAlias / @INSTDIR@/wsgi/rpki.wsgi + +<Directory @INSTDIR@/media> Order deny,allow Allow from all </Directory> -# for use with Django 1.3+ -#Alias /static/ @STATIC_DIR@/ -#<Directory @STATIC_DIR@> -#Order allow,deny -#Allow from all -#</Directory> - -# for use with Django 1.2 -#Alias /media/ /var/www/html/media/ +Alias /media/ @INSTDIR@/media/ +Alias /site_media/ @INSTDIR@/media/ # vim:ft=apache diff --git a/rpkid/portal-gui/manage.py b/rpkid/portal-gui/manage.py deleted file mode 100644 index 5e78ea97..00000000 --- a/rpkid/portal-gui/manage.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python -from django.core.management import execute_manager -try: - import settings # Assumed to be in the same directory. -except ImportError: - import sys - sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) - sys.exit(1) - -if __name__ == "__main__": - execute_manager(settings) diff --git a/rpkid/portal-gui/media/css/bootstrap.min.css b/rpkid/portal-gui/media/css/bootstrap.min.css new file mode 100644 index 00000000..617c87f2 --- /dev/null +++ b/rpkid/portal-gui/media/css/bootstrap.min.css @@ -0,0 +1,356 @@ +html,body{margin:0;padding:0;} +h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,cite,code,del,dfn,em,img,q,s,samp,small,strike,strong,sub,sup,tt,var,dd,dl,dt,li,ol,ul,fieldset,form,label,legend,button,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;font-weight:normal;font-style:normal;font-size:100%;line-height:1;font-family:inherit;} +table{border-collapse:collapse;border-spacing:0;} +ol,ul{list-style:none;} +q:before,q:after,blockquote:before,blockquote:after{content:"";} +html{overflow-y:scroll;font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;} +a:focus{outline:thin dotted;} +a:hover,a:active{outline:0;} +article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;} +audio,canvas,video{display:inline-block;*display:inline;*zoom:1;} +audio:not([controls]){display:none;} +sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;} +sup{top:-0.5em;} +sub{bottom:-0.25em;} +img{border:0;-ms-interpolation-mode:bicubic;} +button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;} +button,input{line-height:normal;*overflow:visible;} +button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;} +button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;} +input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;} +input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;} +textarea{overflow:auto;vertical-align:top;} +body{background-color:#ffffff;margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:18px;color:#404040;} +.container{width:940px;margin-left:auto;margin-right:auto;zoom:1;}.container:before,.container:after{display:table;content:"";zoom:1;} +.container:after{clear:both;} +.container-fluid{position:relative;min-width:940px;padding-left:20px;padding-right:20px;zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";zoom:1;} +.container-fluid:after{clear:both;} +.container-fluid>.sidebar{position:absolute;top:0;left:20px;width:220px;} +.container-fluid>.content{margin-left:240px;} +a{color:#0069d6;text-decoration:none;line-height:inherit;font-weight:inherit;}a:hover{color:#00438a;text-decoration:underline;} +.pull-right{float:right;} +.pull-left{float:left;} +.hide{display:none;} +.show{display:block;} +.row{zoom:1;margin-left:-20px;}.row:before,.row:after{display:table;content:"";zoom:1;} +.row:after{clear:both;} +.row>[class*="span"]{display:inline;float:left;margin-left:20px;} +.span1{width:40px;} +.span2{width:100px;} +.span3{width:160px;} +.span4{width:220px;} +.span5{width:280px;} +.span6{width:340px;} +.span7{width:400px;} +.span8{width:460px;} +.span9{width:520px;} +.span10{width:580px;} +.span11{width:640px;} +.span12{width:700px;} +.span13{width:760px;} +.span14{width:820px;} +.span15{width:880px;} +.span16{width:940px;} +.span17{width:1000px;} +.span18{width:1060px;} +.span19{width:1120px;} +.span20{width:1180px;} +.span21{width:1240px;} +.span22{width:1300px;} +.span23{width:1360px;} +.span24{width:1420px;} +.row>.offset1{margin-left:80px;} +.row>.offset2{margin-left:140px;} +.row>.offset3{margin-left:200px;} +.row>.offset4{margin-left:260px;} +.row>.offset5{margin-left:320px;} +.row>.offset6{margin-left:380px;} +.row>.offset7{margin-left:440px;} +.row>.offset8{margin-left:500px;} +.row>.offset9{margin-left:560px;} +.row>.offset10{margin-left:620px;} +.row>.offset11{margin-left:680px;} +.row>.offset12{margin-left:740px;} +.span-one-third{width:300px;} +.span-two-thirds{width:620px;} +.row>.offset-one-third{margin-left:340px;} +.row>.offset-two-thirds{margin-left:660px;} +p{font-size:13px;font-weight:normal;line-height:18px;margin-bottom:9px;}p small{font-size:11px;color:#bfbfbf;} +h1,h2,h3,h4,h5,h6{font-weight:bold;color:#404040;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#bfbfbf;} +h1{margin-bottom:18px;font-size:30px;line-height:36px;}h1 small{font-size:18px;} +h2{font-size:24px;line-height:36px;}h2 small{font-size:14px;} +h3,h4,h5,h6{line-height:36px;} +h3{font-size:18px;}h3 small{font-size:14px;} +h4{font-size:16px;}h4 small{font-size:12px;} +h5{font-size:14px;} +h6{font-size:13px;color:#bfbfbf;text-transform:uppercase;} +ul,ol{margin:0 0 18px 25px;} +ul ul,ul ol,ol ol,ol ul{margin-bottom:0;} +ul{list-style:disc;} +ol{list-style:decimal;} +li{line-height:18px;color:#808080;} +ul.unstyled{list-style:none;margin-left:0;} +dl{margin-bottom:18px;}dl dt,dl dd{line-height:18px;} +dl dt{font-weight:bold;} +dl dd{margin-left:9px;} +hr{margin:20px 0 19px;border:0;border-bottom:1px solid #eee;} +strong{font-style:inherit;font-weight:bold;} +em{font-style:italic;font-weight:inherit;line-height:inherit;} +.muted{color:#bfbfbf;} +blockquote{margin-bottom:18px;border-left:5px solid #eee;padding-left:15px;}blockquote p{font-size:14px;font-weight:300;line-height:18px;margin-bottom:0;} +blockquote small{display:block;font-size:12px;font-weight:300;line-height:18px;color:#bfbfbf;}blockquote small:before{content:'\2014 \00A0';} +address{display:block;line-height:18px;margin-bottom:18px;} +code,pre{padding:0 3px 2px;font-family:Monaco, Andale Mono, Courier New, monospace;font-size:12px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +code{background-color:#fee9cc;color:rgba(0, 0, 0, 0.75);padding:1px 3px;} +pre{background-color:#f5f5f5;display:block;padding:8.5px;margin:0 0 18px;line-height:18px;font-size:12px;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;white-space:pre;white-space:pre-wrap;word-wrap:break-word;} +form{margin-bottom:18px;} +fieldset{margin-bottom:18px;padding-top:18px;}fieldset legend{display:block;padding-left:150px;font-size:19.5px;line-height:1;color:#404040;*padding:0 0 5px 145px;*line-height:1.5;} +form .clearfix{margin-bottom:18px;zoom:1;}form .clearfix:before,form .clearfix:after{display:table;content:"";zoom:1;} +form .clearfix:after{clear:both;} +label,input,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:normal;} +label{padding-top:6px;font-size:13px;line-height:18px;float:left;width:130px;text-align:right;color:#404040;} +form .input{margin-left:150px;} +input[type=checkbox],input[type=radio]{cursor:pointer;} +input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;font-size:13px;line-height:18px;color:#808080;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} +select{padding:initial;} +input[type=checkbox],input[type=radio]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;border:none;} +input[type=file]{background-color:#ffffff;padding:initial;border:initial;line-height:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +input[type=button],input[type=reset],input[type=submit]{width:auto;height:auto;} +select,input[type=file]{height:27px;*height:auto;line-height:27px;*margin-top:4px;} +select[multiple]{height:inherit;background-color:#ffffff;} +textarea{height:auto;} +.uneditable-input{background-color:#ffffff;display:block;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;} +:-moz-placeholder{color:#bfbfbf;} +::-webkit-input-placeholder{color:#bfbfbf;} +input,textarea{-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);} +input:focus,textarea:focus{outline:0;border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);} +input[type=file]:focus,input[type=checkbox]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:1px dotted #666;} +form .clearfix.error>label,form .clearfix.error .help-block,form .clearfix.error .help-inline{color:#b94a48;} +form .clearfix.error input,form .clearfix.error textarea{color:#b94a48;border-color:#ee5f5b;}form .clearfix.error input:focus,form .clearfix.error textarea:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;} +form .clearfix.error .input-prepend .add-on,form .clearfix.error .input-append .add-on{color:#b94a48;background-color:#fce6e6;border-color:#b94a48;} +form .clearfix.warning>label,form .clearfix.warning .help-block,form .clearfix.warning .help-inline{color:#c09853;} +form .clearfix.warning input,form .clearfix.warning textarea{color:#c09853;border-color:#ccae64;}form .clearfix.warning input:focus,form .clearfix.warning textarea:focus{border-color:#be9a3f;-webkit-box-shadow:0 0 6px #e5d6b1;-moz-box-shadow:0 0 6px #e5d6b1;box-shadow:0 0 6px #e5d6b1;} +form .clearfix.warning .input-prepend .add-on,form .clearfix.warning .input-append .add-on{color:#c09853;background-color:#d2b877;border-color:#c09853;} +form .clearfix.success>label,form .clearfix.success .help-block,form .clearfix.success .help-inline{color:#468847;} +form .clearfix.success input,form .clearfix.success textarea{color:#468847;border-color:#57a957;}form .clearfix.success input:focus,form .clearfix.success textarea:focus{border-color:#458845;-webkit-box-shadow:0 0 6px #9acc9a;-moz-box-shadow:0 0 6px #9acc9a;box-shadow:0 0 6px #9acc9a;} +form .clearfix.success .input-prepend .add-on,form .clearfix.success .input-append .add-on{color:#468847;background-color:#bcddbc;border-color:#468847;} +.input-mini,input.mini,textarea.mini,select.mini{width:60px;} +.input-small,input.small,textarea.small,select.small{width:90px;} +.input-medium,input.medium,textarea.medium,select.medium{width:150px;} +.input-large,input.large,textarea.large,select.large{width:210px;} +.input-xlarge,input.xlarge,textarea.xlarge,select.xlarge{width:270px;} +.input-xxlarge,input.xxlarge,textarea.xxlarge,select.xxlarge{width:530px;} +textarea.xxlarge{overflow-y:auto;} +input.span1,textarea.span1{display:inline-block;float:none;width:30px;margin-left:0;} +input.span2,textarea.span2{display:inline-block;float:none;width:90px;margin-left:0;} +input.span3,textarea.span3{display:inline-block;float:none;width:150px;margin-left:0;} +input.span4,textarea.span4{display:inline-block;float:none;width:210px;margin-left:0;} +input.span5,textarea.span5{display:inline-block;float:none;width:270px;margin-left:0;} +input.span6,textarea.span6{display:inline-block;float:none;width:330px;margin-left:0;} +input.span7,textarea.span7{display:inline-block;float:none;width:390px;margin-left:0;} +input.span8,textarea.span8{display:inline-block;float:none;width:450px;margin-left:0;} +input.span9,textarea.span9{display:inline-block;float:none;width:510px;margin-left:0;} +input.span10,textarea.span10{display:inline-block;float:none;width:570px;margin-left:0;} +input.span11,textarea.span11{display:inline-block;float:none;width:630px;margin-left:0;} +input.span12,textarea.span12{display:inline-block;float:none;width:690px;margin-left:0;} +input.span13,textarea.span13{display:inline-block;float:none;width:750px;margin-left:0;} +input.span14,textarea.span14{display:inline-block;float:none;width:810px;margin-left:0;} +input.span15,textarea.span15{display:inline-block;float:none;width:870px;margin-left:0;} +input.span16,textarea.span16{display:inline-block;float:none;width:930px;margin-left:0;} +input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;} +.actions{background:#f5f5f5;margin-top:18px;margin-bottom:18px;padding:17px 20px 18px 150px;border-top:1px solid #ddd;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}.actions .secondary-action{float:right;}.actions .secondary-action a{line-height:30px;}.actions .secondary-action a:hover{text-decoration:underline;} +.help-inline,.help-block{font-size:13px;line-height:18px;color:#bfbfbf;} +.help-inline{padding-left:5px;*position:relative;*top:-5px;} +.help-block{display:block;max-width:600px;} +.inline-inputs{color:#808080;}.inline-inputs span{padding:0 2px 0 1px;} +.input-prepend input,.input-append input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;} +.input-prepend .add-on,.input-append .add-on{position:relative;background:#f5f5f5;border:1px solid #ccc;z-index:2;float:left;display:block;width:auto;min-width:16px;height:18px;padding:4px 4px 4px 5px;margin-right:-1px;font-weight:normal;line-height:18px;color:#bfbfbf;text-align:center;text-shadow:0 1px 0 #ffffff;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-prepend .active,.input-append .active{background:#a9dba9;border-color:#46a546;} +.input-prepend .add-on{*margin-top:1px;} +.input-append input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;} +.input-append .add-on{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;margin-right:0;margin-left:-1px;} +.inputs-list{margin:0 0 5px;width:100%;}.inputs-list li{display:block;padding:0;width:100%;} +.inputs-list label{display:block;float:none;width:auto;padding:0;margin-left:20px;line-height:18px;text-align:left;white-space:normal;}.inputs-list label strong{color:#808080;} +.inputs-list label small{font-size:11px;font-weight:normal;} +.inputs-list .inputs-list{margin-left:25px;margin-bottom:10px;padding-top:0;} +.inputs-list:first-child{padding-top:6px;} +.inputs-list li+li{padding-top:2px;} +.inputs-list input[type=radio],.inputs-list input[type=checkbox]{margin-bottom:0;margin-left:-20px;float:left;} +.form-stacked{padding-left:20px;}.form-stacked fieldset{padding-top:9px;} +.form-stacked legend{padding-left:0;} +.form-stacked label{display:block;float:none;width:auto;font-weight:bold;text-align:left;line-height:20px;padding-top:0;} +.form-stacked .clearfix{margin-bottom:9px;}.form-stacked .clearfix div.input{margin-left:0;} +.form-stacked .inputs-list{margin-bottom:0;}.form-stacked .inputs-list li{padding-top:0;}.form-stacked .inputs-list li label{font-weight:normal;padding-top:0;} +.form-stacked div.clearfix.error{padding-top:10px;padding-bottom:10px;padding-left:10px;margin-top:0;margin-left:-10px;} +.form-stacked .actions{margin-left:-20px;padding-left:20px;} +table{width:100%;margin-bottom:18px;padding:0;font-size:13px;border-collapse:collapse;}table th,table td{padding:10px 10px 9px;line-height:18px;text-align:left;} +table th{padding-top:9px;font-weight:bold;vertical-align:middle;} +table td{vertical-align:top;border-top:1px solid #ddd;} +table tbody th{border-top:1px solid #ddd;vertical-align:top;} +.condensed-table th,.condensed-table td{padding:5px 5px 4px;} +.bordered-table{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.bordered-table th+th,.bordered-table td+td,.bordered-table th+td{border-left:1px solid #ddd;} +.bordered-table thead tr:first-child th:first-child,.bordered-table tbody tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;} +.bordered-table thead tr:first-child th:last-child,.bordered-table tbody tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;} +.bordered-table tbody tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;} +.bordered-table tbody tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;} +table .span1{width:20px;} +table .span2{width:60px;} +table .span3{width:100px;} +table .span4{width:140px;} +table .span5{width:180px;} +table .span6{width:220px;} +table .span7{width:260px;} +table .span8{width:300px;} +table .span9{width:340px;} +table .span10{width:380px;} +table .span11{width:420px;} +table .span12{width:460px;} +table .span13{width:500px;} +table .span14{width:540px;} +table .span15{width:580px;} +table .span16{width:620px;} +.zebra-striped tbody tr:nth-child(odd) td,.zebra-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;} +.zebra-striped tbody tr:hover td,.zebra-striped tbody tr:hover th{background-color:#f5f5f5;} +table .header{cursor:pointer;}table .header:after{content:"";float:right;margin-top:7px;border-width:0 4px 4px;border-style:solid;border-color:#000 transparent;visibility:hidden;} +table .headerSortUp,table .headerSortDown{background-color:rgba(141, 192, 219, 0.25);text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);} +table .header:hover:after{visibility:visible;} +table .headerSortDown:after,table .headerSortDown:hover:after{visibility:visible;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;} +table .headerSortUp:after{border-bottom:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000;visibility:visible;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;} +table .blue{color:#049cdb;border-bottom-color:#049cdb;} +table .headerSortUp.blue,table .headerSortDown.blue{background-color:#ade6fe;} +table .green{color:#46a546;border-bottom-color:#46a546;} +table .headerSortUp.green,table .headerSortDown.green{background-color:#cdeacd;} +table .red{color:#9d261d;border-bottom-color:#9d261d;} +table .headerSortUp.red,table .headerSortDown.red{background-color:#f4c8c5;} +table .yellow{color:#ffc40d;border-bottom-color:#ffc40d;} +table .headerSortUp.yellow,table .headerSortDown.yellow{background-color:#fff6d9;} +table .orange{color:#f89406;border-bottom-color:#f89406;} +table .headerSortUp.orange,table .headerSortDown.orange{background-color:#fee9cc;} +table .purple{color:#7a43b6;border-bottom-color:#7a43b6;} +table .headerSortUp.purple,table .headerSortDown.purple{background-color:#e2d5f0;} +.topbar{height:40px;position:fixed;top:0;left:0;right:0;z-index:10000;overflow:visible;}.topbar a{color:#bfbfbf;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);} +.topbar h3 a:hover,.topbar .brand:hover,.topbar ul .active>a{background-color:#333;background-color:rgba(255, 255, 255, 0.05);color:#ffffff;text-decoration:none;} +.topbar h3{position:relative;} +.topbar h3 a,.topbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;color:#ffffff;font-size:20px;font-weight:200;line-height:1;} +.topbar p{margin:0;line-height:40px;}.topbar p a:hover{background-color:transparent;color:#ffffff;} +.topbar form{float:left;margin:5px 0 0 0;position:relative;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;} +.topbar form.pull-right{float:right;} +.topbar input{background-color:#444;background-color:rgba(255, 255, 255, 0.3);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:normal;font-weight:13px;line-height:1;padding:4px 9px;color:#ffffff;color:rgba(255, 255, 255, 0.75);border:1px solid #111;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.topbar input:-moz-placeholder{color:#e6e6e6;} +.topbar input::-webkit-input-placeholder{color:#e6e6e6;} +.topbar input:hover{background-color:#bfbfbf;background-color:rgba(255, 255, 255, 0.5);color:#ffffff;} +.topbar input:focus,.topbar input.focused{outline:0;background-color:#ffffff;color:#404040;text-shadow:0 1px 0 #ffffff;border:0;padding:5px 10px;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);} +.topbar-inner,.topbar .fill{background-color:#222;background-color:#222222;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} +.topbar div>ul,.nav{display:block;float:left;margin:0 10px 0 0;position:relative;left:0;}.topbar div>ul>li,.nav>li{display:block;float:left;} +.topbar div>ul a,.nav a{display:block;float:none;padding:10px 10px 11px;line-height:19px;text-decoration:none;}.topbar div>ul a:hover,.nav a:hover{color:#ffffff;text-decoration:none;} +.topbar div>ul .active>a,.nav .active>a{background-color:#222;background-color:rgba(0, 0, 0, 0.5);} +.topbar div>ul.secondary-nav,.nav.secondary-nav{float:right;margin-left:10px;margin-right:0;}.topbar div>ul.secondary-nav .menu-dropdown,.nav.secondary-nav .menu-dropdown,.topbar div>ul.secondary-nav .dropdown-menu,.nav.secondary-nav .dropdown-menu{right:0;border:0;} +.topbar div>ul a.menu:hover,.nav a.menu:hover,.topbar div>ul li.open .menu,.nav li.open .menu,.topbar div>ul .dropdown-toggle:hover,.nav .dropdown-toggle:hover,.topbar div>ul .dropdown.open .dropdown-toggle,.nav .dropdown.open .dropdown-toggle{background:#444;background:rgba(255, 255, 255, 0.05);} +.topbar div>ul .menu-dropdown,.nav .menu-dropdown,.topbar div>ul .dropdown-menu,.nav .dropdown-menu{background-color:#333;}.topbar div>ul .menu-dropdown a.menu,.nav .menu-dropdown a.menu,.topbar div>ul .dropdown-menu a.menu,.nav .dropdown-menu a.menu,.topbar div>ul .menu-dropdown .dropdown-toggle,.nav .menu-dropdown .dropdown-toggle,.topbar div>ul .dropdown-menu .dropdown-toggle,.nav .dropdown-menu .dropdown-toggle{color:#ffffff;}.topbar div>ul .menu-dropdown a.menu.open,.nav .menu-dropdown a.menu.open,.topbar div>ul .dropdown-menu a.menu.open,.nav .dropdown-menu a.menu.open,.topbar div>ul .menu-dropdown .dropdown-toggle.open,.nav .menu-dropdown .dropdown-toggle.open,.topbar div>ul .dropdown-menu .dropdown-toggle.open,.nav .dropdown-menu .dropdown-toggle.open{background:#444;background:rgba(255, 255, 255, 0.05);} +.topbar div>ul .menu-dropdown li a,.nav .menu-dropdown li a,.topbar div>ul .dropdown-menu li a,.nav .dropdown-menu li a{color:#999;text-shadow:0 1px 0 rgba(0, 0, 0, 0.5);}.topbar div>ul .menu-dropdown li a:hover,.nav .menu-dropdown li a:hover,.topbar div>ul .dropdown-menu li a:hover,.nav .dropdown-menu li a:hover{background-color:#191919;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#292929), to(#191919));background-image:-moz-linear-gradient(top, #292929, #191919);background-image:-ms-linear-gradient(top, #292929, #191919);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #292929), color-stop(100%, #191919));background-image:-webkit-linear-gradient(top, #292929, #191919);background-image:-o-linear-gradient(top, #292929, #191919);background-image:linear-gradient(top, #292929, #191919);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#292929', endColorstr='#191919', GradientType=0);color:#ffffff;} +.topbar div>ul .menu-dropdown .active a,.nav .menu-dropdown .active a,.topbar div>ul .dropdown-menu .active a,.nav .dropdown-menu .active a{color:#ffffff;} +.topbar div>ul .menu-dropdown .divider,.nav .menu-dropdown .divider,.topbar div>ul .dropdown-menu .divider,.nav .dropdown-menu .divider{background-color:#222;border-color:#444;} +.topbar ul .menu-dropdown li a,.topbar ul .dropdown-menu li a{padding:4px 15px;} +li.menu,.dropdown{position:relative;} +a.menu:after,.dropdown-toggle:after{width:0;height:0;display:inline-block;content:"↓";text-indent:-99999px;vertical-align:top;margin-top:8px;margin-left:4px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #ffffff;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;} +.menu-dropdown,.dropdown-menu{background-color:#ffffff;float:left;display:none;position:absolute;top:40px;z-index:900;min-width:160px;max-width:220px;_width:160px;margin-left:0;margin-right:0;padding:6px 0;zoom:1;border-color:#999;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:0 1px 1px;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.menu-dropdown li,.dropdown-menu li{float:none;display:block;background-color:none;} +.menu-dropdown .divider,.dropdown-menu .divider{height:1px;margin:5px 0;overflow:hidden;background-color:#eee;border-bottom:1px solid #ffffff;} +.topbar .dropdown-menu a,.dropdown-menu a{display:block;padding:4px 15px;clear:both;font-weight:normal;line-height:18px;color:#808080;text-shadow:0 1px 0 #ffffff;}.topbar .dropdown-menu a:hover,.dropdown-menu a:hover,.topbar .dropdown-menu a.hover,.dropdown-menu a.hover{background-color:#dddddd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#eeeeee), to(#dddddd));background-image:-moz-linear-gradient(top, #eeeeee, #dddddd);background-image:-ms-linear-gradient(top, #eeeeee, #dddddd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, #dddddd));background-image:-webkit-linear-gradient(top, #eeeeee, #dddddd);background-image:-o-linear-gradient(top, #eeeeee, #dddddd);background-image:linear-gradient(top, #eeeeee, #dddddd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0);color:#404040;text-decoration:none;-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);} +.open .menu,.dropdown.open .menu,.open .dropdown-toggle,.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);} +.open .menu-dropdown,.dropdown.open .menu-dropdown,.open .dropdown-menu,.dropdown.open .dropdown-menu{display:block;} +.tabs,.pills{margin:0 0 18px;padding:0;list-style:none;zoom:1;}.tabs:before,.pills:before,.tabs:after,.pills:after{display:table;content:"";zoom:1;} +.tabs:after,.pills:after{clear:both;} +.tabs>li,.pills>li{float:left;}.tabs>li>a,.pills>li>a{display:block;} +.tabs{border-color:#ddd;border-style:solid;border-width:0 0 1px;}.tabs>li{position:relative;margin-bottom:-1px;}.tabs>li>a{padding:0 15px;margin-right:2px;line-height:34px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.tabs>li>a:hover{text-decoration:none;background-color:#eee;border-color:#eee #eee #ddd;} +.tabs .active>a,.tabs .active>a:hover{color:#808080;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;} +.tabs .menu-dropdown,.tabs .dropdown-menu{top:35px;border-width:1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;} +.tabs a.menu:after,.tabs .dropdown-toggle:after{border-top-color:#999;margin-top:15px;margin-left:5px;} +.tabs li.open.menu .menu,.tabs .open.dropdown .dropdown-toggle{border-color:#999;} +.tabs li.open a.menu:after,.tabs .dropdown.open .dropdown-toggle:after{border-top-color:#555;} +.pills a{margin:5px 3px 5px 0;padding:0 15px;line-height:30px;text-shadow:0 1px 1px #ffffff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}.pills a:hover{color:#ffffff;text-decoration:none;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#00438a;} +.pills .active a{color:#ffffff;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#0069d6;} +.pills-vertical>li{float:none;} +.tab-content>.tab-pane,.pill-content>.pill-pane,.tab-content>div,.pill-content>div{display:none;} +.tab-content>.active,.pill-content>.active{display:block;} +.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#f5f5f5;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5));background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline;text-shadow:0 1px 0 #ffffff;} +.breadcrumb .divider{padding:0 5px;color:#bfbfbf;} +.breadcrumb .active a{color:#404040;} +.hero-unit{background-color:#f5f5f5;margin-bottom:30px;padding:60px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;} +.hero-unit p{font-size:18px;font-weight:200;line-height:27px;} +footer{margin-top:17px;padding-top:17px;border-top:1px solid #eee;} +.page-header{margin-bottom:17px;border-bottom:1px solid #ddd;-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}.page-header h1{margin-bottom:8px;} +.btn.danger,.alert-message.danger,.btn.danger:hover,.alert-message.danger:hover,.btn.error,.alert-message.error,.btn.error:hover,.alert-message.error:hover,.btn.success,.alert-message.success,.btn.success:hover,.alert-message.success:hover,.btn.info,.alert-message.info,.btn.info:hover,.alert-message.info:hover{color:#ffffff;} +.btn .close,.alert-message .close{font-family:Arial,sans-serif;line-height:18px;} +.btn.danger,.alert-message.danger,.btn.error,.alert-message.error{background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} +.btn.success,.alert-message.success{background-color:#57a957;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#57a957 #57a957 #3d773d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} +.btn.info,.alert-message.info{background-color:#339bb9;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#339bb9 #339bb9 #22697d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} +.btn{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;}.btn:hover{background-position:0 -15px;color:#333;text-decoration:none;} +.btn:focus{outline:1px dotted #666;} +.btn.primary{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb, #0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);} +.btn.active,.btn:active{-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);} +.btn.disabled{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn[disabled]{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} +.btn.large{font-size:15px;line-height:normal;padding:9px 14px 9px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;} +.btn.small{padding:7px 9px 7px;font-size:11px;} +:root .alert-message,:root .btn{border-radius:0 \0;} +button.btn::-moz-focus-inner,input[type=submit].btn::-moz-focus-inner{padding:0;border:0;} +.close{float:right;color:#000000;font-size:20px;font-weight:bold;line-height:13.5px;text-shadow:0 1px 0 #ffffff;filter:alpha(opacity=25);-khtml-opacity:0.25;-moz-opacity:0.25;opacity:0.25;}.close:hover{color:#000000;text-decoration:none;filter:alpha(opacity=40);-khtml-opacity:0.4;-moz-opacity:0.4;opacity:0.4;} +.alert-message{position:relative;padding:7px 15px;margin-bottom:18px;color:#404040;background-color:#eedc94;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));background-image:-moz-linear-gradient(top, #fceec1, #eedc94);background-image:-ms-linear-gradient(top, #fceec1, #eedc94);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));background-image:-webkit-linear-gradient(top, #fceec1, #eedc94);background-image:-o-linear-gradient(top, #fceec1, #eedc94);background-image:linear-gradient(top, #fceec1, #eedc94);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#eedc94 #eedc94 #e4c652;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);border-width:1px;border-style:solid;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);}.alert-message .close{margin-top:1px;*margin-top:0;} +.alert-message a{font-weight:bold;color:#404040;} +.alert-message.danger p a,.alert-message.error p a,.alert-message.success p a,.alert-message.info p a{color:#ffffff;} +.alert-message h5{line-height:18px;} +.alert-message p{margin-bottom:0;} +.alert-message div{margin-top:5px;margin-bottom:2px;line-height:28px;} +.alert-message .btn{-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);} +.alert-message.block-message{background-image:none;background-color:#fdf5d9;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);padding:14px;border-color:#fceec1;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.alert-message.block-message ul,.alert-message.block-message p{margin-right:30px;} +.alert-message.block-message ul{margin-bottom:0;} +.alert-message.block-message li{color:#404040;} +.alert-message.block-message .alert-actions{margin-top:5px;} +.alert-message.block-message.error,.alert-message.block-message.success,.alert-message.block-message.info{color:#404040;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);} +.alert-message.block-message.error{background-color:#fddfde;border-color:#fbc7c6;} +.alert-message.block-message.success{background-color:#d1eed1;border-color:#bfe7bf;} +.alert-message.block-message.info{background-color:#ddf4fb;border-color:#c6edf9;} +.alert-message.block-message.danger p a,.alert-message.block-message.error p a,.alert-message.block-message.success p a,.alert-message.block-message.info p a{color:#404040;} +.pagination{height:36px;margin:18px 0;}.pagination ul{float:left;margin:0;border:1px solid #ddd;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);} +.pagination li{display:inline;} +.pagination a{float:left;padding:0 14px;line-height:34px;border-right:1px solid;border-right-color:#ddd;border-right-color:rgba(0, 0, 0, 0.15);*border-right-color:#ddd;text-decoration:none;} +.pagination a:hover,.pagination .active a{background-color:#c7eefe;} +.pagination .disabled a,.pagination .disabled a:hover{background-color:transparent;color:#bfbfbf;} +.pagination .next a{border:0;} +.well{background-color:#f5f5f5;margin-bottom:20px;padding:19px;min-height:20px;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);} +.modal-backdrop{background-color:#000000;position:fixed;top:0;left:0;right:0;bottom:0;z-index:10000;}.modal-backdrop.fade{opacity:0;} +.modal-backdrop,.modal-backdrop.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;} +.modal{position:fixed;top:50%;left:50%;z-index:11000;max-height:500px;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal .close{margin-top:7px;} +.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;} +.modal.fade.in{top:50%;} +.modal-header{border-bottom:1px solid #eee;padding:5px 15px;} +.modal-body{padding:15px;} +.modal-body form{margin-bottom:0;} +.modal-footer{background-color:#f5f5f5;padding:14px 15px 15px;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;zoom:1;margin-bottom:0;}.modal-footer:before,.modal-footer:after{display:table;content:"";zoom:1;} +.modal-footer:after{clear:both;} +.modal-footer .btn{float:right;margin-left:5px;} +.modal .popover,.modal .twipsy{z-index:12000;} +.twipsy{display:block;position:absolute;visibility:visible;padding:5px;font-size:11px;z-index:1000;filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}.twipsy.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;} +.twipsy.above .twipsy-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.twipsy.left .twipsy-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.twipsy.below .twipsy-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.twipsy.right .twipsy-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.twipsy-inner{padding:3px 8px;background-color:#000000;color:white;text-align:center;max-width:200px;text-decoration:none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} +.twipsy-arrow{position:absolute;width:0;height:0;} +.popover{position:absolute;top:0;left:0;z-index:1000;padding:5px;display:none;}.popover.above .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;} +.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;} +.popover.below .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;} +.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;} +.popover .arrow{position:absolute;width:0;height:0;} +.popover .inner{background:#000000;background:rgba(0, 0, 0, 0.8);padding:3px;overflow:hidden;width:280px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);} +.popover .title{background-color:#f5f5f5;padding:9px 15px;line-height:1;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;border-bottom:1px solid #eee;} +.popover .content{background-color:#ffffff;padding:14px;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover .content p,.popover .content ul,.popover .content ol{margin-bottom:0;} +.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;} +.label{padding:1px 3px 2px;font-size:9.75px;font-weight:bold;color:#ffffff;text-transform:uppercase;white-space:nowrap;background-color:#bfbfbf;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;text-shadow:none;}.label.important{background-color:#c43c35;} +.label.warning{background-color:#f89406;} +.label.success{background-color:#46a546;} +.label.notice{background-color:#62cffc;} +.media-grid{margin-left:-20px;margin-bottom:0;zoom:1;}.media-grid:before,.media-grid:after{display:table;content:"";zoom:1;} +.media-grid:after{clear:both;} +.media-grid li{display:inline;} +.media-grid a{float:left;padding:4px;margin:0 0 18px 20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}.media-grid a img{display:block;} +.media-grid a:hover{border-color:#0069d6;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);} diff --git a/rpkid/portal-gui/media/img/my.png b/rpkid/portal-gui/media/img/my.png Binary files differdeleted file mode 100644 index c4d1378e..00000000 --- a/rpkid/portal-gui/media/img/my.png +++ /dev/null diff --git a/rpkid/portal-gui/media/img/rpki.png b/rpkid/portal-gui/media/img/rpki.png Binary files differdeleted file mode 100644 index 31351980..00000000 --- a/rpkid/portal-gui/media/img/rpki.png +++ /dev/null diff --git a/rpkid/portal-gui/rpki.wsgi.in b/rpkid/portal-gui/rpki.wsgi.in new file mode 100644 index 00000000..eb49fe05 --- /dev/null +++ b/rpkid/portal-gui/rpki.wsgi.in @@ -0,0 +1,54 @@ +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +# This is an example wsgi application for use with mod_wsgi and apache. + +__version__ = '$Id$' + +VIRTUAL_ENV = '@VIRTUAL_ENV@' + +import os +import os.path +import sys + +old_sys_path = list(sys.path) + +# When used with virtualenv, specify the location of the python modules to use +if VIRTUAL_ENV: + import site + # locate the site-packages directory + for (dp, dn, fn) in os.walk(VIRTUAL_ENV + '/lib'): + if 'site-packages' in dn: + site.addsitedir(os.path.join(dp, 'site-packages')) + break + +import sys +os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' +sys.path.insert(1, '@PYTHONPATH@') + +# reorder sys.path to place newly added directories at the head of the path. +# this is necessary so that the packages in the virtualenv site-packages are +# used rather than the system site-packages. +new_sys_path = [] +for elt in list(sys.path): + if elt not in old_sys_path: + new_sys_path.append(elt) + sys.path.remove(elt) +sys.path[:0] = new_sys_path + +import django.core.handlers.wsgi +application = django.core.handlers.wsgi.WSGIHandler() + +# vim:ft=python diff --git a/rpkid/portal-gui/scripts/adduser.py b/rpkid/portal-gui/scripts/adduser.py deleted file mode 100644 index b06c6bc4..00000000 --- a/rpkid/portal-gui/scripts/adduser.py +++ /dev/null @@ -1,86 +0,0 @@ -# $Id$ -# -# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# -# Helper script to quickly set up a new portal-gui user/handle. This script -# is designed to be safe to run multiple times for the same user. -# -# DO NOT EDIT! This script is automatically generated from adduser.py - -from django.contrib.auth.models import User -from rpki.gui.app import settings -from rpki.gui.app.models import Conf - -import getpass -import pwd - -# The username that apache runs as. This is required so that we can chown -# the csv files that the portal-gui needs to write. -web_uid = pwd.getpwnam(settings.WEB_USER)[2] - -if __name__ == '__main__': - if len(sys.argv) < 3: - print >>sys.stderr, 'usage: adduser <username> <user\'s email> <host handle>' - sys.exit(1) - - if os.getuid() != 0: - print >>sys.stderr, 'error: this script must be run as root so it can set file permissions.' - sys.exit(1) - - username = sys.argv[1] - email = sys.argv[2] - host = sys.argv[3] - print 'username=', username, 'email=', email, 'host=', host - - user_set = User.objects.filter(username=username) - if user_set: - print >>sys.stderr, 'user already exists' - user = user_set[0] - else: - print >>sys.stderr, 'creating user' - password = getpass.getpass() - user = User.objects.create_user(username, email, password) - - conf_set = Conf.objects.filter(handle=username) - if conf_set: - conf = conf_set[0] - else: - print >>sys.stderr, 'creating conf' - conf = Conf.objects.create(handle=username) - - # always try to add the user as owner just in case the Conf object was - # created previously by the "list_resources" script - conf.owner.add(user) - - if host != username: - host_set = Conf.objects.filter(handle=host) - if not host_set: - print >>sys.stderr, 'error: Conf object for host %s does not exist!' % host - sys.exit(1) - - conf.host = host_set[0] - else: - print >>sys.stderr, '%s is self-hosted' % username - conf.save() - - myrpki_dir = '%s/%s' % (settings.CONFDIR, username) - print 'myrpki_dir=', myrpki_dir - if not os.path.exists(myrpki_dir): - print 'creating ', myrpki_dir - os.mkdir(myrpki_dir) - os.chown(myrpki_dir, web_uid, -1) - -# vim:sw=4 ts=8 diff --git a/rpkid/portal-gui/scripts/dumpdata.py b/rpkid/portal-gui/scripts/dumpdata.py new file mode 100755 index 00000000..dcf23666 --- /dev/null +++ b/rpkid/portal-gui/scripts/dumpdata.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# $Id$ +# +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +# This is a helper script which will dump the rpki.gui.app models from +# the old sqlite3 database, forcing the output order to the primary key in +# order to avoid forward references for the AddressRange table. + +from django.conf import settings +settings.configure(DEBUG=True, + DATABASES={ + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': '/usr/local/var/rpki/gui.db', + } + }) + +from django.core import serializers +import django.db.models + +from rpki.gui.app import models +from django.contrib.auth import models as auth_models + +data = [] +for v in (auth_models.User, models.Conf, models.Parent, models.Child, models.AddressRange, models.Asn, models.ResourceCert, models.Roa, models.RoaRequest, models.Ghostbuster): + data.extend(list(v.objects.all().order_by('id'))) + +print serializers.serialize('json', data, use_natural_keys=True) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/portal-gui/scripts/list_resources.py b/rpkid/portal-gui/scripts/list_resources.py deleted file mode 100644 index 13864705..00000000 --- a/rpkid/portal-gui/scripts/list_resources.py +++ /dev/null @@ -1,200 +0,0 @@ -# $Id$ -# -# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# -# This script is reponsible for talking to rpkid and populating the -# portal-gui's sqlite database. It asks rpkid for the list of received -# resources, and the handles of any children. -# -# This script should be run in the directory containing the rpki.conf -# for the handle that is self-hosting rpkid. -# -# Exit values: -# 0 success, no errors -# 1 fatal error -# 2 usage error -# 3 did not receive all <list_received_resources/> responses, try again -# later - -import sys - -import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'rpki.gui.settings' - -import getopt -from datetime import datetime, timedelta -from os.path import basename - -import rpki.config, rpki.left_right, rpki.resource_set -from rpki.gui.app import models, glue - -verbose = False -version = '$Id$' - -def query_rpkid(): - """ - Fetch our received resources from the local rpkid using the rpki.conf - in the current directory. - """ - cfg = rpki.config.parser(section='myrpki') - call_rpkid = glue.build_rpkid_caller(cfg, verbose) - - if verbose: - print 'retrieving the list of <self/> handles served by this rpkid' - rpkid_reply = call_rpkid(rpki.left_right.self_elt.make_pdu(action="list")) - - # retrieve info about each handle - pdus = [] - handles = [] - for h in rpkid_reply: - assert isinstance(h, rpki.left_right.self_elt) - if verbose: - print 'adding handle %s to query' % (h.self_handle,) - # keep a list of the handles served by rpkid so that we may check that - # all expected responses are received. - handles.append(h.self_handle) - pdus.extend( - [rpki.left_right.child_elt.make_pdu(action="list", self_handle=h.self_handle), - rpki.left_right.list_received_resources_elt.make_pdu(self_handle=h.self_handle) - #rpki.left_right.parent_elt.make_pdu(action="list", tag="parents", self_handle=handle), - #rpki.left_right.list_roa_requests_elt.make_pdu(tag='roas', self_handle=handle), - ]) - - if verbose: - print 'querying for children and resources' - return handles, call_rpkid(*pdus) - -def usage(rc): - print 'usage: %s [ -hvV ] [ --help ] [ --verbose ] [ --version ]' % basename(sys.argv[0],) - sys.exit(rc) - -try: - opts, args = getopt.getopt(sys.argv[1:], 'hvV', [ 'help', 'verbose', 'version']) -except getopt.GetoptError, err: - print str(err) - usage(2) - -for o,a in opts: - if o in ('-h', '--help'): - usage(0) - elif o in ('-v', '--verbose'): - verbose = True - elif o in ('-V', '--version'): - print basename(sys.argv[0]), version - sys.exit(0) - -handles, pdus = query_rpkid() -seen = set() # which handles we got <list_received_resources/> responses -for pdu in pdus: - conf_set = models.Conf.objects.filter(handle=pdu.self_handle) - if conf_set.count(): - conf = conf_set[0] - else: - if verbose: - print 'creating new conf for %s' % (pdu.self_handle,) - conf = models.Conf.objects.create(handle=pdu.self_handle) - - #if isinstance(pdu, rpki.left_right.parent_elt): -# print x.parent_handle, x.sia_base, x.sender_name, x.recipient_name, \ -# x.peer_contact_uri - if isinstance(pdu, rpki.left_right.child_elt): - # have we seen this child before? - child_set = conf.children.filter(handle=pdu.child_handle) - if not child_set: - if verbose: - print 'creating new child %s' % (pdu.child_handle,) - # default to 1 year. no easy way to query irdb for the - # current value. - valid_until = datetime.now() + timedelta(days=365) - child = models.Child(conf=conf, handle=pdu.child_handle, - valid_until=valid_until) - child.save() - #elif isinstance(x, rpki.left_right.list_roa_requests_elt): - # print x.asn, x.ipv4, x.ipv6 - elif isinstance(pdu, rpki.left_right.list_received_resources_elt): - # keep track of handles we got replies for - seen.add(pdu.self_handle) - # have we seen this parent before? - parent_set = conf.parents.filter(handle=pdu.parent_handle) - if not parent_set: - if verbose: - print 'creating new parent %s' % (pdu.parent_handle,) - parent = models.Parent(conf=conf, handle=pdu.parent_handle) - parent.save() - else: - parent = parent_set[0] - - not_before = datetime.strptime(pdu.notBefore, "%Y-%m-%dT%H:%M:%SZ") - not_after = datetime.strptime(pdu.notAfter, "%Y-%m-%dT%H:%M:%SZ") - - # have we seen this resource cert before? - cert_set = parent.resources.filter(uri=pdu.uri) - if cert_set.count() == 0: - cert = models.ResourceCert(uri=pdu.uri, parent=parent, - not_before=not_before, not_after=not_after) - else: - cert = cert_set[0] - # update timestamps since it could have been modified - cert.not_before = not_before - cert.not_after = not_after - cert.save() - - for asn in rpki.resource_set.resource_set_as(pdu.asn): - # see if this resource is already part of the cert - if cert.asn.filter(lo=asn.min, hi=asn.max).count() == 0: - # ensure this range wasn't seen from another of our parents - for v in models.Asn.objects.filter(lo=asn.min, hi=asn.max): - # determine if resource is delegated from another parent - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.asn.add(v) - break - else: - if verbose: - print 'adding AS %s' % (asn,) - cert.asn.create(lo=asn.min, hi=asn.max) - cert.save() - - # IPv4/6 - not separated in the django db - def add_missing_address(addr_set): - for ip in addr_set: - lo=str(ip.min) - hi=str(ip.max) - if cert.address_range.filter(lo=lo, hi=hi).count() == 0: - # ensure that this range wasn't previously seen from another of our parents - for v in models.AddressRange.objects.filter(lo=lo, hi=hi): - # determine if this resource is delegated from another parent as well - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.address_range.add(v) - break - else: - if verbose: - print 'adding address range %s' % (ip,) - cert.address_range.create(lo=lo, hi=hi) - cert.save() - - add_missing_address(rpki.resource_set.resource_set_ipv4(pdu.ipv4)) - add_missing_address(rpki.resource_set.resource_set_ipv6(pdu.ipv6)) - -# verify that we got responses for all expected handles -for h in handles: - if h not in seen: - if verbose: - print 'warning: did not receive response for handle %s' % (h,) - sys.exit(3) - -sys.exit(0) # success - -# vim:sw=4 expandtab ts=4 diff --git a/rpkid/portal-gui/scripts/load_csv.py b/rpkid/portal-gui/scripts/load_csv.py deleted file mode 100755 index 9d4fc1ac..00000000 --- a/rpkid/portal-gui/scripts/load_csv.py +++ /dev/null @@ -1,143 +0,0 @@ -# $Id$ -# -# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# -# Helper script to load existing data from csv into the Django DB. -# Primarly useful for the initial load, as the GUI does not sync changes -# made directly to the csv files back into the database. -# -# This script should be run from the directory containing the rpki.conf -# for the handle you are loading data -# - -import csv -import socket # for socket.error - -import rpki.resource_set, rpki.ipaddrs -from rpki.myrpki import csv_reader -from rpki.gui.app import models -from rpki.gui.app.views import add_roa_requests - -cfg = rpki.config.parser(section='myrpki') -handle = cfg.get('handle') -asn_csv = cfg.get('asn_csv') -prefix_csv = cfg.get('prefix_csv') -roa_csv = cfg.get('roa_csv') - -print 'processing csv files for resource handle', handle - -conf = models.Conf.objects.get(handle=handle) - -class RangeError(Exception): - """ - Problem with ASN range or address range. - """ - -# every parent has a favorite -def best_child(address_range, parent, parent_range): - '''Return the child address range that is the closest match, or - returns the arguments if no children.''' - if address_range == parent_range: - return (parent, parent_range) - for q in list(parent.children.all()): # force strict evaluation - t = q.as_resource_range() - if t.min <= address_range.min and t.max >= address_range.max: - return best_child(address_range, q, t) - # check for overlap - if t.min <= address_range.min <= t.max or t.min <= address_range.max <= t.max: - raise RangeError, \ - 'can not handle overlapping ranges: %s and %s' % (address_range, t) - return parent, parent_range - -def get_or_create_prefix(address_range): - '''Returns a AddressRange object for the resource_range_ip specified - as an argument. If no match is found, a new AddressRange object is - created as a child of the best matching received resource.''' - - # get all resources from our parents - prefix_set = models.AddressRange.objects.filter( - from_cert__parent__in=conf.parents.all()) - - # gross, since we store the address ranges as strings in the django - # db, we can't use the normal __lte and __gte filters, so we get to - # do it in python instead. - for prefix in prefix_set: - prefix_range = prefix.as_resource_range() - if (prefix_range.min <= address_range.min and - prefix_range.max >= address_range.max): - # there should only ever be a single matching prefix - break - else: - raise RangeError, '%s does not match any received address range.' % ( - address_range,) - - # find the best match among the children + grandchildren - prefix, prefix_range = best_child(address_range, prefix, prefix_range) - - print 'best match for %s is %s' % (address_range, prefix) - if prefix_range.min != address_range.min or prefix_range.max != address_range.max: - # create suballocation - print 'creating new range' - prefix = models.AddressRange.objects.create(lo=str(address_range.min), - hi=str(address_range.max), parent=prefix) - return prefix - -def get_or_create_asn(asn): - asn_set = models.Asn.objects.filter(lo__lte=asn.min, hi__gte=asn.max, - from_cert__parent__in=conf.parents.all()) - if not asn_set: - raise RangeError, '%s does not match any received AS range' % (asn,) - best = best_child(asn, asn_set[0], asn_set[0].as_resource_range())[0] - print 'best match for %s is %s' % (asn, best) - if best.lo != asn.min or best.hi != asn.max: - best = models.Asn.objects.create(lo=asn.min, hi=asn.max, parent=best) - return best - -def do_asns(): - print 'processing', asn_csv - for child_handle, asn in csv_reader(asn_csv, columns=2): - asn_range = rpki.resource_set.resource_range_as.parse_str(asn) - child = conf.children.get(handle=child_handle) - asn = get_or_create_asn(asn_range) - child.asn.add(asn) - -def do_prefixes(): - print 'processing', prefix_csv - for child_handle, prefix in csv_reader(prefix_csv, columns=2): - child = conf.children.get(handle=child_handle) - try: - rs = rpki.resource_set.resource_range_ipv4.parse_str(prefix) - except ValueError, err: - rs = rpki.resource_set.resource_range_ipv6.parse_str(prefix) - obj = get_or_create_prefix(rs) - obj.allocated = child - obj.save() - -def do_roas(): - print 'processing', roa_csv - for prefix, asn, group in csv_reader(roa_csv, columns=3): - try: - rs = rpki.resource_set.roa_prefix_ipv4.parse_str(prefix) - except ValueError, err: - rs = rpki.resource_set.roa_prefix_ipv6.parse_str(prefix) - - print str(rs.min()), str(rs.max()), rs.max_prefixlen - obj = get_or_create_prefix(rs.to_resource_range()) - add_roa_requests(conf, obj, [int(asn)], rs.max_prefixlen) - -do_asns() -do_prefixes() -do_roas() diff --git a/rpkid/portal-gui/scripts/roa_check.py b/rpkid/portal-gui/scripts/roa_check.py deleted file mode 100755 index c280d935..00000000 --- a/rpkid/portal-gui/scripts/roa_check.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python -# $Id$ -# -# Runs through all the published ROAs and updates the Django DB with the -# current active status of each defined ROA. -# - -import socket - -from rcynic_output_iterator import rcynic_xml_iterator, rcynic_roa -from rpki.resource_set import resource_set_ipv4, resource_set_ipv6 -from rpki.resource_set import roa_prefix_set_ipv4, roa_prefix_set_ipv6 -from rpki.resource_set import resource_range_ipv4, resource_range_ipv6 -from rpki.ipaddrs import v4addr, v6addr - -from rpki.gui.app.models import Roa - -# build up a list of all the authenticated roa's using the asn as the key -roaiter = rcynic_xml_iterator( - rcynic_root='/home/melkins/rcynic/rcynic-data/', - xml_file='/home/melkins/rcynic/rcynic.xml') - -# key is an ASN -# each element is a tuple of (resource_set_ipv4, resource_set_ipv6) -roaauth = {} - -for roa in roaiter: - if isinstance(roa, rcynic_roa): - k = roa.asID - if not roaauth.has_key(k): - v = [resource_set_ipv4(), resource_set_ipv6()] - roaauth[k] = v - else: - v = roaauth[k] - for pfx in roa.prefix_sets: - if isinstance(pfx, roa_prefix_set_ipv4): - v[0] = v[0].union(pfx.to_resource_set()) - elif isinstance(pfx, roa_prefix_set_ipv6): - v[1] = v[1].union(pfx.to_resource_set()) - -#for k, v in roaauth.iteritems(): -# print 'asn %d : prefixes %s' % (k, ' '.join(map(str,v))) - -# run through all the ROA's in the GUI's database -for roa in Roa.objects.all(): - k = int(roa.asn) - valid = False - if roaauth.has_key(k): - # ensure that all prefixes listed in the roa are present - # we convert the list of prefixes into prefix sets and use the - # resource_set class to perform set comparisons - ipv4_set = resource_set_ipv4() - ipv6_set = resource_set_ipv6() - for pfx in roa.prefix.all(): - # IP addresses are just stored as strings in the sqlite db - try: - ipv4_set.append(resource_range_ipv4(v4addr(str(pfx.lo)), v4addr(str(pfx.hi)))) - except socket.error: - ipv6_set.append(resource_range_ipv6(v6addr(str(pfx.lo)), v6addr(str(pfx.hi)))) - r = roaauth[k] - if ipv4_set.issubset(r[0]) and ipv6_set.issubset(r[1]): - valid = True - if valid: - if not roa.active: - roa.active = True - roa.save() - else: - print 'roa for asn %s is not valid' % (roa.asn, ) - if roa.active: - roa.active = False - roa.save() diff --git a/rpkid/portal-gui/scripts/rpkigui-import-routes.py b/rpkid/portal-gui/scripts/rpkigui-import-routes.py new file mode 100644 index 00000000..9a3748d3 --- /dev/null +++ b/rpkid/portal-gui/scripts/rpkigui-import-routes.py @@ -0,0 +1,250 @@ +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' + +import itertools +import _mysql_exceptions +import optparse +import os.path +import re +import sys +import struct +import subprocess +import time +import logging + +from django.db import transaction, connection + +from rpki.resource_set import resource_range_ipv4, resource_range_ipv6 +from rpki.exceptions import BadIPResource +import rpki.gui.app.timestamp + +# globals +BGPDUMP = 'bgpdump' +logger = logging.getLogger(__name__) + + +def parse_text(f): + ip_re = re.compile(r'^[0-9a-fA-F:.]+/\d{1,3}$') + last_prefix = None + cursor = connection.cursor() + range_class = resource_range_ipv4 + table = 'routeview_routeorigin' + sql = "INSERT INTO %s_new SET asn=%%s, prefix_min=%%s, prefix_max=%%s" % table + + try: + logger.info('Dropping existing staging table...') + cursor.execute('DROP TABLE IF EXISTS %s_new' % table) + except _mysql_exceptions.Warning: + pass + + logger.info('Creating staging table...') + cursor.execute('CREATE TABLE %(table)s_new LIKE %(table)s' % {'table': table}) + + logger.info('Disabling autocommit...') + cursor.execute('SET autocommit=0') + + logger.info('Adding rows to table...') + for row in itertools.islice(f, 5, None): + cols = row.split() + + # index -1 is i/e/? for igp/egp + origin_as = cols[-2] + # FIXME: skip AS_SETs + if origin_as[0] == '{': + continue + + prefix = cols[1] + + # validate the prefix since the "sh ip bgp" output is sometimes + # corrupt by no space between the prefix and the next hop IP + # address. + net, bits = prefix.split('/') + if len(bits) > 2: + s = ['mask for %s looks fishy...' % prefix] + prefix = '%s/%s' % (net, bits[0:2]) + s.append('assuming it should be %s' % prefix) + logger.warning(' '.join(s)) + + # the output may contain multiple paths to the same origin. + # if this is the same prefix as the last entry, we don't need + # to validate it again. + # + # prefixes are sorted, but the origin_as is not, so we keep a set to + # avoid duplicates, and insert into the db once we've seen all the + # origin_as values for a given prefix + if prefix != last_prefix: + # output routes for previous prefix + if last_prefix is not None: + try: + rng = range_class.parse_str(last_prefix) + rmin = long(rng.min) + rmax = long(rng.max) + cursor.executemany(sql, [(asn, rmin, rmax) for asn in asns]) + except BadIPResource: + logger.warning('skipping bad prefix: ' + last_prefix) + + asns = set() + last_prefix = prefix + + asns.add(int(origin_as)) + + logger.info('Committing...') + cursor.execute('COMMIT') + + try: + logger.info('Dropping old table...') + cursor.execute('DROP TABLE IF EXISTS %s_old' % table) + except _mysql_exceptions.Warning: + pass + + logger.info('Swapping staging table with live table...') + cursor.execute('RENAME TABLE %(table)s TO %(table)s_old, %(table)s_new TO %(table)s' % {'table': table}) + + transaction.commit_unless_managed() + + logger.info('Updating timestamp metadata...') + rpki.gui.app.timestamp.update('bgp_v4_import') + + +def parse_mrt(f): + # filter input through bgpdump + pipe = subprocess.Popen([BGPDUMP, '-m', '-v', '-'], stdin=f, + stdout=subprocess.PIPE) + + last_prefix = None + last_as = None + for e in pipe.stdout.readlines(): + a = e.split('|') + prefix = a[5] + try: + origin_as = int(a[6].split()[-1]) + except ValueError: + # skip AS_SETs + continue + + if prefix != last_prefix: + last_prefix = prefix + elif last_as == origin_as: + continue + last_as = origin_as + + asns = PREFIXES.get(prefix) + if not asns: + asns = set() + PREFIXES[prefix] = asns + asns.add(origin_as) + + pipe.wait() + if pipe.returncode: + raise ProgException('bgpdump exited with code %d' % pipe.returncode) + + +class ProgException(Exception): + pass + + +class BadArgument(ProgException): + pass + + +class UnknownInputType(ProgException): + pass + + +class PipeFailed(ProgException): + pass + + +if __name__ == '__main__': + start_time = time.time() + + parser = optparse.OptionParser(usage='%prog [options] PATH', + description="""This tool is used to import the IPv4/6 BGP table dumps +from routeviews.org into the RPKI Web Portal database. If the +input file is a bzip2 compressed file, it will be decompressed +automatically.""") + parser.add_option('-t', '--type', dest='filetype', metavar='TYPE', + help='Specify the input file type (auto, text, mrt) [Default: %default]') + parser.add_option('-l', '--level', dest='log_level', default='INFO', + help='Set logging level [Default: %default]') + parser.add_option('-u', '--bunzip2', dest='bunzip', metavar='PROG', + help='Specify bunzip2 program to use') + parser.add_option('-b', '--bgpdump', dest='bgpdump', metavar='PROG', + help='Specify path to bgdump binary') + parser.set_defaults(debug=False, verbose=False, filetype='auto') + options, args = parser.parse_args() + + v = getattr(logging, options.log_level.upper()) + logger.setLevel(v) + logging.basicConfig() + logger.info('logging level set to ' + logging.getLevelName(v)) + + if options.bgpdump: + BGPDUMP = os.path.expanduser(options.bgpdump) + + try: + if len(args) != 1: + raise BadArgument('no filename specified, or more than one filename specified') + filename = args[0] + + if options.filetype == 'auto': + # try to determine input type from filename, based on the default + # filenames from archive.routeviews.org + bname = os.path.basename(filename) + if bname.startswith('oix-full-snapshot-latest'): + filetype = 'text' + elif bname.startswith('rib.'): + filetype = 'mrt' + else: + raise UnknownInputType('unable to automatically determine input file type') + logging.info('Detected import format as "%s"' % filetype) + else: + filetype = options.filetype + + pipe = None + if filename.endswith('.bz2'): + bunzip = 'bunzip2' if not options.bunzip else os.path.expanduser(options.bunzip) + logging.info('Decompressing input file on the fly...') + pipe = subprocess.Popen([bunzip, '--stdout', filename], + stdout=subprocess.PIPE) + input_file = pipe.stdout + else: + input_file = open(filename) + + try: + dispatch = {'text': parse_text, 'mrt': parse_mrt} + dispatch[filetype](input_file) + except KeyError: + raise UnknownInputType('"%s" is an unknown input file type' % filetype) + + if pipe: + logging.debug('Waiting for child to exit...') + pipe.wait() + if pipe.returncode: + raise PipeFailed('Child exited code %d' % pipe.returncode) + pipe = None + else: + input_file.close() + + logger.info('Elapsed time %d secs' % (time.time() - start_time)) + rc = 0 + + except ProgException, e: + logger.exception(e) + rc = 1 + + logging.shutdown() + sys.exit(rc) diff --git a/rpkid/portal-gui/scripts/rpkigui-rcynic.py b/rpkid/portal-gui/scripts/rpkigui-rcynic.py index 3dc0d9bd..3205fc8d 100644 --- a/rpkid/portal-gui/scripts/rpkigui-rcynic.py +++ b/rpkid/portal-gui/scripts/rpkigui-rcynic.py @@ -1,5 +1,5 @@ -# $Id$ # Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -12,216 +12,212 @@ # LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -# + +__version__ = '$Id$' default_logfile = '/var/rcynic/data/summary.xml' default_root = '/var/rcynic/data' -import time, vobject +import time +import vobject +import logging + +from django.db import transaction +import django.db.models +from django.core.exceptions import ObjectDoesNotExist + +import rpki +import rpki.gui.app.timestamp from rpki.gui.cacheview import models from rpki.rcynic import rcynic_xml_iterator, label_iterator from rpki.sundial import datetime -from django.db import transaction -import django.db.models -debug = False -fam_map = { 'roa_prefix_set_ipv6': 6, 'roa_prefix_set_ipv4': 4 } +logger = logging.getLogger(__name__) -class rcynic_object(object): - def __call__(self, vs): - """ - do initial processing on a rcynic_object instance. +def rcynic_cert(cert, obj): + obj.sia = cert.sia_directory_uri - return value is a tuple: first element is a boolean value indicating whether - the object is changed/new since the last time we processed it. second - element is the db instance. - """ - if debug: - print 'processing %s at %s' % (vs.file_class.__name__, vs.uri) + # object must be saved for the related manager methods below to work + obj.save() + # resources can change when a cert is updated + obj.asns.clear() + obj.addresses.clear() - # rcynic will generation <validation_status/> elements for objects - # listed in the manifest but not found on disk - if os.path.exists(vs.filename): - q = self.model_class.objects.filter(uri=vs.uri) + for asr in cert.resources.asn: + logger.debug('processing %s' % asr) + + attrs = {'min': asr.min, 'max': asr.max} + q = models.ASRange.objects.filter(**attrs) + if not q: + obj.asns.create(**attrs) + else: + obj.asns.add(q[0]) + + for cls, addr_obj, addrset in (models.AddressRange, obj.addresses, cert.resources.v4), (models.AddressRangeV6, obj.addresses_v6, cert.resources.v6): + for rng in addrset: + logger.debug('processing %s' % rng) + + attrs = {'prefix_min': rng.min, 'prefix_max': rng.max} + q = cls.objects.filter(**attrs) if not q: - if debug: - print 'creating new db instance' - inst = self.model_class(uri=vs.uri) + addr_obj.create(**attrs) else: - inst = q[0] + addr_obj.add(q[0]) + + +def rcynic_roa(roa, obj): + obj.asid = roa.asID + # object must be saved for the related manager methods below to work + obj.save() + obj.prefixes.clear() + obj.prefixes_v6.clear() + for pfxset in roa.prefix_sets: + if pfxset.__class__.__name__ == 'roa_prefix_set_ipv6': + roa_cls = models.ROAPrefixV6 + prefix_obj = obj.prefixes_v6 + else: + roa_cls = models.ROAPrefixV4 + prefix_obj = obj.prefixes + + for pfx in pfxset: + attrs = {'prefix_min': pfx.min(), + 'prefix_max': pfx.max(), + 'max_length': pfx.max_prefixlen} + q = roa_cls.objects.filter(**attrs) + if not q: + prefix_obj.create(**attrs) + else: + prefix_obj.add(q[0]) - # determine if the object is changed/new - mtime = os.stat(vs.filename)[8] - if mtime != inst.mtime: - inst.mtime = mtime - obj = vs.obj # causes object to be lazily loaded - inst.not_before = obj.notBefore.to_sql() - inst.not_after = obj.notAfter.to_sql() - if debug: - sys.stderr.write('name=%s ski=%s\n' % (obj.subject, obj.ski)) - inst.name = obj.subject - inst.keyid = obj.ski - # look up signing cert - if obj.issuer == obj.subject: - # self-signed cert (TA) - inst.cert = inst - else: - q = models.Cert.objects.filter(keyid=obj.aki, name=obj.issuer) - if q: - inst.issuer = q[0] - else: - sys.stderr.write('warning: unable to find signing cert with ski=%s (%s)\n' % (obj.aki, obj.issuer)) - return None - - self.callback(obj, inst) - else: - if debug: - print 'object is unchanged' +def rcynic_gbr(gbr, obj): + vcard = vobject.readOne(gbr.vcard) + logger.debug(vcard.prettyPrint()) + obj.full_name = vcard.fn.value if hasattr(vcard, 'fn') else None + obj.email_address = vcard.email.value if hasattr(vcard, 'email') else None + obj.telephone = vcard.tel.value if hasattr(vcard, 'tel') else None + obj.organization = vcard.org.value[0] if hasattr(vcard, 'org') else None + +LABEL_CACHE = {} - # save required to create new ValidationStatus object refering to it - inst.save() - inst.statuses.create(generation=models.generations_dict[vs.generation] if vs.generation else None, - timestamp=datetime.fromXMLtime(vs.timestamp).to_sql(), - status=models.ValidationLabel.objects.get(label=vs.status)) - return inst +def save_statuses(inst, statuses): + for vs in statuses: + timestamp = datetime.fromXMLtime(vs.timestamp).to_sql() + + # cache validation labels + if vs.status in LABEL_CACHE: + status = LABEL_CACHE[vs.status] else: - if debug: - print 'ERROR - file is missing: %s' % vs.filename + status = models.ValidationLabel.objects.get(label=vs.status) + LABEL_CACHE[vs.status] = status + + g = models.generations_dict[vs.generation] if vs.generation else None - return True + inst.statuses.create(generation=g, timestamp=timestamp, status=status) -class rcynic_cert(rcynic_object): - model_class = models.Cert +@transaction.commit_on_success +def process_cache(root, xml_file): + dispatch = { + 'rcynic_certificate': rcynic_cert, + 'rcynic_roa': rcynic_roa, + 'rcynic_ghostbuster': rcynic_gbr + } + model_class = { + 'rcynic_certificate': models.Cert, + 'rcynic_roa': models.ROA, + 'rcynic_ghostbuster': models.Ghostbuster + } - def callback(self, cert, obj): - """ - Process a RPKI resource certificate. - """ + last_uri = None + statuses = [] - obj.sia = cert.sia_directory_uri - obj.save() + logger.info('clearing validation statuses') + models.ValidationStatus.objects.all().delete() - # resources can change when a cert is updated - obj.asns.clear() - obj.addresses.clear() + logger.info('updating validation status') + for vs in rcynic_xml_iterator(root, xml_file): + if vs.uri != last_uri: + if statuses: + obj, created = models.RepositoryObject.objects.get_or_create(uri=last_uri) + save_statuses(obj, statuses) - for asr in cert.resources.asn: - if debug: - sys.stderr.write('processing %s\n' % asr) + statuses = [] + last_uri = vs.uri - attrs = { 'min': asr.min, 'max': asr.max } - q = models.ASRange.objects.filter(**attrs) + statuses.append(vs) + + if vs.status == 'object_accepted': + logger.debug('processing %s' % vs.filename) + + cls = model_class[vs.file_class.__name__] + q = cls.objects.filter(repo__uri=vs.uri) if not q: - obj.asns.create(**attrs) + repo, created = models.RepositoryObject.objects.get_or_create(uri=vs.uri) + inst = cls(repo=repo) else: - obj.asns.add(q[0]) + inst = q[0] - for family, addrset in (4, cert.resources.v4), (6, cert.resources.v6): - for rng in addrset: - if debug: - sys.stderr.write('processing %s\n' % rng) + # determine if the object is changed/new + mtime = os.stat(vs.filename)[8] + if mtime != inst.mtime: + inst.mtime = mtime + try: + obj = vs.obj # causes object to be lazily loaded + except rpki.POW._der.DerError, e: + logger.warning('Caught %s while processing %s: %s' % (type(e), vs.filename, e)) + continue - attrs = { 'family': family, 'min': str(rng.min), 'max': str(rng.max) } - q = models.AddressRange.objects.filter(**attrs) - if not q: - obj.addresses.create(**attrs) - else: - obj.addresses.add(q[0]) - - if debug: - print 'finished processing rescert at %s' % cert.uri - -class rcynic_roa(rcynic_object): - model_class = models.ROA - - def callback(self, roa, obj): - obj.asid = roa.asID - obj.save() - obj.prefixes.clear() - for pfxset in roa.prefix_sets: - family = fam_map[pfxset.__class__.__name__] - for pfx in pfxset: - attrs = { 'family' : family, - 'prefix': str(pfx.prefix), - 'bits' : pfx.prefixlen, - 'max_length': pfx.max_prefixlen } - q = models.ROAPrefix.objects.filter(**attrs) - if not q: - obj.prefixes.create(**attrs) - else: - obj.prefixes.add(q[0]) + inst.not_before = obj.notBefore.to_sql() + inst.not_after = obj.notAfter.to_sql() + inst.name = obj.subject + inst.keyid = obj.ski -class rcynic_gbr(rcynic_object): - model_class = models.Ghostbuster + # look up signing cert + if obj.issuer == obj.subject: + # self-signed cert (TA) + assert(isinstance(inst, models.Cert)) + inst.issuer = inst + else: + try: + inst.issuer = models.Cert.objects.get(keyid=obj.aki, name=obj.issuer) + except ObjectDoesNotExist: + logger.warning('unable to find signing cert with ski=%s (%s)' % (obj.aki, obj.issuer)) + continue - def callback(self, gbr, obj): - vcard = vobject.readOne(gbr.vcard) - if debug: - vcard.prettyPrint() - obj.full_name = vcard.fn.value if hasattr(vcard, 'fn') else None - obj.email_address = vcard.email.value if hasattr(vcard, 'email') else None - obj.telephone = vcard.tel.value if hasattr(vcard, 'tel') else None - obj.organization = vcard.org.value[0] if hasattr(vcard, 'org') else None + # do object-specific tasks + dispatch[vs.file_class.__name__](obj, inst) -def process_cache(root, xml_file): - start = time.time() + inst.save() # don't require a save in the dispatch methods + else: + logger.debug('object is unchanged') - dispatch = { - 'rcynic_certificate': rcynic_cert(), - 'rcynic_roa' : rcynic_roa(), - 'rcynic_ghostbuster': rcynic_gbr() - } + # insert the saved validation statuses now that the object has been + # created. + save_statuses(inst.repo, statuses) + statuses = [] - # remove all existing ValidationStatus_* entries - models.ValidationStatus_Cert.objects.all().delete() - models.ValidationStatus_ROA.objects.all().delete() - models.ValidationStatus_Ghostbuster.objects.all().delete() - - # loop over all rcynic objects and dispatch based on the returned - # rcynic_object subclass - n = 1 - defer = rcynic_xml_iterator(root, xml_file) - while defer: - if debug: - print 'starting iteration %d for deferred objects' % n - n = n + 1 - - elts = defer - defer = [] - for vs in elts: - # need to defer processing this object, most likely because - # the <validation_status/> element for the signing cert hasn't - # been seen yet - if not dispatch[vs.file_class.__name__](vs): - defer.append(vs) + # process any left over statuses for an object that was not ultimately + # accepted + if statuses: + obj, created = models.RepositoryObject.objects.get_or_create(uri=last_uri) + save_statuses(obj, statuses) # garbage collection # remove all objects which have no ValidationStatus references, which # means they did not appear in the last XML output - if debug: - print 'performing garbage collection' + logger.info('performing garbage collection') - # trying to .delete() the querysets directly results in a "too many sql variables" exception - for qs in (models.Cert.objects.annotate(num_statuses=django.db.models.Count('statuses')).filter(num_statuses=0), - models.Ghostbuster.objects.annotate(num_statuses=django.db.models.Count('statuses')).filter(num_statuses=0), - models.ROA.objects.annotate(num_statuses=django.db.models.Count('statuses')).filter(num_statuses=0)): - for e in qs: - e.delete() - - if debug: - stop = time.time() - sys.stdout.write('elapsed time %d seconds.\n' % (stop - start)) + # Delete all objects that have zero validation status elements. + models.RepositoryObject.objects.annotate(num_statuses=django.db.models.Count('statuses')).filter(num_statuses=0).delete() +@transaction.commit_on_success def process_labels(xml_file): - if debug: - sys.stderr.write('updating labels...\n') + logger.info('updating labels...') for label, kind, desc in label_iterator(xml_file): - if debug: - sys.stderr.write('label=%s kind=%s desc=%s\n' % (label, kind, desc)) + logger.debug('label=%s kind=%s desc=%s' % (label, kind, desc)) if kind: q = models.ValidationLabel.objects.filter(label=label) if not q: @@ -233,24 +229,33 @@ def process_labels(xml_file): obj.status = desc obj.save() + if __name__ == '__main__': import optparse parser = optparse.OptionParser() - parser.add_option("-d", "--debug", action="store_true", - help="enable debugging message") + parser.add_option("-l", "--level", dest="log_level", default='INFO', + help="specify the logging level [default: %default]") parser.add_option("-f", "--file", dest="logfile", - help="specify the rcynic XML file to parse [default: %default]", - default=default_logfile) + help="specify the rcynic XML file to parse [default: %default]", + default=default_logfile) parser.add_option("-r", "--root", - help="specify the chroot directory for the rcynic jail [default: %default]", - metavar="DIR", default=default_root) + help="specify the chroot directory for the rcynic jail [default: %default]", + metavar="DIR", default=default_root) options, args = parser.parse_args(sys.argv) - if options.debug: - debug = True - with transaction.commit_on_success(): - process_labels(options.logfile) - process_cache(options.root, options.logfile) + v = getattr(logging, options.log_level.upper()) + logger.setLevel(v) + logging.basicConfig() + logger.info('log level set to %s' % logging.getLevelName(v)) + + start = time.time() + process_labels(options.logfile) + process_cache(options.root, options.logfile) + + rpki.gui.app.timestamp.update('rcynic_import') + + stop = time.time() + logger.info('elapsed time %d seconds.' % (stop - start)) -# vim:sw=4 ts=8 + logging.shutdown() diff --git a/rpkid/portal-gui/scripts/rpkigui-reset-demo.py b/rpkid/portal-gui/scripts/rpkigui-reset-demo.py new file mode 100644 index 00000000..acfddabd --- /dev/null +++ b/rpkid/portal-gui/scripts/rpkigui-reset-demo.py @@ -0,0 +1,34 @@ +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +""" +This script is used to reset all of the labuser* accounts on demo.rpki.net back +to a state suitable for a new workshop. It removes all ROAs and Ghostbuster +issued by the labuser accounts. + +""" + +__version__ = '$Id$' + +from rpki.irdb.models import ROARequest, GhostbusterRequest, ResourceHolderCA +from rpki.gui.app.glue import list_received_resources + +for n in xrange(1, 33): + username = 'labuser%02d' % n + print 'removing objects for ' + username + for cls in (ROARequest, GhostbusterRequest): + cls.objects.filter(issuer__handle=username).delete() + print '... updating resource certificate cache' + conf = ResourceHolderCA.objects.get(handle=username) + list_received_resources(sys.stdout, conf) diff --git a/rpkid/portal-gui/scripts/rpkigui-response.py b/rpkid/portal-gui/scripts/rpkigui-response.py deleted file mode 100755 index 9b150c51..00000000 --- a/rpkid/portal-gui/scripts/rpkigui-response.py +++ /dev/null @@ -1,70 +0,0 @@ -# $Id$ -# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions -# -# Permission to use, copy, modify, and distribute this software for any -# purpose with or without fee is hereby granted, provided that the above -# copyright notice and this permission notice appear in all copies. -# -# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -# PERFORMANCE OF THIS SOFTWARE. -# -# -# Helper script for use on the server side when using rpkidemo. -# Takes a xml result from either configure_parent or -# configure_publication_client and places it in the portal gui -# outbox with the appropriate rfc822 header fields. - -import os -os.environ['DJANGO_SETTINGS_MODULE'] = 'rpki.gui.settings' - -import sys -import pwd -import email.message, email.utils, mailbox -from django.conf import settings - -if len(sys.argv) < 4: - sys.stderr.write("""usage: rpkigui-response <target-handle> <response-type> <xml-response-file> - -<target-handle> the handle for the rpkidemo user to which this - response should be sent - -<response-type> 'parent' for a configure_child response, or - 'repository' for a configure_publication_client - response - -<xml-response-file> the file containing the xml response for a - configure_child or configure_publication_client - command -""") - - sys.exit(0) - -class InvalidResponseType(Exception): - """ - Invalid response type. - """ - -request_type = sys.argv[2] -if not request_type in ('parent', 'repository'): - raise InvalidResponseType, 'invalid response type: %s' % request_type - -# make sure apache process can manipulate the outbox! -os.setuid(pwd.getpwnam(settings.WEB_USER)[2]) - -msg = email.message.Message() -msg['X-rpki-self-handle'] = sys.argv[1] -msg['X-rpki-type'] = request_type -msg['Date'] = email.utils.formatdate() -msg['Message-ID'] = email.utils.make_msgid() -msg.set_type('application/x-rpki-setup') -msg.set_payload(open(sys.argv[3]).read()) - -box = mailbox.Maildir(settings.OUTBOX) -box.add(msg) - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/portal-gui/settings.py.in b/rpkid/portal-gui/settings.py.in index 2800bc24..186f3f1d 100644 --- a/rpkid/portal-gui/settings.py.in +++ b/rpkid/portal-gui/settings.py.in @@ -6,13 +6,33 @@ # DO NOT EDIT! This file is automatically generated from # settings.py.in +import rpki.config + DEBUG = True TEMPLATE_DEBUG = DEBUG +# needs to be set prior to the call to rpki.config.parser so it knows +# where to find the system rpki.conf +rpki.config.default_dirname = '%(AC_SYSCONFDIR)s' + +# load the sql authentication bits from the system rpki.conf +rpki_config = rpki.config.parser(section='web_portal') + DATABASES = { 'default' : { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME' : '%(AC_DATABASE_PATH)s' + 'ENGINE' : 'django.db.backends.mysql', + 'NAME' : rpki_config.get('sql-database'), + 'USER' : rpki_config.get('sql-username'), + 'PASSWORD': rpki_config.get('sql-password'), + + # Ensure the default storage engine is InnoDB since we need + # foreign key support. The Django documentation suggests + # removing this after the syncdb is performed as an optimization, + # but there isn't an easy way to do this automatically. + + 'OPTIONS' : { + 'init_command': 'SET storage_engine=INNODB' + } } } @@ -50,13 +70,14 @@ ROOT_URLCONF = 'rpki.gui.urls' INSTALLED_APPS = ( 'django.contrib.auth', - 'django.contrib.admin', - 'django.contrib.admindocs', + #'django.contrib.admin', + #'django.contrib.admindocs', 'django.contrib.contenttypes', 'django.contrib.sessions', -# 'django.contrib.staticfiles', + 'rpki.irdb', 'rpki.gui.app', - 'rpki.gui.cacheview' + 'rpki.gui.cacheview', + 'rpki.gui.routeview', ) TEMPLATE_CONTEXT_PROCESSORS = ( @@ -67,8 +88,3 @@ TEMPLATE_CONTEXT_PROCESSORS = ( "django.contrib.messages.context_processors.messages", "django.core.context_processors.request" ) - -#STATIC_URL = '/static/' -#STATIC_ROOT = '%(AC_DATAROOTDIR)s/rpki/gui/static' -#STATICFILES_DIRS = (("admin", "%(AC_DJANGO_DIR)s/contrib/admin/media"),) -#STATICFILES_FINDERS = ("django.contrib.staticfiles.finders.FileSystemFinder",) diff --git a/rpkid/rpki-sql-setup.py b/rpkid/rpki-sql-setup.py index 051f6980..0a900399 100644 --- a/rpkid/rpki-sql-setup.py +++ b/rpkid/rpki-sql-setup.py @@ -30,11 +30,12 @@ def read_schema(name): Convert an SQL file into a list of SQL statements. """ lines = [] - for line in getattr(rpki.sql_schemas, name).splitlines(): + for line in getattr(rpki.sql_schemas, name, "").splitlines(): line = " ".join(line.split()) if line and not line.startswith("--"): lines.append(line) - return [statement.strip() for statement in " ".join(lines).rstrip(";").split(";")] + + return [statement.strip() for statement in " ".join(lines).rstrip(";").split(";") if statement.strip()] def sql_setup(name): """ diff --git a/rpkid/rpki/csv_utils.py b/rpkid/rpki/csv_utils.py new file mode 100644 index 00000000..f7eed414 --- /dev/null +++ b/rpkid/rpki/csv_utils.py @@ -0,0 +1,100 @@ +""" +CSV utilities, moved here from myrpki.py. + +$Id$ + +Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import csv +import os + +class BadCSVSyntax(Exception): + """ + Bad CSV syntax. + """ + +class csv_reader(object): + """ + Reader for tab-delimited text that's (slightly) friendlier than the + stock Python csv module (which isn't intended for direct use by + humans anyway, and neither was this package originally, but that + seems to be the way that it has evolved...). + + Columns parameter specifies how many columns users of the reader + expect to see; lines with fewer columns will be padded with None + values. + + Original API design for this class courtesy of Warren Kumari, but + don't blame him if you don't like what I did with his ideas. + """ + + def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"): + assert columns is None or isinstance(columns, int) + assert min_columns is None or isinstance(min_columns, int) + if columns is not None and min_columns is None: + min_columns = columns + self.filename = filename + self.columns = columns + self.min_columns = min_columns + self.comment_characters = comment_characters + self.file = open(filename, "r") + + def __iter__(self): + line_number = 0 + for line in self.file: + line_number += 1 + line = line.strip() + if not line or line[0] in self.comment_characters: + continue + fields = line.split() + if self.min_columns is not None and len(fields) < self.min_columns: + raise BadCSVSyntax, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line) + if self.columns is not None and len(fields) > self.columns: + raise BadCSVSyntax, "%s:%d: Too many columns in line %r" % (self.filename, line_number, line) + if self.columns is not None and len(fields) < self.columns: + fields += tuple(None for i in xrange(self.columns - len(fields))) + yield fields + +class csv_writer(object): + """ + Writer object for tab delimited text. We just use the stock CSV + module in excel-tab mode for this. + + If "renmwo" is set (default), the file will be written to + a temporary name and renamed to the real filename after closing. + """ + + def __init__(self, filename, renmwo = True): + self.filename = filename + self.renmwo = "%s.~renmwo%d~" % (filename, os.getpid()) if renmwo else filename + self.file = open(self.renmwo, "w") + self.writer = csv.writer(self.file, dialect = csv.get_dialect("excel-tab")) + + def close(self): + """ + Close this writer. + """ + if self.file is not None: + self.file.close() + self.file = None + if self.filename != self.renmwo: + os.rename(self.renmwo, self.filename) + + def __getattr__(self, attr): + """ + Fake inheritance from whatever object csv.writer deigns to give us. + """ + return getattr(self.writer, attr) diff --git a/rpkid/rpki/gui/app/AllocationTree.py b/rpkid/rpki/gui/app/AllocationTree.py deleted file mode 100644 index f51ed430..00000000 --- a/rpkid/rpki/gui/app/AllocationTree.py +++ /dev/null @@ -1,151 +0,0 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -from rpki.gui.app import misc, models -from rpki import resource_set - -class AllocationTree(object): - '''Virtual class representing a tree of unallocated resource ranges. - Keeps track of which subsets of a resource range have been - allocated.''' - - def __init__(self, resource): - self.resource = resource - self.range = resource.as_resource_range() - self.need_calc = True - - def calculate(self): - if self.need_calc: - self.children = [] - self.alloc = self.__class__.set_type() - self.unalloc = self.__class__.set_type() - - if self.is_allocated(): - self.alloc.append(self.range) - else: - for child in self.resource.children.all(): - c = self.__class__(child) - if c.unallocated(): - self.children.append(c) - self.alloc = self.alloc.union(c.alloc) - total = self.__class__.set_type() - total.append(self.range) - self.unalloc = total.difference(self.alloc) - self.need_calc=False - - def unallocated(self): - self.calculate() - return self.unalloc - - def as_ul(self): - '''Returns a string of the tree as an unordered HTML list.''' - s = [] - s.append('<a href="%s">%s</a>' % (self.resource.get_absolute_url(), self.resource)) - - # when the unallocated range is a subset of the current range, - # display the missing ranges - u = self.unallocated() - if len(u) != 1 or self.range != u[0]: - s.append(' (missing: ') - s.append(', '.join(str(x) for x in u)) - s.append(')') - - # quick access links - if self.resource.parent: - s.append(' | <a href="%s/delete">delete</a>' % (self.resource.get_absolute_url(),)) - s.append(' | <a href="%s/allocate">give</a>' % (self.resource.get_absolute_url(),)) - if self.range.min != self.range.max: - s.append(' | <a href="%s/split">split</a>' % (self.resource.get_absolute_url(),)) - # add type-specific actions - a = self.supported_actions() - if a: - s.extend(a) - - if self.children: - s.append('\n<ul>\n') - for c in self.children: - s.append('<li>' + c.as_ul()) - s.append('\n</ul>') - - return ''.join(s) - - def supported_actions(self): - '''Virtual method allowing subclasses to add actions to the HTML list.''' - return None - - @classmethod - def from_resource_range(cls, resource): - if isinstance(resource, resource_set.resource_range_as): - return AllocationTreeAS(resource) - if isinstance(resource, resoute_set.resource_range_ip): - return AllocationTreeIP(resource) - raise ValueError, 'Unsupported resource range type' - -class AllocationTreeAS(AllocationTree): - set_type = resource_set.resource_set_as - - def __init__(self, *args, **kwargs): - AllocationTree.__init__(self, *args, **kwargs) - self.conf = misc.top_parent(self.resource).from_cert.all()[0].parent.conf - - def is_allocated(self): - '''Returns true if this AS has been allocated to a child or - used in a ROA request.''' - # FIXME: detect use in ROA requests - - if self.resource.allocated: - return True - - # for individual ASNs - if self.range.min == self.range.max: - # is this ASN used in any roa? - if self.conf.roas.filter(asn=self.range.min): - return True - - return False - -class AllocationTreeIP(AllocationTree): - '''virtual class representing a tree of IP address ranges.''' - - @classmethod - def from_prefix(cls, prefix): - r = prefix.as_resource_range() - if isinstance(r, resource_set.resource_range_ipv4): - return AllocationTreeIPv4(prefix) - elif isinstance(r, resource_set.resource_range_ipv6): - return AllocationTreeIPv6(prefix) - raise ValueError, 'Unsupported IP range type' - - def supported_actions(self): - '''add a link to issue a ROA for this IP range''' - if self.resource.is_prefix(): - return [' | <a href="%s/roa">roa</a>' % self.resource.get_absolute_url()] - else: - return [] - - def is_allocated(self): - '''Return True if this IP range is allocated to a child or used - in a ROA request.''' - return self.resource.allocated or self.resource.roa_requests.count() - -class AllocationTreeIPv4(AllocationTreeIP): - set_type = resource_set.resource_set_ipv4 - -class AllocationTreeIPv6(AllocationTreeIP): - set_type = resource_set.resource_set_ipv6 - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py deleted file mode 100644 index 52dc2c87..00000000 --- a/rpkid/rpki/gui/app/admin.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -$Id$ - -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -from django import forms -from django.contrib import admin -from rpki.gui.app import models - -class ConfAdmin( admin.ModelAdmin ): - pass - -class ChildAdmin( admin.ModelAdmin ): - pass - -class AddressRangeAdmin( admin.ModelAdmin ): - #list_display = ('__unicode__', 'lo', 'hi') - pass - -class AsnAdmin( admin.ModelAdmin ): - #list_display = ('__unicode__',) - pass - -class ParentAdmin( admin.ModelAdmin ): - pass - -class RoaAdmin( admin.ModelAdmin ): - pass - -class ResourceCertAdmin(admin.ModelAdmin): - pass - -class RoaRequestAdmin(admin.ModelAdmin): - pass - -class GhostbusterAdmin(admin.ModelAdmin): - pass - -admin.site.register(models.AddressRange, AddressRangeAdmin) -admin.site.register(models.Child, ChildAdmin) -admin.site.register(models.Conf, ConfAdmin) -admin.site.register(models.Asn, AsnAdmin) -admin.site.register(models.Ghostbuster, GhostbusterAdmin) -admin.site.register(models.Parent, ParentAdmin) -admin.site.register(models.ResourceCert, ResourceCertAdmin) -admin.site.register(models.Roa, RoaAdmin) -admin.site.register(models.RoaRequest, RoaRequestAdmin) - -# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/app/asnset.py b/rpkid/rpki/gui/app/asnset.py deleted file mode 100644 index beb3a8dc..00000000 --- a/rpkid/rpki/gui/app/asnset.py +++ /dev/null @@ -1,40 +0,0 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -class asnset(object): - """A set-like object for containing sets of ASN values.""" - v = set() - - def __init__(self, init=None): - """ - May be initialized from a comma separated list of positive integers. - """ - if init: - self.v = set(int(x) for x in init.split(',') if x.strip() != '') - if [x for x in self.v if x <= 0]: - raise ValueError, 'must be a positive integer' - - def __str__(self): - return ','.join(str(x) for x in sorted(self.v)) - - def __iter__(self): - return iter(self.v) - - def add(self, n): - assert isinstance(n, int) - assert n > 0 - self.v.add(n) diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index aad9185d..fb48fb08 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -1,26 +1,29 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' + + +from django.contrib.auth.models import User from django import forms +from rpki.resource_set import (resource_range_as, resource_range_ipv4, + resource_range_ipv6) +from rpki.gui.app import models +from rpki.exceptions import BadIPResource +from rpki.gui.app.glue import str_to_resource_range -import rpki.ipaddrs - -from rpki.gui.app import models, misc -from rpki.gui.app.asnset import asnset class AddConfForm(forms.Form): handle = forms.CharField(required=True, @@ -44,212 +47,296 @@ class AddConfForm(forms.Form): label='Pubd contact', help_text='email address for the operator of your pubd instance') -class ImportForm(forms.Form): - '''Form used for uploading parent/child identity xml files''' - handle = forms.CharField(max_length=30, help_text='your name for this entity') - xml = forms.FileField(help_text='xml filename') -def PrefixSplitForm(parent, *args, **kwargs): - class _wrapper(forms.Form): - prefix = forms.CharField(max_length=200, help_text='CIDR or range') +class GhostbusterRequestForm(forms.ModelForm): + """ + Generate a ModelForm with the subset of parents for the current + resource handle. + """ + # override default form field + parent = forms.ModelChoiceField(queryset=None, required=False, + help_text='Specify specific parent, or none for all parents') - def clean(self): - p = self.cleaned_data.get('prefix') - try: - r = misc.parse_resource_range(p) - except ValueError, err: - print err - raise forms.ValidationError, 'invalid prefix or range' - # we get AssertionError is the range is misordered (hi before lo) - except AssertionError, err: - print err - raise forms.ValidationError, 'invalid prefix or range' - pr = parent.as_resource_range() - if r.min < pr.min or r.max > pr.max: - raise forms.ValidationError, \ - 'range is outside parent range' - if r.min == pr.min and r.max == pr.max: - raise forms.ValidationError, \ - 'range is equal to parent' - if parent.allocated: - raise forms.ValidationError, 'prefix is assigned to child' - for p in parent.children.all(): - c = p.as_resource_range() - if c.min <= r.min <= c.max or c.min <= r.max <= c.max: - raise forms.ValidationError, \ - 'overlap with another child prefix: %s' % (c,) - - return self.cleaned_data - return _wrapper(*args, **kwargs) - -def PrefixAllocateForm(iv, child_set, *args, **kwargs): - class _wrapper(forms.Form): - child = forms.ModelChoiceField(initial=iv, queryset=child_set, - required=False, empty_label='(Unallocated)') - return _wrapper(*args, **kwargs) - -def PrefixRoaForm(prefix, *args, **kwargs): - prefix_range = prefix.as_resource_range() - - class _wrapper(forms.Form): - asns = forms.CharField(max_length=200, required=False, - help_text='Comma-separated list of ASNs') - max_length = forms.IntegerField(min_value=prefix_range.prefixlen(), - max_value=prefix_range.datum_type.bits, - initial=prefix_range.prefixlen(), - help_text='must be in range %d-%d' % (prefix_range.prefixlen(), prefix_range.datum_type.bits)) + # override full_name. it is required in the db schema, but we allow the + # user to skip it and default from family+given name + full_name = forms.CharField(max_length=40, required=False, + help_text='automatically generated from family and given names if left blank') - def clean_asns(self): + def __init__(self, issuer, *args, **kwargs): + super(GhostbusterRequestForm, self).__init__(*args, **kwargs) + self.fields['parent'].queryset = models.Parent.objects.filter(issuer=issuer) + + class Meta: + model = models.GhostbusterRequest + exclude = ('issuer', 'vcard') + + def clean(self): + family_name = self.cleaned_data.get('family_name') + given_name = self.cleaned_data.get('given_name') + if not all([family_name, given_name]): + raise forms.ValidationError, 'Family and Given names must be specified' + + email = self.cleaned_data.get('email_address') + postal = self.cleaned_data.get('postal_address') + telephone = self.cleaned_data.get('telephone') + if not any([email, postal, telephone]): + raise forms.ValidationError, 'One of telephone, email or postal address must be specified' + + # if the full name is not specified, default to given+family + fn = self.cleaned_data.get('full_name') + if not fn: + self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name) + + return self.cleaned_data + + +class ImportForm(forms.Form): + """Form used for uploading parent/child identity xml files.""" + handle = forms.CharField(required=False, + widget=forms.TextInput(attrs={'class': 'xlarge'}), + help_text='Optional. Your name for this entity, or blank to accept name in XML') + xml = forms.FileField(label='XML file', + widget=forms.FileInput(attrs={'class': 'input-file'})) + + +class ImportRepositoryForm(forms.Form): + handle = forms.CharField(max_length=30, required=False, + label='Parent Handle', + help_text='Optional. Must be specified if you use a different name for this parent') + xml = forms.FileField(label='XML file', + widget=forms.FileInput(attrs={'class': 'input-file'})) + + +class ImportClientForm(forms.Form): + """Form used for importing publication client requests.""" + xml = forms.FileField(label='XML file', + widget=forms.FileInput(attrs={'class': 'input-file'})) + + +class UserCreateForm(forms.Form): + handle = forms.CharField(max_length=30, help_text='handle for new child') + email = forms.CharField(max_length=30, + help_text='email address for new user') + password = forms.CharField(widget=forms.PasswordInput) + password2 = forms.CharField(widget=forms.PasswordInput, + label='Confirm Password') + parent = forms.ModelChoiceField(required=False, + queryset=models.Conf.objects.all(), + help_text='optionally make a child of') + + def clean_handle(self): + handle = self.cleaned_data.get('handle') + if (handle and models.Conf.objects.filter(handle=handle).exists() or + User.objects.filter(username=handle).exists()): + raise forms.ValidationError('user already exists') + return handle + + def clean(self): + p1 = self.cleaned_data.get('password') + p2 = self.cleaned_data.get('password2') + if p1 != p2: + raise forms.ValidationError('passwords do not match') + handle = self.cleaned_data.get('handle') + parent = self.cleaned_data.get('parent') + if handle and parent and parent.children.filter(handle=handle).exists(): + raise forms.ValidationError('parent already has a child by that name') + return self.cleaned_data + + +class UserEditForm(forms.Form): + """Form for editing a user.""" + email = forms.CharField() + pw = forms.CharField(widget=forms.PasswordInput, label='Password', + required=False) + pw2 = forms.CharField(widget=forms.PasswordInput, label='Confirm password', + required=False) + + def clean(self): + p1 = self.cleaned_data.get('pw') + p2 = self.cleaned_data.get('pw2') + if p1 != p2: + raise forms.ValidationError('Passwords do not match') + return self.cleaned_data + + +class ROARequest(forms.Form): + """Form for entering a ROA request. + + Handles both IPv4 and IPv6.""" + + asn = forms.IntegerField(label='AS') + prefix = forms.CharField(max_length=50) + max_prefixlen = forms.CharField(required=False, + label='Max Prefix Length') + confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False) + + def __init__(self, *args, **kwargs): + """Takes an optional `conf` keyword argument specifying the user that + is creating the ROAs. It is used for validating that the prefix the + user entered is currently allocated to that user. + + """ + conf = kwargs.pop('conf', None) + super(ROARequest, self).__init__(*args, **kwargs) + self.conf = conf + + def _as_resource_range(self): + """Convert the prefix in the form to a + rpki.resource_set.resource_range_ip object. + + """ + prefix = self.cleaned_data.get('prefix') + return str_to_resource_range(prefix) + + def clean_asn(self): + value = self.cleaned_data.get('asn') + if value < 0: + raise forms.ValidationError('AS must be a positive value or 0') + return value + + def clean_prefix(self): + try: + r = self._as_resource_range() + except: + raise forms.ValidationError('invalid IP address') + + manager = models.ResourceRangeAddressV4 if isinstance(r, resource_range_ipv4) else models.ResourceRangeAddressV6 + if not manager.objects.filter(cert__parent__issuer=self.conf, + prefix_min__lte=r.min, + prefix_max__gte=r.max).exists(): + raise forms.ValidationError('prefix is not allocated to you') + return str(r) + + def clean_max_prefixlen(self): + v = self.cleaned_data.get('max_prefixlen') + if v: + if v[0] == '/': + v = v[1:] # allow user to specify /24 try: - v = asnset(self.cleaned_data.get('asns')) - return ','.join(str(x) for x in sorted(v)) + if int(v) < 0: + raise forms.ValidationError('max prefix length must be positive or 0') except ValueError: + raise forms.ValidationError('invalid integer value') + return v + + def clean(self): + if 'prefix' in self.cleaned_data: + r = self._as_resource_range() + max_prefixlen = self.cleaned_data.get('max_prefixlen') + max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen() + if max_prefixlen < r.prefixlen(): + raise forms.ValidationError('max prefix length must be greater than or equal to the prefix length') + if max_prefixlen > r.datum_type.bits: raise forms.ValidationError, \ - 'Must be a list of integers separated by commas.' - return self.cleaned_data['asns'] + 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.datum_type.bits) + self.cleaned_data['max_prefixlen'] = str(max_prefixlen) - def clean(self): - if not prefix.is_prefix(): - raise forms.ValidationError, \ - '%s can not be represented as a prefix.' % (prefix,) - if prefix.allocated: - raise forms.ValidationError, \ - 'Prefix is allocated to a child.' - return self.cleaned_data + return self.cleaned_data - return _wrapper(*args, **kwargs) -def PrefixDeleteForm(prefix, *args, **kwargs): - class _wrapped(forms.Form): +class ROARequestConfirm(forms.Form): + asn = forms.IntegerField(widget=forms.HiddenInput) + prefix = forms.CharField(widget=forms.HiddenInput) + max_prefixlen = forms.IntegerField(widget=forms.HiddenInput) - def clean(self): - if not prefix.parent: - raise forms.ValidationError, \ - 'Can not delete prefix received from parent' - if prefix.allocated: - raise forms.ValidationError, 'Prefix is allocated to child' - if prefix.roa_requests.all(): - raise forms.ValidationError, 'Prefix is used in your ROAs' - if prefix.children.all(): - raise forms.ValidationError, 'Prefix has been split' - return self.cleaned_data - - return _wrapped(*args, **kwargs) - -def GhostbusterForm(parent_qs, conf=None): - """ - Generate a ModelForm with the subset of parents for the current - resource handle. + def clean_asn(self): + value = self.cleaned_data.get('asn') + if value < 0: + raise forms.ValidationError('AS must be a positive value or 0') + return value - The 'conf' argument is required when creating a new object, in - order to specify the value of the 'conf' field in the new - Ghostbuster object. - """ - class wrapped(forms.ModelForm): - # override parent - parent = forms.ModelMultipleChoiceField(queryset=parent_qs, required=False, - help_text='use this record for a specific parent, or leave blank for all parents') - # override full_name. it is required in the db schema, but we allow the - # user to skip it and default from family+given name - full_name = forms.CharField(max_length=40, required=False, - help_text='automatically generated from family and given names if left blank') - - class Meta: - model = models.Ghostbuster - exclude = [ 'conf' ] - - def clean(self): - family_name = self.cleaned_data.get('family_name') - given_name = self.cleaned_data.get('given_name') - if not all([family_name, given_name]): - raise forms.ValidationError, 'Family and Given names must be specified' - - email = self.cleaned_data.get('email_address') - postal = self.cleaned_data.get('postal_address') - telephone = self.cleaned_data.get('telephone') - if not any([email, postal, telephone]): - raise forms.ValidationError, 'One of telephone, email or postal address must be specified' - - # if the full name is not specified, default to given+family - fn = self.cleaned_data.get('full_name') - if not fn: - self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name) - - return self.cleaned_data - - def save(self, *args, **kwargs): - if conf: - # the generic create_object view doesn't allow us to set - # the conf field, so wrap the save() method and set it - # here - kwargs['commit'] = False - obj = super(wrapped, self).save(*args, **kwargs) - obj.conf = conf - obj.save() - return obj - else: - return super(wrapped, self).save(*args, **kwargs) - - return wrapped - -class ChildForm(forms.ModelForm): + def clean_prefix(self): + try: + r = str_to_resource_range(self.cleaned_data.get('prefix')) + except BadIPResource: + raise forms.ValidationError('invalid prefix') + return str(r) + + def clean(self): + try: + r =str_to_resource_range(self.cleaned_data.get('prefix')) + if r.prefixlen() > self.cleaned_data.get('max_prefixlen'): + raise forms.ValidationError('max length is smaller than mask') + except BadIPResource: + pass + return self.cleaned_data + + +def AddASNForm(qs): """ - Subclass for editing rpki.gui.app.models.Child objects. + Generate a form class which only allows specification of ASNs contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildASN. + """ - class Meta: - model = models.Child - exclude = [ 'conf', 'handle' ] + class _wrapped(forms.Form): + asns = forms.CharField(label='ASNs', help_text='single ASN or range') -def ImportChildForm(parent_conf, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text="Child's RPKI handle") - xml = forms.FileField(help_text="Child's identity.xml file") + def clean_asns(self): + try: + r = resource_range_as.parse_str(self.cleaned_data.get('asns')) + except: + raise forms.ValidationError('invalid AS or range') + if not qs.filter(min__lte=r.min, max__gte=r.max).exists(): + raise forms.ValidationError('AS or range is not delegated to you') + return str(r) - def clean_handle(self): - if parent_conf.children.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, "a child with that handle already exists" - return self.cleaned_data['handle'] + return _wrapped - return wrapped(*args, **kwargs) -def ImportParentForm(conf, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text="Parent's RPKI handle") - xml = forms.FileField(help_text="XML response from parent", required=False) +def AddNetForm(qsv4, qsv6): + """ + Generate a form class which only allows specification of prefixes contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildNet. - def clean_handle(self): - if conf.parents.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, "a parent with that handle already exists" - return self.cleaned_data['handle'] + """ - return wrapped(*args, **kwargs) + class _wrapped(forms.Form): + address_range = forms.CharField(help_text='CIDR or range') -class ImportRepositoryForm(forms.Form): - parent_handle = forms.CharField(max_length=30, required=False, help_text='(optional)') - xml = forms.FileField(help_text='xml file from repository operator') + def clean_address_range(self): + address_range = self.cleaned_data.get('address_range') + try: + r = resource_range_ipv4.parse_str(address_range) + if not qsv4.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + try: + r = resource_range_ipv6.parse_str(address_range) + if not qsv6.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + raise forms.ValidationError('invalid IP address range') + return str(r) + + return _wrapped + + +def ChildForm(instance): + """ + Form for editing a Child model. -class ImportPubClientForm(forms.Form): - xml = forms.FileField(help_text='xml file from publication client') + This is roughly based on the equivalent ModelForm, but uses Form as a base + class so that selection boxes for the AS and Prefixes can be edited in a + single form. -def ChildWizardForm(parent, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text='handle for new child') - #create_user = forms.BooleanField(help_text='create a new user account for this handle?') - #password = forms.CharField(widget=forms.PasswordInput, help_text='password for new user', required=False) - #password2 = forms.CharField(widget=forms.PasswordInput, help_text='repeat password', required=False) + """ - def clean_handle(self): - if parent.children.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, 'a child with that handle already exists' - return self.cleaned_data['handle'] + class _wrapped(forms.Form): + valid_until = forms.DateTimeField(initial=instance.valid_until) + as_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildASN.objects.filter(child=instance), + required=False, + label='AS Ranges', + help_text='deselect to remove delegation') + address_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildNet.objects.filter(child=instance), + required=False, + help_text='deselect to remove delegation') - return wrapped(*args, **kwargs) + return _wrapped -class GenericConfirmationForm(forms.Form): - """ - stub form used for doing confirmations. - """ - pass -# vim:sw=4 ts=8 expandtab +class UserDeleteForm(forms.Form): + """Stub form for deleting users.""" + pass diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py index 687af268..7de1a9e5 100644 --- a/rpkid/rpki/gui/app/glue.py +++ b/rpkid/rpki/gui/app/glue.py @@ -1,125 +1,48 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. +""" +This file contains code that interfaces between the django views implementing +the portal gui and the rpki.* modules. -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. """ from __future__ import with_statement -import os, os.path, csv, shutil, stat, sys -from datetime import datetime, timedelta +__version__ = '$Id$' -from django.db.models import F +from datetime import datetime -import rpki, rpki.async, rpki.http, rpki.x509, rpki.left_right, rpki.myrpki -import rpki.publication +from rpki.resource_set import (resource_set_as, resource_set_ipv4, + resource_set_ipv6, resource_range_ipv4, + resource_range_ipv6) +from rpki.left_right import list_received_resources_elt +from rpki.irdb.zookeeper import Zookeeper from rpki.gui.app import models, settings -def confpath(*handle): - """ - Return the absolute pathname to the configuration directory for - the given resource handle. If additional arguments are given, they - are taken to mean files/subdirectories. - """ - argv = [ settings.CONFDIR ] - argv.extend(handle) - return os.path.join(*argv) - -def read_file_from_handle(handle, fname): - """read a filename relative to the directory for the given resource handle. returns - a tuple of (content, mtime)""" - with open(confpath(handle, fname), 'r') as fp: - data = fp.read() - mtime = os.fstat(fp.fileno())[stat.ST_MTIME] - return data, mtime - -read_identity = lambda h: read_file_from_handle(h, 'entitydb/identity.xml')[0] - -def output_asns(path, handle): - '''Write out csv file containing asns delegated to my children.''' - qs = models.Asn.objects.filter(lo=F('hi'), allocated__in=handle.children.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([asn.allocated.handle, asn.lo] for asn in qs) - w.close() - -def output_prefixes(path, handle): - '''Write out csv file containing prefixes delegated to my children.''' - qs = models.AddressRange.objects.filter(allocated__in=handle.children.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([p.allocated.handle, p.as_resource_range()] for p in qs) - w.close() - -def output_roas(path, handle): - '''Write out csv file containing my roas.''' - qs = models.RoaRequest.objects.filter(roa__in=handle.roas.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([req.as_roa_prefix(), req.roa.asn, - '%s-group-%d' % (handle.handle, req.roa.pk)] for req in qs) - w.close() - -def qualify_path(pfx, fname): - """Ensure 'path' is an absolute filename.""" - return fname if fname.startswith('/') else os.path.join(pfx, fname) - -def build_rpkid_caller(cfg, verbose=False): - """ - Returns a function suitable for calling rpkid using the - configuration information specified in the rpki.config.parser - object. - """ - bpki_servers_dir = cfg.get("bpki_servers_directory") - if not bpki_servers_dir.startswith('/'): - bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir) - - bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir) - rpkid_base = "http://%s:%s/" % (cfg.get("rpkid_server_host"), cfg.get("rpkid_server_port")) - - return rpki.async.sync_wrapper(rpki.http.caller( - proto = rpki.left_right, - client_key = rpki.x509.RSA(PEM_file = bpki_servers.dir + "/irbe.key"), - client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"), - server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer), - server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/rpkid.cer"), - url = rpkid_base + "left-right", - debug = verbose)) - -def build_pubd_caller(cfg): - bpki_servers_dir = cfg.get("bpki_servers_directory") - if not bpki_servers_dir.startswith('/'): - bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir) - - bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir) - pubd_base = "http://%s:%s/" % (cfg.get("pubd_server_host"), cfg.get("pubd_server_port")) - - return rpki.async.sync_wrapper(rpki.http.caller( - proto = rpki.publication, - client_key = rpki.x509.RSA( PEM_file = bpki_servers.dir + "/irbe.key"), - client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"), - server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer), - server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/pubd.cer"), - url = pubd_base + "control")) def ghostbuster_to_vcard(gbr): - """ - Convert a Ghostbuster object into a vCard object. - """ + """Convert a GhostbusterRequest object into a vCard object.""" import vobject vcard = vobject.vCard() - vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, given=gbr.given_name) + vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, + given=gbr.given_name) - adr_fields = [ 'box', 'extended', 'street', 'city', 'region', 'code', 'country' ] + adr_fields = ['box', 'extended', 'street', 'city', 'region', 'code', + 'country'] adr_dict = dict((f, getattr(gbr, f, '')) for f in adr_fields) if any(adr_dict.itervalues()): vcard.add('ADR').value = vobject.vcard.Address(**adr_dict) @@ -128,185 +51,64 @@ def ghostbuster_to_vcard(gbr): # the ORG type is a sequence of organization unit names, so # transform the org name into a tuple before stuffing into the # vCard object - attrs = [ ('FN', 'full_name', None), - ('TEL', 'telephone', None), - ('ORG', 'organization', lambda x: (x,)), - ('EMAIL', 'email_address', None) ] + attrs = [('FN', 'full_name', None), + ('TEL', 'telephone', None), + ('ORG', 'organization', lambda x: (x,)), + ('EMAIL', 'email_address', None)] for vtype, field, transform in attrs: v = getattr(gbr, field) if v: vcard.add(vtype).value = transform(v) if transform else v return vcard.serialize() -def qualify_path(pfx, fname): - """ - Ensure 'path' is an absolute filename. - """ - return fname if fname.startswith('/') else os.path.join(pfx, fname) - -def configure_resources(log, handle): - """ - This function should be called when resources for this resource - holder have changed. It updates IRDB and notifies rpkid to - immediately process the changes, rather than waiting for the cron - job to run. - For backwards compatability (and backups), it also writes the csv - files for use with the myrpki.py command line script. +def list_received_resources(log, conf): """ + Query rpkid for this resource handle's received resources. - path = confpath(handle.handle) - cfg = rpki.config.parser(os.path.join(path, 'rpki.conf'), 'myrpki') - - output_asns(qualify_path(path, cfg.get('asn_csv')), handle) - output_prefixes(qualify_path(path, cfg.get('prefix_csv')), handle) - output_roas(qualify_path(path, cfg.get('roa_csv')), handle) - - roa_requests = [] - for roa in handle.roas.all(): - v4 = rpki.resource_set.roa_prefix_set_ipv4() - v6 = rpki.resource_set.roa_prefix_set_ipv6() - for req in roa.from_roa_request.all(): - pfx = req.as_roa_prefix() - if isinstance(pfx, rpki.resource_set.roa_prefix_ipv4): - v4.append(pfx) - else: - v6.append(pfx) - roa_requests.append((roa.asn, v4, v6)) - - children = [] - for child in handle.children.all(): - asns = rpki.resource_set.resource_set_as([a.as_resource_range() for a in child.asn.all()]) - - v4 = rpki.resource_set.resource_set_ipv4() - v6 = rpki.resource_set.resource_set_ipv6() - for pfx in child.address_range.all(): - rng = pfx.as_resource_range() - if isinstance(rng, rpki.resource_set.resource_range_ipv4): - v4.append(rng) - else: - v6.append(rng) - - # convert from datetime.datetime to rpki.sundial.datetime - valid_until = rpki.sundial.datetime.fromdatetime(child.valid_until) - children.append((child.handle, asns, v4, v6, valid_until)) - - ghostbusters = [] - for gbr in handle.ghostbusters.all(): - vcard = ghostbuster_to_vcard(gbr) - parent_set = gbr.parent.all() - if parent_set: - for p in parent_set: - ghostbusters.append((p, vcard)) - else: - ghostbusters.append((None, vcard)) + The semantics are to clear the entire table and populate with the list of + certs received. Other models should not reference the table directly with + foreign keys. - # for hosted handles, get the config for the irdbd/rpkid host - if handle.host: - cfg = rpki.config.parser(confpath(handle.host.handle, 'rpki.conf'), 'myrpki') - - irdb = rpki.myrpki.IRDB(cfg) - irdb.update(handle, roa_requests, children, ghostbusters) - irdb.close() + """ - # contact rpkid to request immediate update - call_rpkid = build_rpkid_caller(cfg) - call_rpkid(rpki.left_right.self_elt.make_pdu(action='set', self_handle=handle.handle, run_now=True)) + z = Zookeeper(handle=conf.handle) + pdus = z.call_rpkid(list_received_resources_elt.make_pdu(self_handle=conf.handle)) -def list_received_resources(log, conf): - "Query rpkid for this resource handle's children and received resources." - - # if this handle is hosted, get the cfg for the host - rpki_conf = conf.host if conf.host else conf - cfg = rpki.config.parser(confpath(rpki_conf.handle, 'rpki.conf'), 'myrpki') - call_rpkid = build_rpkid_caller(cfg) - pdus = call_rpkid(rpki.left_right.list_received_resources_elt.make_pdu(self_handle=conf.handle), - rpki.left_right.child_elt.make_pdu(action="list", self_handle=conf.handle), - rpki.left_right.parent_elt.make_pdu(action="list", self_handle=conf.handle)) + models.ResourceCert.objects.filter(parent__issuer=conf).delete() for pdu in pdus: - if isinstance(pdu, rpki.left_right.child_elt): - # have we seen this child before? - child_set = conf.children.filter(handle=pdu.child_handle) - if not child_set: - # default to 1 year. no easy way to query irdb for the - # current value. - valid_until = datetime.now() + timedelta(days=365) - child = models.Child(conf=conf, handle=pdu.child_handle, - valid_until=valid_until) - child.save() - - elif isinstance(pdu, rpki.left_right.parent_elt): - # have we seen this parent before? - parent_set = conf.parents.filter(handle=pdu.parent_handle) - if not parent_set: - parent = models.Parent(conf=conf, handle=pdu.parent_handle) - parent.save() - - elif isinstance(pdu, rpki.left_right.list_received_resources_elt): - - # have we seen this parent before? - parent_set = conf.parents.filter(handle=pdu.parent_handle) - if not parent_set: - parent = models.Parent(conf=conf, handle=pdu.parent_handle) - parent.save() - else: - parent = parent_set[0] + if isinstance(pdu, list_received_resources_elt): + parent = models.Parent.objects.get(issuer=conf, + handle=pdu.parent_handle) not_before = datetime.strptime(pdu.notBefore, "%Y-%m-%dT%H:%M:%SZ") not_after = datetime.strptime(pdu.notAfter, "%Y-%m-%dT%H:%M:%SZ") - #print >>log, 'uri: %s, not before: %s, not after: %s' % (pdu.uri, not_before, not_after) + cert = models.ResourceCert.objects.create(parent=parent, + not_before=not_before, not_after=not_after, + uri=pdu.uri) - # have we seen this resource cert before? - cert_set = parent.resources.filter(uri=pdu.uri) - if cert_set.count() == 0: - cert = models.ResourceCert(uri=pdu.uri, parent=parent, - not_before=not_before, not_after=not_after) - else: - cert = cert_set[0] - # update timestamps since it could have been modified - cert.not_before = not_before - cert.not_after = not_after - cert.save() + for asn in resource_set_as(pdu.asn): + cert.asn_ranges.create(min=asn.min, max=asn.max) - for asn in rpki.resource_set.resource_set_as(pdu.asn): - # see if this resource is already part of the cert - if cert.asn.filter(lo=asn.min, hi=asn.max).count() == 0: - # ensure this range wasn't seen from another of our parents - for v in models.Asn.objects.filter(lo=asn.min, hi=asn.max): - # determine if resource is delegated from another parent - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.asn.add(v) - break - else: - cert.asn.create(lo=asn.min, hi=asn.max) - cert.save() + for rng in resource_set_ipv4(pdu.ipv4): + print >>log, 'adding v4 address range: %s' % rng + cert.address_ranges.create(prefix_min=rng.min, + prefix_max=rng.max) - # IPv4/6 - not separated in the django db - def add_missing_address(addr_set): - for ip in addr_set: - lo=str(ip.min) - hi=str(ip.max) - if cert.address_range.filter(lo=lo, hi=hi).count() == 0: - # ensure that this range wasn't previously seen from another of our parents - for v in models.AddressRange.objects.filter(lo=lo, hi=hi): - # determine if this resource is delegated from another parent as well - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.address_range.add(v) - break - else: - cert.address_range.create(lo=lo, hi=hi) - cert.save() + for rng in resource_set_ipv6(pdu.ipv6): + cert.address_ranges_v6.create(prefix_min=rng.min, + prefix_max=rng.max) + else: + print >>log, "error: unexpected pdu from rpkid type=%s" % type(pdu) - add_missing_address(rpki.resource_set.resource_set_ipv4(pdu.ipv4)) - add_missing_address(rpki.resource_set.resource_set_ipv6(pdu.ipv6)) def config_from_template(dest, a): """ - Create a new rpki.conf file from a generic template. Go line by - line through the template and substitute directives from the - dictionary 'a'. + Create a new rpki.conf file from a generic template. Go line by line + through the template and substitute directives from the dictionary 'a'. + """ with open(dest, 'w') as f: for r in open(settings.RPKI_CONF_TEMPLATE): @@ -320,181 +122,9 @@ def config_from_template(dest, a): else: print >>f, r, -class Myrpki(rpki.myrpki.main): - """ - wrapper around rpki.myrpki.main to force the config file to what i want, - and avoid cli arg parsing. - """ - def __init__(self, handle): - self.cfg_file = confpath(handle, 'rpki.conf') - self.read_config() - -def configure_daemons(log, conf, m): - if conf.host: - m.configure_resources_main() - - host = Myrpki(conf.host.handle) - host.do_configure_daemons(m.cfg.get('xml_filename')) - else: - m.do_configure_daemons('') - -def initialize_handle(log, handle, host, owner=None, commit=True): - """ - Create a new Conf object for this user. - """ - print >>log, "initializing new resource handle %s" % handle - - qs = models.Conf.objects.filter(handle=handle) - if not qs: - conf = models.Conf(handle=handle, host=host) - conf.save() - if owner: - conf.owner.add(owner) - else: - conf = qs[0] - - # create the config directory if it doesn't already exist - top = confpath(conf.handle) - if not os.path.exists(top): - os.makedirs(top) - - cfg_file = confpath(conf.handle, 'rpki.conf') - - # create rpki.conf file if it doesn't exist - if not os.path.exists(cfg_file): - print >>log, "generating rpki.conf for %s" % conf.handle - config_from_template(cfg_file, { 'handle': conf.handle, - 'configuration_directory': top, 'run_rpkid': 'false'}) - - # create stub csv files - for f in ('asns', 'prefixes', 'roas'): - p = confpath(conf.handle, f + '.csv') - if not os.path.exists(p): - f = open(p, 'w') - f.close() - - # load configuration for self - m = Myrpki(conf.handle) - m.do_initialize('') - - if commit: - # run twice the first time to get bsc cert issued - configure_daemons(log, conf, m) - configure_daemons(log, conf, m) - - return conf, m - -def import_child(log, conf, child_handle, xml_file): - """ - Import a child's identity.xml. - """ - m = Myrpki(conf.handle) - m.do_configure_child(xml_file) - configure_daemons(log, conf, m) - -def import_parent(log, conf, parent_handle, xml_file): - m = Myrpki(conf.handle) - m.do_configure_parent(xml_file) - configure_daemons(log, conf, m) - -def import_pubclient(log, conf, xml_file): - m = Myrpki(conf.handle) - m.do_configure_publication_client(xml_file) - configure_daemons(log, conf, m) - -def import_repository(log, conf, xml_file): - m = Myrpki(conf.handle) - m.do_configure_repository(xml_file) - configure_daemons(log, conf, m) - -def create_child(log, parent_conf, child_handle): - """ - implements the child create wizard to create a new locally hosted child - """ - child_conf, child = initialize_handle(log, handle=child_handle, host=parent_conf, commit=False) - - parent_handle = parent_conf.handle - parent = Myrpki(parent_handle) - - child_identity_xml = os.path.join(child.cfg.get("entitydb_dir"), 'identity.xml') - parent_response_xml = os.path.join(parent.cfg.get("entitydb_dir"), 'children', child_handle + '.xml') - repo_req_xml = os.path.join(child.cfg.get('entitydb_dir'), 'repositories', parent_handle + '.xml') - # XXX for now we assume the child is hosted by parent's pubd - repo_resp_xml = os.path.join(parent.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (parent_handle, child_handle)) - - parent.do_configure_child(child_identity_xml) - - child.do_configure_parent(parent_response_xml) - - parent.do_configure_publication_client(repo_req_xml) - - child.do_configure_repository(repo_resp_xml) - - # run twice the first time to get bsc cert issued - sys.stdout = sys.stderr - configure_daemons(log, child_conf, child) - configure_daemons(log, child_conf, child) - -def destroy_handle(log, handle): - conf = models.Conf.objects.get(handle=handle) - - cfg = rpki.config.parser(confpath(conf.host.handle, 'rpki.conf'), 'myrpki') - call_rpkid = build_rpkid_caller(cfg) - call_pubd = build_pubd_caller(cfg) - - # destroy the <self/> object and the <child/> object from the host/parent. - rpkid_reply = call_rpkid( - rpki.left_right.self_elt.make_pdu(action="destroy", self_handle=handle), - rpki.left_right.child_elt.make_pdu(action="destroy", self_handle=conf.host.handle, child_handle=handle)) - if isinstance(rpkid_reply[0], rpki.left_right.report_error_elt): - print >>log, "Error while calling pubd to delete client %s:" % handle - print >>log, rpkid_reply[0] - - pubd_reply = call_pubd(rpki.publication.client_elt.make_pdu(action="destroy", client_handle=handle)) - if isinstance(pubd_reply[0], rpki.publication.report_error_elt): - print >>log, "Error while calling pubd to delete client %s:" % handle - print >>log, pubd_reply[0] - - conf.delete() - - shutil.remove(confpath(handle)) - -def read_child_response(log, conf, child_handle): - m = Myrpki(conf.handle) - bname = child_handle + '.xml' - return open(os.path.join(m.cfg.get('entitydb_dir'), 'children', bname)).read() - -def read_child_repo_response(log, conf, child_handle): - """ - Return the XML file for the configure_publication_client response to the - child. - - Note: the current model assumes the publication client is a child of this - handle. - """ - - m = Myrpki(conf.handle) - return open(os.path.join(m.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (conf.handle, child_handle))).read() - -def update_bpki(log, conf): - m = Myrpki(conf.handle) - - # automatically runs configure_daemons when self-hosted - # otherwise runs configure_resources - m.do_update_bpki('') - - # when hosted, ship off to rpkid host - if conf.host: - configure_daemons(log, conf, m) - -def delete_child(log, conf, child_handle): - m = Myrpki(conf.handle) - m.do_delete_child(child_handle) - configure_daemons(log, conf, m) - -def delete_parent(log, conf, parent_handle): - m = Myrpki(conf.handle) - m.do_delete_parent(parent_handle) - configure_daemons(log, conf, m) - -# vim:sw=4 ts=8 expandtab +def str_to_resource_range(prefix): + try: + r = resource_range_ipv4.parse_str(prefix) + except BadIPResource: + r = resource_range_ipv6.parse_str(prefix) + return r diff --git a/rpkid/rpki/gui/app/misc.py b/rpkid/rpki/gui/app/misc.py deleted file mode 100644 index 5d3cba93..00000000 --- a/rpkid/rpki/gui/app/misc.py +++ /dev/null @@ -1,47 +0,0 @@ -# $Id$ -""" -Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -import rpki.resource_set -import rpki.ipaddrs - -def str_to_range(lo, hi): - """Convert IP address strings to resource_range_ip.""" - x = rpki.ipaddrs.parse(lo) - y = rpki.ipaddrs.parse(hi) - assert type(x) == type(y) - if isinstance(x, rpki.ipaddrs.v4addr): - return rpki.resource_set.resource_range_ipv4(x, y) - else: - return rpki.resource_set.resource_range_ipv6(x, y) - -def parse_resource_range(s): - '''Parse an IPv4/6 resource range.''' - # resource_set functions only accept str - if isinstance(s, unicode): - s = s.encode() - try: - return rpki.resource_set.resource_range_ipv4.parse_str(s) - except ValueError: - return rpki.resource_set.resource_range_ipv6.parse_str(s) - -def top_parent(prefix): - '''Returns the topmost resource from which the specified argument derives''' - while prefix.parent: - prefix = prefix.parent - return prefix - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index b78736b5..b7393717 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -1,271 +1,273 @@ -# $Id$ -""" -Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -import socket +# Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' from django.db import models -from django.contrib.auth.models import User - -from rpki.gui.app.misc import str_to_range import rpki.resource_set import rpki.exceptions +import rpki.irdb.models +import rpki.gui.models +import rpki.gui.routeview.models -class HandleField(models.CharField): - def __init__(self, **kwargs): - models.CharField.__init__(self, max_length=255, **kwargs) - -class IPAddressField(models.CharField): - def __init__( self, **kwargs ): - models.CharField.__init__(self, max_length=40, **kwargs) class TelephoneField(models.CharField): - def __init__( self, **kwargs ): + def __init__(self, **kwargs): models.CharField.__init__(self, max_length=40, **kwargs) -class Conf(models.Model): - '''This is the center of the universe, also known as a place to - have a handle on a resource-holding entity. It's the <self> - in the rpkid schema.''' - handle = HandleField(unique=True, db_index=True) - owner = models.ManyToManyField(User) - # NULL if self-hosted, otherwise the conf that is hosting us - host = models.ForeignKey('Conf', related_name='hosting', null=True, blank=True) +class Parent(rpki.irdb.models.Parent): + """proxy model for irdb Parent""" def __unicode__(self): - return self.handle + return u"%s's parent %s" % (self.issuer.handle, self.handle) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.parent_detail', [str(self.pk)]) + + class Meta: + proxy = True + verbose_name = 'Parent' + -class Child(models.Model): - conf = models.ForeignKey(Conf, related_name='children') - handle = HandleField() # parent's name for child - valid_until = models.DateTimeField(help_text='date and time when authorization to use delegated resources ends') +class Child(rpki.irdb.models.Child): + """proxy model for irdb Child""" def __unicode__(self): - return u"%s's child %s" % (self.conf, self.handle) + return u"%s's child %s" % (self.issuer.handle, self.handle) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.child_view', [self.handle]) + return ('rpki.gui.app.views.child_view', [str(self.pk)]) class Meta: - verbose_name_plural = "children" - # children of a specific configuration should be unique - unique_together = ('conf', 'handle') - -class AddressRange(models.Model): - '''An address range/prefix.''' - lo = IPAddressField(blank=False) - hi = IPAddressField(blank=False) - # parent address range - parent = models.ForeignKey('AddressRange', related_name='children', - blank=True, null=True) - # child to which this resource is delegated - allocated = models.ForeignKey('Child', related_name='address_range', - blank=True, null=True) + proxy = True + verbose_name_plural = 'Children' + + +class ChildASN(rpki.irdb.models.ChildASN): + """Proxy model for irdb ChildASN.""" class Meta: - ordering = ['lo', 'hi'] + proxy = True def __unicode__(self): - if self.lo == self.hi: - return u"%s" % (self.lo,) - - try: - # pretty print cidr - return unicode(self.as_resource_range()) - except socket.error, err: - print err - # work around for bug when hi/lo get reversed - except AssertionError, err: - print err - return u'%s - %s' % (self.lo, self.hi) + return u'AS%s' % self.as_resource_range() - #__unicode__.admin_order_field = 'lo' - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.address_view', [str(self.pk)]) - - def as_resource_range(self): - '''Convert to rpki.resource_set.resource_range_ip.''' - return str_to_range(self.lo, self.hi) - - def is_prefix(self): - '''Returns True if this address range can be represented as a - prefix.''' - try: - self.as_resource_range().prefixlen() - except rpki.exceptions.MustBePrefix, err: - return False - return True - -class Asn(models.Model): - '''An ASN or range thereof.''' - lo = models.IntegerField(blank=False) - hi = models.IntegerField(blank=False) - # parent asn range - parent = models.ForeignKey('Asn', related_name='children', - blank=True, null=True) - # child to which this resource is delegated - allocated = models.ForeignKey(Child, related_name='asn', - blank=True, null=True) +class ChildNet(rpki.irdb.models.ChildNet): + """Proxy model for irdb ChildNet.""" class Meta: - ordering = ['lo', 'hi'] + proxy = True def __unicode__(self): - if self.lo == self.hi: - return u"ASN %d" % (self.lo,) - else: - return u"ASNs %d - %d" % (self.lo, self.hi) + return u'%s' % self.as_resource_range() - #__unicode__.admin_order_field = 'lo' - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.asn_view', [str(self.pk)]) +class Conf(rpki.irdb.models.ResourceHolderCA): + """This is the center of the universe, also known as a place to + have a handle on a resource-holding entity. It's the <self> + in the rpkid schema. - def as_resource_range(self): - # we force conversion to long() here because resource_range_as() wants - # the type of both arguments to be identical, and models.IntegerField - # will be a long when the value is large - return rpki.resource_set.resource_range_as(long(self.lo), long(self.hi)) + """ + @property + def parents(self): + """Simulates irdb.models.Parent.objects, but returns app.models.Parent + proxy objects. -class Parent(models.Model): - conf = models.ForeignKey(Conf, related_name='parents') - handle = HandleField() # my name for this parent + """ + return Parent.objects.filter(issuer=self) - def __unicode__(self): - return u"%s's parent %s" % (self.conf, self.handle) + @property + def children(self): + """Simulates irdb.models.Child.objects, but returns app.models.Child + proxy objects. + + """ + return Child.objects.filter(issuer=self) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.parent_view', [self.handle]) + return ('rpki.gui.app.views.user_detail', [str(self.pk)]) class Meta: - # parents of a specific configuration should be unique - unique_together = ('conf', 'handle') + proxy = True + class ResourceCert(models.Model): - parent = models.ForeignKey(Parent, related_name='resources') + """Represents a resource certificate. - # resources granted from my parent - asn = models.ManyToManyField(Asn, related_name='from_cert', blank=True, - null=True) - address_range = models.ManyToManyField(AddressRange, - related_name='from_cert', blank=True, null=True) + This model is used to cache the output of <list_received_resources/>. - # unique id for this resource certificate - # FIXME: URLField(verify_exists=False) doesn't seem to work - the admin - # editor won't accept a rsync:// scheme as valid - uri = models.CharField(max_length=200) + """ + # pointer to the parent object in the irdb + parent = models.ForeignKey(Parent, related_name='certs') # certificate validity period not_before = models.DateTimeField() not_after = models.DateTimeField() + # Locator for this object. Used to look up the validation status, expiry + # of ancestor certs in cacheview + uri = models.CharField(max_length=255) + def __unicode__(self): - return u"%s's resource cert from parent %s" % (self.parent.conf.handle, - self.parent.handle) + return u"%s's cert from %s" % (self.parent.issuer.handle, + self.parent.handle) + -class Roa(models.Model): - '''Maps an ASN to the set of prefixes it can originate routes for. - This differs from a real ROA in that prefixes from multiple - parents/resource certs can be selected. The glue module contains - code to split the ROAs into groups by common resource certs.''' +class ResourceRangeAddressV4(rpki.gui.models.PrefixV4): + cert = models.ForeignKey(ResourceCert, related_name='address_ranges') - conf = models.ForeignKey(Conf, related_name='roas') - asn = models.IntegerField() - active = models.BooleanField() - # the resource cert from which all prefixes for this roa are derived - cert = models.ForeignKey(ResourceCert, related_name='roas') +class ResourceRangeAddressV6(rpki.gui.models.PrefixV6): + cert = models.ForeignKey(ResourceCert, related_name='address_ranges_v6') + + +class ResourceRangeAS(rpki.gui.models.ASN): + cert = models.ForeignKey(ResourceCert, related_name='asn_ranges') + + +class ROARequest(rpki.irdb.models.ROARequest): + class Meta: + proxy = True def __unicode__(self): - return u"%s's ROA for %d" % (self.conf, self.asn) + return u"%s's ROA request for AS%d" % (self.issuer.handle, self.asn) - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.roa_view', [str(self.pk)]) -class RoaRequest(models.Model): - roa = models.ForeignKey(Roa, related_name='from_roa_request') - max_length = models.IntegerField() - prefix = models.ForeignKey(AddressRange, related_name='roa_requests') +class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix): + class Meta: + proxy = True + verbose_name = 'ROA' def __unicode__(self): - return u'roa request for asn %d on %s-%d' % (self.roa.asn, self.prefix, - self.max_length) - - def as_roa_prefix(self): - '''Convert to a rpki.resouce_set.roa_prefix subclass.''' - r = self.prefix.as_resource_range() - if isinstance(r, rpki.resource_set.resource_range_ipv4): - return rpki.resource_set.roa_prefix_ipv4(r.min, r.prefixlen(), - self.max_length) - else: - return rpki.resource_set.roa_prefix_ipv6(r.min, r.prefixlen(), - self.max_length) + return u'ROA request prefix %s for asn %d' % (str(self.as_roa_prefix()), + self.roa_request.asn) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.roa_request_view', [str(self.pk)]) + return ('rpki.gui.app.views.roa_detail', [str(self.pk)]) + -class Ghostbuster(models.Model): +class GhostbusterRequest(rpki.irdb.models.GhostbusterRequest): """ - Stores the information require to fill out a vCard entry to populate - a ghostbusters record. + Stores the information require to fill out a vCard entry to + populate a ghostbusters record. + + This model is inherited from the irdb GhostBusterRequest model so + that the broken out fields can be included for ease of editing. """ + full_name = models.CharField(max_length=40) # components of the vCard N type - family_name = models.CharField(max_length=20) - given_name = models.CharField(max_length=20) - additional_name = models.CharField(max_length=20, blank=True, null=True) + family_name = models.CharField(max_length=20) + given_name = models.CharField(max_length=20) + additional_name = models.CharField(max_length=20, blank=True, null=True) honorific_prefix = models.CharField(max_length=10, blank=True, null=True) honorific_suffix = models.CharField(max_length=10, blank=True, null=True) - email_address = models.EmailField(blank=True, null=True) - organization = models.CharField(blank=True, null=True, max_length=255) - telephone = TelephoneField(blank=True, null=True) + email_address = models.EmailField(blank=True, null=True) + organization = models.CharField(blank=True, null=True, max_length=255) + telephone = TelephoneField(blank=True, null=True) # elements of the ADR type - box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, max_length=40) + box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, + max_length=40) extended = models.CharField(blank=True, null=True, max_length=255) - street = models.CharField(blank=True, null=True, max_length=255) - city = models.CharField(blank=True, null=True, max_length=40) - region = models.CharField(blank=True, null=True, max_length=40, help_text='state or province') - code = models.CharField(verbose_name='Postal Code', blank=True, null=True, max_length=40) - country = models.CharField(blank=True, null=True, max_length=40) - - conf = models.ForeignKey(Conf, related_name='ghostbusters') - # parent can be null when using the same record for all parents - parent = models.ManyToManyField(Parent, related_name='ghostbusters', - blank=True, null=True, help_text='use this record for a specific parent, or leave blank for all parents') + street = models.CharField(blank=True, null=True, max_length=255) + city = models.CharField(blank=True, null=True, max_length=40) + region = models.CharField(blank=True, null=True, max_length=40, + help_text='state or province') + code = models.CharField(verbose_name='Postal Code', blank=True, null=True, + max_length=40) + country = models.CharField(blank=True, null=True, max_length=40) def __unicode__(self): - return u"%s's GBR: %s" % (self.conf, self.full_name) + return u"%s's GBR: %s" % (self.issuer.handle, self.full_name) @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.ghostbuster_view', [str(self.pk)]) class Meta: - ordering = [ 'family_name', 'given_name' ] + ordering = ('family_name', 'given_name') + verbose_name = 'Ghostbuster' + + +class Timestamp(models.Model): + """Model to hold metadata about the collection of external data. + + This model is a hash table mapping a timestamp name to the + timestamp value. All timestamps values are in UTC. + + The utility function rpki.gui.app.timestmap.update(name) should be used to + set timestamps rather than updating this model directly.""" + + name = models.CharField(max_length=30, primary_key=True) + ts = models.DateTimeField(null=False) + + def __unicode__(self): + return '%s: %s' % (self.name, self.ts) + + +class Repository(rpki.irdb.models.Repository): + class Meta: + proxy = True + verbose_name_plural = 'Repositories' + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.repository_detail', [str(self.pk)]) + + def __unicode__(self): + return "%s's repository %s" % (self.issuer.handle, self.handle) + + +class Client(rpki.irdb.models.Client): + "Proxy model for pubd clients." + + class Meta: + proxy = True + verbose_name = 'Client' + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.client_detail', [str(self.pk)]) + + def __unicode__(self): + return self.handle -# vim:sw=4 ts=8 expandtab + +class RouteOrigin(rpki.gui.routeview.models.RouteOrigin): + class Meta: + proxy = True + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.route_detail', [str(self.pk)]) + + +class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6): + class Meta: + proxy = True + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.route_detail', [str(self.pk)]) diff --git a/rpkid/rpki/gui/app/range_list.py b/rpkid/rpki/gui/app/range_list.py new file mode 100755 index 00000000..fcfcfc24 --- /dev/null +++ b/rpkid/rpki/gui/app/range_list.py @@ -0,0 +1,244 @@ +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' + +import bisect +import unittest + +class RangeList(list): + """A sorted list of ranges, which automatically merges adjacent ranges. + + Items in the list are expected to have ".min" and ".max" attributes.""" + + def __init__(self, ini=None): + list.__init__(self) + if ini: + self.extend(ini) + + def append(self, v): + keys = [x.min for x in self] + + # lower bound + i = bisect.bisect_left(keys, v.min) + + # upper bound + j = bisect.bisect_right(keys, v.max, lo=i) + + # if the max value for the previous item is greater than v.min, include the previous item in the range to replace + # and use its min value. also include the previous item if the max value is 1 less than the min value for the + # inserted item + if i > 0 and self[i-1].max >= v.min - 1: + i = i - 1 + vmin = self[i].min + else: + vmin = v.min + + # if the max value for the previous item is greater than the max value for the new item, use the previous item's max + if j > 0 and self[j-1].max > v.max: + vmax = self[j-1].max + else: + vmax = v.max + + # if the max value for the new item is 1 less than the min value for the next item, combine into a single item + if j < len(self) and vmax+1 == self[j].min: + vmax = self[j].max + j = j+1 + + # replace the range with a new object covering the entire range + self[i:j] = [v.__class__(min=vmin, max=vmax)] + + def extend(self, args): + for x in args: + self.append(x) + + def difference(self, other): + """Return a RangeList object which contains ranges in this object which are not in "other".""" + it = iter(other) + + try: + cur = it.next() + except StopIteration: + return self + + r = RangeList() + + for x in self: + xmin = x.min + + def V(v): + """convert the integer value to the appropriate type for this + range""" + return x.__class__.datum_type(v) + + try: + while xmin <= x.max: + if xmin < cur.min: + r.append(x.__class__(min=V(xmin), + max=V(min(x.max,cur.min-1)))) + xmin = cur.max+1 + elif xmin == cur.min: + xmin = cur.max+1 + else: # xmin > cur.min + if xmin <= cur.max: + xmin = cur.max+1 + else: # xmin > cur.max + cur = it.next() + + except StopIteration: + r.append(x.__class__(min=V(xmin), max=x.max)) + + return r + +class TestRangeList(unittest.TestCase): + class MinMax(object): + def __init__(self, min, max): + self.min = min + self.max = max + + def __str__(self): + return '(%d, %d)' % (self.min, self.max) + + def __repr__(self): + return '<MinMax: (%d, %d)>' % (self.min, self.max) + + def __eq__(self, other): + return self.min == other.min and self.max == other.max + + def setUp(self): + self.v1 = TestRangeList.MinMax(1,2) + self.v2 = TestRangeList.MinMax(4,5) + self.v3 = TestRangeList.MinMax(7,8) + self.v4 = TestRangeList.MinMax(3,4) + self.v5 = TestRangeList.MinMax(2,3) + self.v6 = TestRangeList.MinMax(1,10) + + def test_empty_append(self): + s = RangeList() + s.append(self.v1) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v1) + + def test_no_overlap(self): + s = RangeList() + s.append(self.v1) + s.append(self.v2) + self.assertTrue(len(s) == 2) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + + def test_no_overlap_prepend(self): + s = RangeList() + s.append(self.v2) + s.append(self.v1) + self.assertTrue(len(s) == 2) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + + def test_insert_middle(self): + s = RangeList() + s.append(self.v1) + s.append(self.v3) + s.append(self.v2) + self.assertTrue(len(s) == 3) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + self.assertEqual(s[2], self.v3) + + def test_append_overlap(self): + s = RangeList() + s.append(self.v1) + s.append(self.v5) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,3)) + + def test_combine_range(self): + s = RangeList() + s.append(self.v1) + s.append(self.v4) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,4)) + + def test_append_subset(self): + s = RangeList() + s.append(self.v6) + s.append(self.v3) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_append_equal(self): + s = RangeList() + s.append(self.v6) + s.append(self.v6) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_prepend_combine(self): + s = RangeList() + s.append(self.v4) + s.append(self.v1) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,4)) + + def test_append_aggregate(self): + s = RangeList() + s.append(self.v1) + s.append(self.v2) + s.append(self.v3) + s.append(self.v6) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_diff_empty(self): + s = RangeList() + s.append(self.v1) + self.assertEqual(s, s.difference([])) + + def test_diff_self(self): + s = RangeList() + s.append(self.v1) + self.assertEqual(s.difference(s), []) + + def test_diff_middle(self): + s1 = RangeList([self.v6]) + s2 = RangeList([self.v3]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,6), TestRangeList.MinMax(9, 10)])) + + def test_diff_overlap(self): + s1 = RangeList([self.v2]) + s2 = RangeList([self.v4]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(5,5)])) + + def test_diff_overlap2(self): + s1 = RangeList([self.v2]) + s2 = RangeList([self.v4]) + self.assertEqual(s2.difference(s1), RangeList([TestRangeList.MinMax(3,3)])) + + def test_diff_multi(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(4,5)]) + s2 = RangeList([TestRangeList.MinMax(4,4)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(5,5)])) + + def test_diff_multi_overlap(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4)]) + s2 = RangeList([TestRangeList.MinMax(2,3)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4)])) + + def test_diff_multi_overlap2(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4), TestRangeList.MinMax(6,7)]) + s2 = RangeList([TestRangeList.MinMax(2,3), TestRangeList.MinMax(6,6)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4), TestRangeList.MinMax(7,7)])) + +if __name__ == '__main__': + unittest.main() diff --git a/rpkid/rpki/gui/app/settings.py.in b/rpkid/rpki/gui/app/settings.py.in index 28410f35..fcfe4678 100644 --- a/rpkid/rpki/gui/app/settings.py.in +++ b/rpkid/rpki/gui/app/settings.py.in @@ -7,16 +7,4 @@ from django.conf import settings -# directory containing the resource handles served by the rpki portal gui -CONFDIR = settings.MYRPKI if hasattr(settings, 'CONFDIR') else '%(AC_LOCALSTATEDIR)s/rpki/conf' - -# maildir-style mailbox where uploaded requests are saved -INBOX = settings.MYRPKI if hasattr(settings, 'INBOX') else '%(AC_LOCALSTATEDIR)s/rpki/inbox' - -# maildir-style mailbox where responses to client requests are stored -OUTBOX = settings.MYRPKI if hasattr(settings, 'OUTBOX') else '%(AC_LOCALSTATEDIR)s/rpki/outbox' - -# uid the web server runs as -WEB_USER = settings.MYRPKI if hasattr(settings, 'WEB_USER') else '%(AC_WEBUSER)s' - -RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/gui/rpki.conf.template' +RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/rpki.conf.template' diff --git a/rpkid/rpki/gui/app/templates/app/app_base.html b/rpkid/rpki/gui/app/templates/app/app_base.html new file mode 100644 index 00000000..c7901115 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/app_base.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{# This template defines the common structure for the rpki.gui.app application. #} + +{% block sidebar %} + +<h2>{{ request.session.handle }}</h2> + +{# common navigation #} + +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.dashboard %}">dashboard</a></li> + <li><a href="{% url rpki.gui.app.views.route_view %}">routes</a></li> + <li><a href="{% url rpki.gui.app.views.parent_list %}">parents</a></li> + <li><a href="{% url rpki.gui.app.views.child_list %}">children</a></li> + <li><a href="{% url rpki.gui.app.views.roa_list %}">roas</a></li> + <li><a href="{% url rpki.gui.app.views.ghostbuster_list %}">ghostbusters</a></li> + <li><a href="{% url rpki.gui.app.views.repository_list %}">repositories</a></li> +</ul> + +{% if request.user.is_superuser %} +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.client_list %}">pubclients</a></li> + <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li> + <li><a href="{% url rpki.gui.app.views.user_list %}" title="manage users">users</a></li> +</ul> +{% endif %} + +{% block sidebar_extra %}{% endblock %} + +{% endblock sidebar %} diff --git a/rpkid/rpki/gui/app/templates/app/bootstrap_form.html b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html new file mode 100644 index 00000000..bf8b3553 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html @@ -0,0 +1,33 @@ +{# vim:set ft=htmldjango #} + +{% if form.non_field_errors %} +<div class='alert-message error'> + <p>{{ form.non_field_errors }} +</div> +{% endif %} + +{% for field in form %} + +{% if field.is_hidden %} + {{ field }} +{% else %} + <div class='clearfix {% if field.errors %}error{% endif %}'> + {{ field.label_tag }} + {% if field.required %}*{% endif %} + <div class='input'> + {{ field }} + {% if field.help_text %} + <span class='help-inline'>{{ field.help_text }}</span> + {% endif %} + {% if field.errors %} + <ul> + {% for error in field.errors %} + <li class='help-inline'>{{ error }}</li> + {% endfor %} + </ul> + {% endif %} + </div><!-- input --> + </div><!-- clearfix --> +{% endif %} + +{% endfor %} diff --git a/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html new file mode 100644 index 00000000..98789191 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> + <h1>Add Resource: {{ object.handle }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html b/rpkid/rpki/gui/app/templates/app/child_delete_form.html index 22c40a60..22c40a60 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html +++ b/rpkid/rpki/gui/app/templates/app/child_delete_form.html diff --git a/rpkid/rpki/gui/app/templates/app/child_detail.html b/rpkid/rpki/gui/app/templates/app/child_detail.html new file mode 100644 index 00000000..b180633d --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_detail.html @@ -0,0 +1,53 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Child Handle</strong> + </div> + <div class='span2'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>Valid until</strong> + </div> + <div class='span4'> + <p>{{ object.valid_until }} + </div> +</div> + +<div class='row'> + <div class='span4'> + <strong>Addresses</strong> + {% if object.address_ranges.all %} + <ul class='unstyled'> + {% for a in object.address_ranges.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> + <div class='span4'> + <strong>ASNs</strong> + {% if object.asns.all %} + <ul class='unstyled'> + {% for a in object.asns.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> +</div> +{% endblock object_detail %} + +{% block actions %} +<a class='btn' href="{{ object.get_absolute_url }}/add_asn" title='Delegate an ASN to this child'>+AS</a> +<a class='btn' href="{{ object.get_absolute_url }}/add_address" title='Delegate a prefix to this child'>+Prefix</a> +<a class='btn' href="{{ object.get_absolute_url }}/export" title='Download XML file to send to child'>Export</a> +{% endblock actions %} diff --git a/rpkid/rpki/gui/app/templates/app/child_form.html b/rpkid/rpki/gui/app/templates/app/child_form.html new file mode 100644 index 00000000..cd9b2a8c --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_form.html @@ -0,0 +1,17 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> + <h1>Edit Child: {{ object.handle }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a> + </div> +</form> + +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/child_import_form.html b/rpkid/rpki/gui/app/templates/app/child_import_form.html new file mode 100644 index 00000000..4b0cf9d2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_import_form.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Child</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a> + </div> +</form> + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/child_list.html b/rpkid/rpki/gui/app/templates/app/child_list.html new file mode 100644 index 00000000..9ba31ffd --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_list.html @@ -0,0 +1,7 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock object_detail %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/client_detail.html b/rpkid/rpki/gui/app/templates/app/client_detail.html new file mode 100644 index 00000000..4755fbca --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/client_detail.html @@ -0,0 +1,20 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Name</strong> + </div> + <div class='span6'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>SIA</strong> + </div> + <div class='span6'> + <p>{{ object.sia_base }} + </div> +</div> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html b/rpkid/rpki/gui/app/templates/app/client_import_form.html index acd6bf61..acd6bf61 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html +++ b/rpkid/rpki/gui/app/templates/app/client_import_form.html diff --git a/rpkid/rpki/gui/app/templates/app/client_list.html b/rpkid/rpki/gui/app/templates/app/client_list.html new file mode 100644 index 00000000..a2a0a5a2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/client_list.html @@ -0,0 +1 @@ +{% extends "app/object_list.html" %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html b/rpkid/rpki/gui/app/templates/app/conf_empty.html index 0ef9366c..0ef9366c 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html +++ b/rpkid/rpki/gui/app/templates/app/conf_empty.html diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html b/rpkid/rpki/gui/app/templates/app/conf_list.html index 4bb18114..4bb18114 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html +++ b/rpkid/rpki/gui/app/templates/app/conf_list.html diff --git a/rpkid/rpki/gui/app/templates/app/dashboard.html b/rpkid/rpki/gui/app/templates/app/dashboard.html new file mode 100644 index 00000000..f74dad09 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/dashboard.html @@ -0,0 +1,85 @@ +{% extends "app/app_base.html" %} + +{% block sidebar_extra %} +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li> +</ul> + +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li> +</ul> +{% endblock sidebar_extra %} + +{% block content %} +<div class='page-header'> + <h1>Dashboard</h1> +</div> + +<div class='row'> + <div class='span10'> + <h2>Resources</h2> + <table class='condensed-table zebra-striped'> + <tr> + <th>Resource</th> + <th>Valid Until</th> + <th>Parent</th> + </tr> + + {% for object in asns %} + <tr> + <td>{{ object }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + + {% for object in prefixes %} + <tr> + <td>{{ object.as_resource_range }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + + {% if prefixes_v6 %} + {% for object in prefixes_v6 %} + <tr> + <td>{{ object.as_resource_range }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + {% endif %} + </table> + </div> + <div class='span6'> + <h2>Unallocated Resources</h2> + <p>The following resources have not been allocated to a child, nor appear in a ROA. + + {% if unused_asns %} + <ul> + {% for asn in unused_asns %} + <li>AS{{ asn }} + {% endfor %} <!-- ASNs --> + </ul> + {% endif %} + + {% if unused_prefixes %} + <ul> + {% for addr in unused_prefixes %} + <li>{{ addr }} + {% endfor %} <!-- addrs --> + </ul> + {% endif %} + + {% if unused_prefixes_v6 %} + <ul> + {% for addr in unused_prefixes_v6 %} + <li>{{ addr }} + {% endfor %} <!-- addrs --> + </ul> + {% endif %} + + </div><!-- /span --> +</div><!-- /row --> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html index e1e6711f..e1e6711f 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html +++ b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html diff --git a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html b/rpkid/rpki/gui/app/templates/app/generic_result.html index 65d4e42e..65d4e42e 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html +++ b/rpkid/rpki/gui/app/templates/app/generic_result.html diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html new file mode 100644 index 00000000..76b1d25a --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html @@ -0,0 +1,20 @@ +{% extends "app/ghostbuster_detail.html" %} + +{% block extra %} + +<div class='alert-message block-message warning'> + <p> + <strong>Please confirm</strong> that you really want to delete by clicking Delete. + + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <input class='btn danger' type='submit' value='Delete' /> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </form> + </div> +</div> + +{% endblock %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html new file mode 100644 index 00000000..b6f28815 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html @@ -0,0 +1,21 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Edit Ghostbuster Request</h1> +</div> + +<form action='{{ request.get_full_path }}' method='POST'> + {% csrf_token %} + + {# include code to render form using Twitter Bootstrap CSS Framework #} + {% include "app/bootstrap_form.html" %} + + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a> + </div> + +</form> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html new file mode 100644 index 00000000..fa8915e4 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html @@ -0,0 +1,53 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<table class='zebra-striped condensed-table'> + <tr><td >Full Name</td><td>{{ object.full_name }}</td></tr> + + {% if object.honorific_prefix %} + <tr><td >Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr> + {% endif %} + + {% if object.organization %} + <tr><td >Organization</td><td>{{ object.organization }}</td></tr> + {% endif %} + + {% if object.telephone %} + <tr><td >Telephone</td><td>{{ object.telephone }}</td></tr> + {% endif %} + + {% if object.email_address %} + <tr><td >Email</td><td>{{ object.email_address }}</td></tr> + {% endif %} + + {% if object.box %} + <tr><td >P.O. Box</td><td>{{ object.box }}</td></tr> + {% endif %} + + {% if object.extended %} + <tr><td >Extended Address</td><td>{{ object.extended }}</td></tr> + {% endif %} + + {% if object.street %} + <tr><td >Street Address</td><td>{{ object.street }}</td></tr> + {% endif %} + + {% if object.city %} + <tr><td >City</td><td>{{ object.city }}</td></tr> + {% endif %} + + {% if object.region %} + <tr><td >Region</td><td>{{ object.region }}</td></tr> + {% endif %} + + {% if object.code %} + <tr><td >Postal Code</td><td>{{ object.code }}</td></tr> + {% endif %} + + {% if object.country %} + <tr><td >Country</td><td>{{ object.country }}</td></tr> + {% endif %} + +</table> +{% endblock object_detail %} +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html new file mode 100644 index 00000000..327b79b1 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html @@ -0,0 +1,13 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.full_name }}</a></li> +{% endblock object_detail %} + +{% block actions %} +<div class='actions'> + <a class='btn' href='{% url rpki.gui.app.views.ghostbuster_create %}' title='Create a new Ghostbuster Request'>Create</a> +</div> +{% endblock actions %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html b/rpkid/rpki/gui/app/templates/app/initialize_form.html index 372316ee..372316ee 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html +++ b/rpkid/rpki/gui/app/templates/app/initialize_form.html diff --git a/rpkid/rpki/gui/app/templates/app/object_detail.html b/rpkid/rpki/gui/app/templates/app/object_detail.html new file mode 100644 index 00000000..6a93f644 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_detail.html @@ -0,0 +1,33 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{% block content %} +<div class='page-header'> + <h1>{% verbose_name object %}</h1> +</div> + +{% block object_detail %} +{{ object }} +{% endblock object_detail %} + +{% if confirm_delete %} +<div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete this object. + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <input class='btn danger' type='submit' value='Delete'/> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </form> + </div> +</div> +{% else %} +<div class='actions'> + {% if can_edit %} + <a class='btn' href='{{ object.get_absolute_url }}/edit'>Edit</a> + {% endif %} + <a class='btn danger' href='{{ object.get_absolute_url }}/delete' title='Permanently delete this object'>Delete</a> + {% block actions %}{% endblock actions %} +</div> +{% endif %} +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/object_list.html b/rpkid/rpki/gui/app/templates/app/object_list.html new file mode 100644 index 00000000..e78eab98 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_list.html @@ -0,0 +1,36 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{# generic object list #} + +{% block content %} + +<div class='page-header'> + <h1>{% verbose_name_plural object_list %}</h1> +</div> + +{% if object_list %} +<ul> + {% for object in object_list %} + {% block object_detail %} + <li><a href="{{ object.get_absolute_url }}">{{ object }}</a></li> + {% endblock %} + {% endfor %} +</ul> +{% else %} +<div class='alert-message warning'> + <p>There are <strong>no items</strong> in this list. +</div> +{% endif %} + +{% block actions %} +{% if create_url %} +<div class='actions'> + <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a> +</div> +{% endif %} +{% endblock %} + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/object_table.html b/rpkid/rpki/gui/app/templates/app/object_table.html new file mode 100644 index 00000000..4d154490 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_table.html @@ -0,0 +1,41 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{# Generic object list displayed as a table. #} + +{% block content %} + +<div class='page-header'> + <h1>{% verbose_name_plural object_list %}</h1> +</div> + +{% if object_list %} +<table style='zebra-striped condensed-table'> + <tr> + {% block table_header %}{% endblock %} + </tr> + {% for object in object_list %} + <tr> + {% block object_detail %} + <td><a href="{{ object.get_absolute_url }}">{{ object }}</a></td> + {% endblock %} + </tr> + {% endfor %} +</table> +{% else %} +<div class='alert-message warning'> + <p>There are <strong>no items</strong> in this list. +</div> +{% endif %} + +{% block actions %} +{% if create_url %} +<div class='actions'> + <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a> +</div> +{% endif %} +{% endblock %} + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/parent_detail.html b/rpkid/rpki/gui/app/templates/app/parent_detail.html new file mode 100644 index 00000000..e5703074 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_detail.html @@ -0,0 +1,62 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<h2>{{ object.handle }}</h2> + +<table> + <tr> + <td>service_uri</td> + <td>{{ object.service_uri }}</td> + </tr> + <tr> + <td>parent_handle</td> + <td>{{ object.parent_handle }}</td> + </tr> + <tr> + <td>child_handle</td> + <td>{{ object.child_handle }}</td> + </tr> + <tr> + <td>repository_type</td> + <td>{{ object.repository_type }}</td> + </tr> + <tr> + <td>referrer</td> + <td>{{ object.referrer }}</td> + </tr> + <tr> + <td>ta validity period</td> + <td>{{ object.ta.getNotBefore }} - {{ object.ta.getNotAfter }}</td> + </tr> +</table> + +<div class='row'> + <div class='span4'> + <h3>Delegated Addresses</h3> + <ul class='unstyled'> + {% for c in object.certs.all %} + {% for a in c.address_ranges.all %} + <li>{{ a }}</li> + {% endfor %} + {% for a in c.address_ranges_v6.all %} + <li>{{ a }}</li> + {% endfor %} + {% endfor %} + </ul> + </div> + <div class='span4'> + <h3>Delegated ASNs</h3> + <ul class='unstyled'> + {% for c in object.certs.all %} + {% for a in c.asn_ranges.all %} + <li>{{ a }}</li> + {% endfor %} + {% endfor %} + </ul> + </div> +</div> +{% endblock object_detail %} + +{% block actions %} +<a class='btn' href='{{ object.get_absolute_url }}/export' title='Download XML to send to repository operator'>Export</a> +{% endblock actions %} diff --git a/rpkid/rpki/gui/app/templates/app/parent_import_form.html b/rpkid/rpki/gui/app/templates/app/parent_import_form.html new file mode 100644 index 00000000..c192a4a4 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_import_form.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Parent</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.parent_list %}">Cancel</a> + </div> +</form> + +{% endblock content %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/parent_list.html b/rpkid/rpki/gui/app/templates/app/parent_list.html new file mode 100644 index 00000000..81744130 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_list.html @@ -0,0 +1,5 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/pubclient_list.html b/rpkid/rpki/gui/app/templates/app/pubclient_list.html new file mode 100644 index 00000000..0296dcdf --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/pubclient_list.html @@ -0,0 +1,9 @@ +{% extends "app/object_list.html" %} + +{% block actions %} +<div class='actions'> + <a class='btn' href='{% url rpki.gui.app.views.pubclient_import %}'>Import</a> +</div> +{% endblock actions %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/repository_detail.html b/rpkid/rpki/gui/app/templates/app/repository_detail.html new file mode 100644 index 00000000..599357bd --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_detail.html @@ -0,0 +1,20 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Name</strong> + </div> + <div class='span6'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>SIA</strong> + </div> + <div class='span6'> + <p>{{ object.sia_base }}</td> + </div> +</div> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/repository_import_form.html b/rpkid/rpki/gui/app/templates/app/repository_import_form.html new file mode 100644 index 00000000..bf79e59c --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_import_form.html @@ -0,0 +1,18 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Repository</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.repository_list %}">Cancel</a> + </div> +</form> + +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/repository_list.html b/rpkid/rpki/gui/app/templates/app/repository_list.html new file mode 100644 index 00000000..2ccd0223 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_list.html @@ -0,0 +1,7 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html new file mode 100644 index 00000000..4c8228b6 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html @@ -0,0 +1,54 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> +<h1>Delete ROA Prefix</h1> +</div> + +<div class='row'> + <div class='span8'> + <div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete the following ROA Request. The table to the right indicates how validation status for matching routes may change. + + <table style='condensed-table'> + <tr> + <th>Prefix</th> + <th>Max Length</th> + <th>AS</th> + <tr> + <td>{{ object.prefix }}/{{ object.prefixlen }}</td> + <td>{{ object.max_prefixlen }}</td> + <td>{{ object.roa_request.asn }}</td> + </tr> + </table> + + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <div class='alert-actions'> + <input class='btn danger' type='submit' value='Delete'/> + <a class='btn' href="{% url rpki.gui.app.views.roa_list %}">Cancel</a> + </div> + </form> + </div> + </div> + + <div class='span8'> + <h2>Matching Routes</h2> + + <table style='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + </tr> + {% endfor %} + </table> + </div><!-- /span8 --> +</div><!-- /row --> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_list.html b/rpkid/rpki/gui/app/templates/app/roa_request_list.html new file mode 100644 index 00000000..9ffe4f57 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roa_request_list.html @@ -0,0 +1,14 @@ +{% extends "app/object_table.html" %} + +{% block table_header %} +<th>Prefix</th><th>Max Length</th><th>ASN</th><th>Action</th> +{% endblock %} + +{% block object_detail %} +<td>{{ object.prefix }}/{{ object.prefixlen }}</a></td> +<td>{{ object.max_prefixlen }}</td> +<td>{{ object.roa_request.asn }}</td> +<td><a class='btn danger' href="{{ object.get_absolute_url }}/delete">Delete</a></td> +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html new file mode 100644 index 00000000..60d0b0fe --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html @@ -0,0 +1,58 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create ROA</h1> +</div> + +<div class='row'> + <div class='span8'> + <div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to create the following ROA. + The accompanying table indicates how the validation status may change as a result. + + <table class='condensed-table'> + <tr> + <th>AS</th> + <th>Prefix</th> + <th>Max Length</th> + </tr> + <tr> + <td>{{ asn }}</td> + <td>{{ prefix }}</td> + <td>{{ max_prefixlen }}</td> + </tr> + </table> + + <form method='POST' action='{% url rpki.gui.app.views.roa_create_confirm %}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='alert-actions'> + <input class='btn primary' type='submit' value='Create'/> + <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a> + </div> + </form> + </div><!-- /alert-message --> + </div> + + <div class='span8'> + <h2>Matched Routes</h2> + + <table style='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + </tr> + {% endfor %} + </table> + </div> + +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_form.html new file mode 100644 index 00000000..5385cab0 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roarequest_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create ROA</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Create'/> + <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/route_roa_list.html b/rpkid/rpki/gui/app/templates/app/route_roa_list.html new file mode 100644 index 00000000..1907315d --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/route_roa_list.html @@ -0,0 +1,19 @@ +{% extends "app/object_table.html" %} + +{# template for displaying the list of ROAs covering a specific route #} + +{% block table_header %} +<th>Prefix</th> +<th>Max Length</th> +<th>ASN</th> +<th>Expires</th> +<th>URI</th> +{% endblock %} + +{% block object_detail %} +<td>{{ object.as_resource_range }}</td> +<td>{{ object.max_length }}</td> +<td>{{ object.roas.all.0.asid }}</td> +<td>{{ object.roas.all.0.not_after }}</td> +<td>{{ object.roas.all.0.repo.uri }}</td> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/routes_view.html b/rpkid/rpki/gui/app/templates/app/routes_view.html new file mode 100644 index 00000000..be4f8f6e --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/routes_view.html @@ -0,0 +1,41 @@ +{% extends "app/app_base.html" %} + +{% block sidebar_extra %} +<p> +BGP data updated<br> +IPv4: {{ timestamp.bgp_v4_import.isoformat }}<br> +IPv6: {{ timestamp.bgp_v6_import.isoformat }} +<p> +rcynic cache updated<br> +{{ timestamp.rcynic_import.isoformat }} + +{% endblock sidebar_extra %} + +{% block content %} + +<div class='page-header'> + <h1>Route View</h1> +</div> + +<p> +This view shows currently advertised routes for the prefixes listed in resource certs received from RPKI parents. + +<table class='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td> + <span class='label {{ r.status_label }}'>{{ r.status }}</span> + <a href='{{ r.get_absolute_url }}/roa/' help='display ROAs matching this prefix'>roas</a> + </td> + </tr> + {% endfor %} +</table> + +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html index b232c4e9..b232c4e9 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html +++ b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html diff --git a/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html new file mode 100644 index 00000000..76c66775 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Delete User</h1> +</div> + +<div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete the following user account. + <h2>{{ object.handle }}</h2> + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {{ form }} + <input class='btn danger' value='Delete' type='submit'> + <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a> + </form> + </div> +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_create_form.html b/rpkid/rpki/gui/app/templates/app/user_create_form.html new file mode 100644 index 00000000..1a07402f --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_create_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create User</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Create"> + <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a> + </div> +</form> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/user_edit_form.html b/rpkid/rpki/gui/app/templates/app/user_edit_form.html new file mode 100644 index 00000000..59fc01c2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_edit_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Edit User: {{ object.username }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_list.html b/rpkid/rpki/gui/app/templates/app/user_list.html new file mode 100644 index 00000000..804e94f0 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_list.html @@ -0,0 +1,29 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Users</h1> +</div> + +<table class='zebra-striped'> + <tr> + <th>Username</th> + <th>Email</th> + <th>Action</th> + </tr> + {% for u in users %} + <tr> + <td>{{ u.0.handle }}</td> + <td>{{ u.1.email }}</td> + <td> + <a class='btn small' href='{{ u.0.get_absolute_url }}/edit'>Edit</a> + <a class='btn small danger' href='{{ u.0.get_absolute_url }}/delete'>Delete</a> + </td> + </tr> + {% endfor %} +</table> + +<div class='actions'> + <a class='btn' href="{% url rpki.gui.app.views.user_create %}" title="create a new locally hosted resource handle">Create</a> +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/base.html b/rpkid/rpki/gui/app/templates/base.html index d6c859f2..ac8abd17 100644 --- a/rpkid/rpki/gui/app/templates/base.html +++ b/rpkid/rpki/gui/app/templates/base.html @@ -1,36 +1,44 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> <html> -<head> - <title>{% block title %}RPKI{% endblock %}</title> - {% block head %}{% endblock %} - <style type="text/css"> - #header { background-color: #00ccff; border-style: solid; border-width: thin; padding-left:2em } - #sidebar { background-color: #dddddd; border-style: none solid solid; border-width: thin; float:left; min-width:9em } - #content { float:left; margin-left:1em } - ul.compact {list-style:none inside; margin-left:1em; padding-left:0} - table { border: solid 1px; border-collapse: collapse } - th { border: solid 1px; padding: 1em } - td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } - {% block css %}{% endblock %} - </style> -</head> -<body> - <div id="header"> - {% if user.is_authenticated %} - <span style="float: right; font-size: 80%;">Logged in as {{ user }} | - {% if user.is_staff %}<a href="/admin/">admin</a> |{% endif %} - <a href="{% url django.contrib.auth.views.logout %}">Log Out</a></span> - {% else %} - <span style="float: right; font-size: 80%;"><a href="{% url django.contrib.auth.views.login %}">Log In</a></span> - {% endif %} - <h1>RPKI Portal GUI</h1> - </div> + <head> + <meta name='Content-Type' content='text/html; charset=UTF-8'> + <title>{% block title %}RPKI{% endblock %}</title> + {% block head %}{% endblock %} + <link rel="stylesheet" href="/site_media/css/bootstrap.min.css"> + <style type="text/css"> + body { padding-top: 50px; } + {% block css %}{% endblock %} + </style> + </head> + <body> + <!-- TOP BAR --> + <div class="topbar"> + <div class="topbar-inner"> + <div class="container"> + <h3><a href="#">rpki.net</a></h3> - <div id='sidebar'> - {% block sidebar %}{% endblock %} - </div> + {% if user.is_authenticated %} + <ul class='nav'> + <li><p>Logged in as {{ user }}</li> + <li><a href="{% url django.contrib.auth.views.logout %}">Log Out</a></li> + </ul> + {% endif %} - <div id="content"> - {% block content %}{% endblock %} - </div> -</body> + </div> + </div> + </div><!-- topbar --> + + <!-- MAIN CONTENT --> + <div class="container-fluid"> + <div class='content'> + {% block content %}{% endblock %} + </div><!-- /content --> + + <div class="sidebar"> + {% block sidebar %}{% endblock %} + </div><!-- /sidebar --> + </div><!-- /container-fluid --> + + </body> </html> diff --git a/rpkid/rpki/gui/app/templates/registration/login.html b/rpkid/rpki/gui/app/templates/registration/login.html index f99e9a25..27ad21cf 100644 --- a/rpkid/rpki/gui/app/templates/registration/login.html +++ b/rpkid/rpki/gui/app/templates/registration/login.html @@ -3,24 +3,33 @@ {% block content %} {% if form.errors %} -<p>Your username and password didn't match. Please try again.</p> +<div class='alert-message error'> + <p>Your username and password didn't match. Please try again.</p> +</div> {% endif %} -<form method="post" action="{% url django.contrib.auth.views.login %}">{% csrf_token %} -<table> -<tr> - <td>{{ form.username.label_tag }}</td> - <td>{{ form.username }}</td> -</tr> -<tr> - <td>{{ form.password.label_tag }}</td> - <td>{{ form.password }}</td> -</tr> -</table> - -<input type="submit" value="login" /> -<input type="hidden" name="next" value="{{ next }}" /> +<form method="post" action="{% url django.contrib.auth.views.login %}"> + {% csrf_token %} + + <div class="clearfix"> + {{ form.username.label_tag }} + <div class="input"> + {{ form.username }} + </div> + </div> + + <div class="clearfix"> + {{ form.password.label_tag }} + <div class="input"> + {{ form.password }} + </div> + </div> + + <div class="actions"> + <input type="submit" value="Login" class="btn primary" /> + </div> + + <input type="hidden" name="next" value="{{ next }}" /> </form> {% endblock %} - diff --git a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html b/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html deleted file mode 100644 index 204a6677..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -table { border-collapse: collapse } -th { border: solid 1px; padding: 1em } -td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li> <a href="{{asn.get_absolute_url}}/allocate">give to child</a></li> -</ul> -{% endblock %} - -{% block content %} - -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > AS View > {{ asn }} -</p> - -<h1>AS View</h1> - -<table> - <tr> <td>ASN:</td><td>{{ asn }}</td> </tr> - {% if asn.parent %} - <tr> - <td>Suballocated from:</td> - <td><a href="{{ asn.parent.get_absolute_url }}">{{ asn.parent }}</a></td> - </tr> - {% endif %} - <tr> - <td>Received from:</td> - <td> - {% for p in parent %} - <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a> - {% endfor %} - </td> - </tr> - <tr><td>Validity:</td><td>{{ asn.from_cert.all.0.not_before }} - {{ asn.from_cert.all.0.not_after }} </td></tr> - - {% if asn.allocated %} - <tr><td>Allocated:</td><td><a href="{{asn.allocated.get_absolute_url}}">{{asn.allocated.handle}}</a></td></tr> - {% endif %} -</table> - -{% if asn.children.count %} -<h2>Suballocations</h2> - -<ul> -{% for subaddr in asn.children.all %} -<li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a> -{% endfor %} -</ul> - -{% endif %} - -{% if roas %} -<h2>ROAs</h2> -<table> - <tr><th>Prefixes</th></tr> - {% for r in roas %} - <tr> - <td style='text-align: left'> - <ul> - {% for p in r.from_roa_request.all %} - <li><a href="{{ p.prefix.get_absolute_url }}">{{ p.prefix }}</a> - {% endfor %} - </ul> - </td> - </tr> - {% endfor %} - </ul> -</table> -{% endif %} <!-- roas --> - -{% if unallocated %} -<h2>Unallocated</h2> -<ul> -{% for u in unallocated %} -<li>{{ u }} -{% endfor %} -</ul> -{% endif %} - -{% if form %} -<h2>Edit</h2> -<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %} - {{ form.as_p }} - <input type="submit"> -</form> -{% endif %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_form.html deleted file mode 100644 index 0e5a5ac2..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > -<a href="{{ child.get_absolute_url }}">{{ child.handle }}</a> > Edit -</p> - -<h1>Edit Child</h1> - -<p><span style='font-weight:bold'>Child:</span> {{ child.handle }}</p> - -<form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - {{ form.as_p }} - <input type='submit'/ value='Save'> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html b/rpkid/rpki/gui/app/templates/rpkigui/child_view.html deleted file mode 100644 index 474798ce..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="{{ child.get_absolute_url }}/edit">edit</a></li> - <li><a href="{{ child.get_absolute_url }}/export" title="download XML response file to return to child">export child response</a></li> - <li><a href="{{ child.get_absolute_url }}/export_repo" title="download XML response to publication client request">export repo response</a></li> - <li><a href="{{ child.get_absolute_url }}/delete" title="remove this handle as a RPKI child">delete</a></li> - <li><a href="{{ child.get_absolute_url }}/destroy" title="completely remove a locally hosted resource handle and gui account">destroy</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > {{ child.handle }} -</p> - -<h1>Child View</h1> - -<table> - <tr> - <td>Child</td> - <td>{{ child.handle }}</td> - </tr> - <tr> - <td>Valid until</td> - <td>{{ child.valid_until }}</td> - </tr> -</table> - -<h2>Delegated Addresses</h2> -{% if child.address_range.all %} -<ul> -{% for a in child.address_range.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<h2>Delegated ASNs</h2> -{% if child.asn.all %} -<ul> -{% for a in child.asn.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -{% if form %} -<form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - <input type='submit'/> -</form> -{% endif %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html deleted file mode 100644 index 85c85ed5..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Create"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html deleted file mode 100644 index e21eb4eb..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html +++ /dev/null @@ -1,193 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -table { border-collapse: collapse } -th { border: solid 1px; padding: 1em } -td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } -h2 { text-align:center; background-color:#dddddd } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="#parents">parents</a></li> - <li><a href="#children">children</a></li> - <li><a href="#roas">roas</a></li> - <li><a href="#ghostbusters">ghostbusters</a></li> - <li><a href="#unallocated">unallocated</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li> - <li><a href="{% url rpki.gui.app.views.update_bpki %}" title="renew all BPKI certificates">update bpki</a></li> - <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.child_wizard %}" title="create a new locally hosted resource handle">create child wizard</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.import_parent %}" title="upload XML response from remote parent">import parent</a></li> - <li><a href="{% url rpki.gui.app.views.import_repository %}" title="upload XML response from remote repository">import repository</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.import_child %}" title="import a new child's identity.xml file">import child</a></li> - <li><a href="{% url rpki.gui.app.views.import_pubclient %}" title="import XML request from a publication client">import pubclient</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li> -</ul> -{% endblock %} - -{% block content %} - -<p id='breadcrumb'>{{ request.session.handle }} > Dashboard</p> - -<h1>Dashboard</h1> - -<div class='separator'> -<a name='parents'><h2>Parents</h2></a> - -{% if request.session.handle.parents.all %} -<ul> -{% for parent in request.session.handle.parents.all %} -<li><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a> -<p> -<table> -<tr><th>Accepted Resource</th><th>Not Before</th><th>Not After</th></tr> -{% for cert in parent.resources.all %} - -{% for asn in cert.asn.all %} -<tr><td style='text-align:left'><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></td> -<td>{{cert.not_before}}</td> -<td>{{cert.not_after}}</td> -</tr> -{% endfor %} - -{% for address in cert.address_range.all %} -<tr> - <td style='text-align: left'><a href="{{ address.get_absolute_url }}">{{ address }}</a></td> - <td>{{cert.not_before}}</td> - <td>{{cert.not_after}}</td> -</tr> -{% endfor %} - -{% endfor %} <!--certs--> -</table> - -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -</div><!--parents--> - -<div class='separator'> - <a name='children'><h2>Children</h2></a> - -{% if request.session.handle.children.all %} -<ul> -{% for child in request.session.handle.children.all %} -<li><a href="{% url rpki.gui.app.views.child_view child.handle %}">{{ child.handle }}</a>, valid until {{ child.valid_until }} -{% if child.address_range.count or child.asn.count %} -<p>Delegated resources: -<ul> -{% for asn in child.asn.all %} -<li><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></li> -{% endfor %} -{% for address in child.address_range.all %} -<li><a href="{{ address.get_absolute_url}}">{{ address }}</a></li> -{% endfor %} -</ul> -{% endif %} -</li> -{% endfor %} -</ul> -<!-- -<a href="/myrpki/import/child">[add]</a> ---> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<p> -Export resources delegated to children (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.handle %}" title="ASs delegated to children">asns</a> | -<a href="{% url rpki.gui.app.views.download_prefixes request.session.handle %}" title="prefixes delegated to children">prefixes</a> - -</div> - -<div class='separator'> <!-- ROAs --> - <a name='roas'><h2>ROA Requests</h2></a> - - {% if request.session.handle.roas.all %} - <table> - <tr> <th>Prefix</th> <th>ASN</th> </tr> - - {% for roa in request.session.handle.roas.all %} - <tr> - <td style='text-align: left'> - <ul style='list-style-position: outside'> - {% for req in roa.from_roa_request.all %} - <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a> - {% endfor %} - </ul> - </td> - <td>{{ roa.asn }}</td> - </tr> - {% endfor %} - </table> - {% else %} - <p style='font-style:italic'>none</p> - {% endif %} - - <p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a> -</div><!-- roas --> - -<div class='separator'><!-- ghostbusters --> -<a name='ghostbusters'><h2>Ghostbuster Requests</h2></a> - {% if request.session.handle.ghostbusters.all %} - <ul> - {% for gbr in request.session.handle.ghostbusters.all %} - <li><a href="{{ gbr.get_absolute_url }}">{{ gbr.full_name }}</a> | - <a href="{{ gbr.get_absolute_url }}/edit">edit</a> | - <a href="{{ gbr.get_absolute_url }}/delete">delete</a> - </li> - {% endfor %} - {% else %} -<p style='font-style:italic'>none</p> - {% endif %} -</ul> -<p><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></p> -</div> - -<div class='separator'> -<a name='unallocated'><h2>Unallocated Resources</h2></a> - {% if asns or ars %} - - {% if asns %} - <ul> - {% for asn in asns %} - <li>{{ asn.as_ul|safe }} - {% endfor %} <!-- ASNs --> - </ul> - {% endif %} - - {% if ars %} - <ul> - {% for addr in ars %} - <li>{{ addr.as_ul|safe }} - {% endfor %} <!-- addrs --> - </ul> - {% endif %} - - {% else %} -<p style='font-style:italic'>none</p> - {% endif %} - - </ul> -</div> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html deleted file mode 100644 index 81f4c093..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "rpkigui/ghostbuster_detail.html" %} - -{% block extra %} - -<p> -Please confirm that you really want to delete this object by clicking Delete. -</p> - -<form method=POST action='{{ request.get_full_path }}'> - {% csrf_token %} - <input type='submit' value='Delete' /> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html deleted file mode 100644 index 4a9ed73a..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -td { padding-right: 1em } -td.label { font-weight:bold } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li><a href='{{ object.get_absolute_url }}/edit'>edit</a></li> - <li><a href='{{ object.get_absolute_url }}/delete'>delete</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> > {{ object.full_name }}</p> - -<h1>Ghostbuster View</h1> - -<table> - <tr><td class='label'>Full Name</td><td>{{ object.full_name }}</td></tr> - - {% if object.honorific_prefix %} - <tr><td class='label'>Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr> - {% endif %} - - {% if object.organization %} - <tr><td class='label'>Organization</td><td>{{ object.organization }}</td></tr> - {% endif %} - - {% if object.telephone %} - <tr><td class='label'>Telephone</td><td>{{ object.telephone }}</td></tr> - {% endif %} - - {% if object.email_address %} - <tr><td class='label'>Email</td><td>{{ object.email_address }}</td></tr> - {% endif %} - - {% if object.box %} - <tr><td class='label'>P.O. Box</td><td>{{ object.box }}</td></tr> - {% endif %} - - {% if object.extended %} - <tr><td class='label'>Extended Address</td><td>{{ object.extended }}</td></tr> - {% endif %} - - {% if object.street %} - <tr><td class='label'>Street Address</td><td>{{ object.street }}</td></tr> - {% endif %} - - {% if object.city %} - <tr><td class='label'>City</td><td>{{ object.city }}</td></tr> - {% endif %} - - {% if object.region %} - <tr><td class='label'>Region</td><td>{{ object.region }}</td></tr> - {% endif %} - - {% if object.code %} - <tr><td class='label'>Postal Code</td><td>{{ object.code }}</td></tr> - {% endif %} - - {% if object.country %} - <tr><td class='label'>Country</td><td>{{ object.country }}</td></tr> - {% endif %} - -</table> -{% block extra %}{% endblock %} -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html deleted file mode 100644 index 0d77d796..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> > Edit</p> - -<h1>Edit Ghostbuster Request</h1> - -<form action='{{ request.get_full_path }}' method='POST'> - {% csrf_token %} - <table> -{{ form.as_table }} - -</table> -<p></p><!-- add vertical space --> - <input type='submit' value='Save' /> -</form> -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html deleted file mode 100644 index 6890782d..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > Ghostbusters</p> - -<h1>Ghostbuster Requests</h1> - -{% if object_list %} -<ul> - {% for obj in object_list %} - <li><a href="{{ obj.get_absolute_url }}">{{ obj.full_name }}</a> | <a href="{{obj.get_absolute_url}}/edit">edit</a> | <a href="{{obj.get_absolute_url}}/delete">delete</a></li> - {% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html deleted file mode 100644 index 4209c537..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "rpkigui/parent_view.html" %} - -{% block form %} - -<form method="POST" action="{{ request.get_full_path }}"> -{% csrf_token %} -{{ form }} -<input type="submit" value="{{ submit_label }}"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html deleted file mode 100644 index a57bd888..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="{{ parent.get_absolute_url }}/delete">delete</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > Parent View > {{ parent.handle }} -</p> - -<h1>Parent View</h1> - -<p>Parent: {{ parent.handle }} - -<h2>Delegated Addresses</h2> -<ul> -{% for c in parent.resources.all %} -{% for a in c.address_range.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a> -{% endfor %} -{% endfor %} -</ul> - -<h2>Delegated ASNs</h2> -<ul> -{% for c in parent.resources.all %} -{% for a in c.asn.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a> -{% endfor %} -{% endfor %} -</ul> - -{% block form %}{% endblock %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html b/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html deleted file mode 100644 index 6679eff9..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> -{% if not addr.allocated %} -<li><a href="{{addr.get_absolute_url}}/split">split</a></li> -{% endif %} -{% if not addr.roa_requests.all %} -<li><a href="{{addr.get_absolute_url}}/allocate">give to child</a></li> -{% endif %} -{% if addr.is_prefix and not addr.allocated %} -<li><a href="{{ addr.get_absolute_url }}/roa">roa</a></li> -{% endif %} -{% if not addr.allocated and addr.parent %} -<li><a href="{{ addr.get_absolute_url }}/delete">delete</a></li> -{% endif %} -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > Prefix View > {{ addr }} -</p> - -<h1>Prefix View</h1> - -<table> - <tr> <td>Range:</td><td>{{ addr }}</td> </tr> - {% if addr.parent %} - <tr> - <td>Suballocated from:</td> - <td><a href="{{ addr.parent.get_absolute_url }}">{{ addr.parent }}</a></td> - </tr> - {% endif %} - <tr> - <td>Received from:</td> - <td> - {% for p in parent %} - <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a> - {% endfor %} - </td> - </tr> - <tr><td>Validity:</td><td>{{ addr.from_cert.all.0.not_before }} - {{ addr.from_cert.all.0.not_after }} </td></tr> - - {% if addr.allocated %} - <tr> - <td>Allocated:</td> - <td><a href="{{addr.allocated.get_absolute_url}}">{{ addr.allocated.handle }}</a></td> - </tr> - {% endif %} -</table> - -{% if addr.children.count %} -<h2>Suballocations</h2> -<ul> - {% for subaddr in addr.children.all %} - <li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a></li> - {% endfor %} -</ul> -{% endif %} <!-- suballocations --> - -{% if addr.roa_requests.count %} -<h2>ROA requests</h2> -<table> - <tr><th>ASN</th><th>Max Length</th></tr> - - {% for r in addr.roa_requests.all %} - <tr> - <td>{{ r.roa.asn }}</td> - <td>{{ r.max_length }}</td> - <td><a href="{{ r.get_absolute_url }}/delete">delete</a></td> - </tr> - {% endfor %} -</table> -{% endif %} <!-- roa requests --> - -{% if unallocated %} -<h2>Unallocated</h2> -<ul> - {% for u in unallocated %} - <li>{{ u }}</li> - {% endfor %} -</ul> -{% endif %} - -{% if form %} -<div style='background-color: #dddddd'> -<h2>{{ form_title }}</h2> -<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %} - {{ form.as_p }} - <input type="submit"> -</form> -</div> -{% endif %} <!-- form --> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html deleted file mode 100644 index 7d5187d3..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> > <a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a> > Delete ROA Request</p> - -<h1>Delete ROA Request</h1> - -<p>Please confirm that you would like to delete the following ROA request:</p> - -<table> - <tr><td>AS</td> <td>{{ object.roa.asn }}</td></tr> - <tr><td>Prefix</td> <td><a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a></td></tr> - <tr><td>Max Length</td><td>{{ object.max_length }}</td></tr> -</table> - -<p></p><!--add some space--> - -<form method='POST' action='{{ request.get_full_path }}'> -{% csrf_token %} -<input type='submit' value='Delete'/> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templatetags/__init__.py b/rpkid/rpki/gui/app/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/app/templatetags/__init__.py diff --git a/rpkid/rpki/gui/app/templatetags/app_extras.py b/rpkid/rpki/gui/app/templatetags/app_extras.py new file mode 100644 index 00000000..acb17e14 --- /dev/null +++ b/rpkid/rpki/gui/app/templatetags/app_extras.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def verbose_name(obj): + "Return the model class' verbose name." + return obj._meta.verbose_name + +@register.simple_tag +def verbose_name_plural(qs): + "Return the verbose name for the model class." + return qs.model._meta.verbose_name_plural diff --git a/rpkid/rpki/gui/app/timestamp.py b/rpkid/rpki/gui/app/timestamp.py new file mode 100644 index 00000000..959f2025 --- /dev/null +++ b/rpkid/rpki/gui/app/timestamp.py @@ -0,0 +1,25 @@ +# $Id$ +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# + +import models +from datetime import datetime + +def update(name): + "Set the timestamp value for the given name to the current time." + q = models.Timestamp.objects.filter(name=name) + obj = q[0] if q else models.Timestamp(name=name) + obj.ts = datetime.utcnow() + obj.save() diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index ae9352b1..7e2e9878 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -1,19 +1,19 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" +__version__ = '$Id$' from django.conf.urls.defaults import * from rpki.gui.app import views @@ -23,44 +23,45 @@ urlpatterns = patterns('', (r'^conf/export$', views.conf_export), (r'^conf/list$', views.conf_list), (r'^conf/select$', views.conf_select), - (r'^parent/(?P<parent_handle>[^/]+)$', views.parent_view), - (r'^parent/(?P<parent_handle>[^/]+)/delete$', views.parent_delete), - (r'^child/(?P<child_handle>[^/]+)$', views.child_view), - (r'^child/(?P<child_handle>[^/]+)/delete$', views.child_delete), - (r'^child/(?P<child_handle>[^/]+)/edit$', views.child_edit), - (r'^child/(?P<child_handle>[^/]+)/export$', views.export_child_response), - (r'^child/(?P<child_handle>[^/]+)/export_repo$', views.export_child_repo_response), - (r'^child/(?P<handle>[^/]+)/destroy$', views.destroy_handle), - (r'^address/(?P<pk>\d+)$', views.address_view), - (r'^address/(?P<pk>\d+)/split$', views.prefix_split_view), - (r'^address/(?P<pk>\d+)/allocate$', views.prefix_allocate_view), - (r'^address/(?P<pk>\d+)/roa$', views.prefix_roa_view), - (r'^address/(?P<pk>\d+)/delete$', views.prefix_delete_view), - (r'^asn/(?P<pk>\d+)$', views.asn_view), - (r'^asn/(?P<pk>\d+)/allocate$', views.asn_allocate_view), - (r'^gbr/$', views.ghostbusters_list), + (r'^parent/$', views.parent_list), + (r'^parent/import$', views.parent_import), + (r'^parent/(?P<pk>\d+)$', views.parent_detail), + (r'^parent/(?P<pk>\d+)/delete$', views.parent_delete), + (r'^parent/(?P<pk>\d+)/export$', views.parent_export), + (r'^child/$', views.child_list), + (r'^child/import$', views.child_import), + (r'^child/(?P<pk>\d+)$', views.child_view), + (r'^child/(?P<pk>\d+)/add_asn/$', views.child_add_asn), + (r'^child/(?P<pk>\d+)/add_address/$', views.child_add_address), + (r'^child/(?P<pk>\d+)/delete$', views.child_delete), + (r'^child/(?P<pk>\d+)/edit$', views.child_edit), + (r'^child/(?P<pk>\d+)/export$', views.child_response), + (r'^gbr/$', views.ghostbuster_list), (r'^gbr/create$', views.ghostbuster_create), (r'^gbr/(?P<pk>\d+)$', views.ghostbuster_view), (r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit), (r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete), (r'^refresh$', views.refresh), - (r'^roa/(?P<pk>\d+)$', views.roa_view), - (r'^roareq/(?P<pk>\d+)$', views.roa_request_view), - (r'^roareq/(?P<pk>\d+)/delete$', views.roa_request_delete_view), - (r'^demo/down/asns/(?P<self_handle>[^/]+)$', views.download_asns), - (r'^demo/down/prefixes/(?P<self_handle>[^/]+)$', views.download_prefixes), - (r'^demo/down/roas/(?P<self_handle>[^/]+)$', views.download_roas), - (r'^demo/login', views.login), - (r'^demo/myrpki-xml/(?P<self_handle>[^/]+)$', views.myrpki_xml), - (r'^demo/parent-request/(?P<self_handle>[^/]+)$', views.parent_request), - (r'^demo/repository-request/(?P<self_handle>[^/]+)$', views.repository_request), - (r'^import_child$', views.import_child), - (r'^import_parent$', views.import_parent), - (r'^import_pubclient$', views.import_pubclient), - (r'^import_repository$', views.import_repository), -# (r'^initialize$', views.initialize), - (r'^child_wizard$', views.child_wizard), - (r'^update_bpki', views.update_bpki), + (r'^client/$', views.client_list), + (r'^client/import$', views.client_import), + (r'^client/(?P<pk>\d+)$', views.client_detail), + (r'^client/(?P<pk>\d+)/delete$', views.client_delete), + (r'^client/(?P<pk>\d+)/export$', views.client_export), + (r'^repo/$', views.repository_list), + (r'^repo/import$', views.repository_import), + (r'^repo/(?P<pk>\d+)$', views.repository_detail), + (r'^repo/(?P<pk>\d+)/delete$', views.repository_delete), + (r'^roa/$', views.roa_list), + (r'^roa/create$', views.roa_create), + (r'^roa/confirm$', views.roa_create_confirm), + (r'^roa/(?P<pk>\d+)$', views.roa_detail), + (r'^roa/(?P<pk>\d+)/delete$', views.roa_delete), + (r'^routes/$', views.route_view), + (r'^routes/(?P<pk>\d+)$', views.route_detail), + (r'^routes/(?P<pk>\d+)/roa/$', views.route_roa_list), + (r'^user/$', views.user_list), + (r'^user/create$', views.user_create), + (r'^user/(?P<pk>\d+)$', views.user_detail), + (r'^user/(?P<pk>\d+)/delete$', views.user_delete), + (r'^user/(?P<pk>\d+)/edit$', views.user_edit), ) - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index 0fb34525..6ba6f1c4 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -1,873 +1,1025 @@ +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. -# $Id$ """ -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. +This module contains the view functions implementing the web portal +interface. + """ -from __future__ import with_statement +__version__ = '$Id$' -import email.message, email.utils, mailbox -import os, os.path -import sys, tempfile +import os +import os.path +from tempfile import NamedTemporaryFile from django.contrib.auth.decorators import login_required -from django.contrib import auth from django.shortcuts import get_object_or_404, render_to_response from django.utils.http import urlquote from django.template import RequestContext from django import http from django.views.generic.list_detail import object_list, object_detail -from django.views.generic.create_update import delete_object, update_object, create_object +from django.views.generic.create_update import delete_object from django.core.urlresolvers import reverse +from django.contrib.auth.models import User -from rpki.gui.app import models, forms, glue, misc, AllocationTree, settings -from rpki.gui.app.asnset import asnset +from rpki.irdb import Zookeeper, ChildASN, ChildNet +from rpki.gui.app import models, forms, glue, range_list +from rpki.resource_set import (resource_range_as, resource_range_ipv4, + resource_range_ipv6, roa_prefix_ipv4) +from rpki.exceptions import BadIPResource +from rpki import sundial -debug = False +from rpki.gui.cacheview.models import ROAPrefixV4, ROAPrefixV6, ROA -def my_login_required(f): - """ - A version of django.contrib.auth.decorators.login_required - that will fail instead of redirecting to the login page when - the user is not logged in. - For use with the rpkidemo service URLs where we want to detect - failure to log in. Otherwise django will return code 200 with - the login form, and fools rpkidemo. +def superuser_required(f): + """Decorator which returns HttpResponseForbidden if the user does + not have superuser permissions. + """ - def wrapped(request, *args, **kwargs): - if not request.user.is_authenticated(): + @login_required + def _wrapped(request, *args, **kwargs): + if not request.user.is_superuser: return http.HttpResponseForbidden() return f(request, *args, **kwargs) + return _wrapped + - return wrapped +# FIXME This method is included in Django 1.3 and can be removed when Django +# 1.2 is out of its support window. +def render(request, template, context): + """ + https://docs.djangoproject.com/en/1.3/topics/http/shortcuts/#render + + """ + return render_to_response(template, context, + context_instance=RequestContext(request)) -# For each type of object, we have a detail view, a create view and -# an update view. We heavily leverage the generic views, only -# adding our own idea of authorization. def handle_required(f): + """Decorator for view functions which require the user to be logged in and + a resource handle selected for the session. + + """ @login_required def wrapped_fn(request, *args, **kwargs): if 'handle' not in request.session: if request.user.is_superuser: conf = models.Conf.objects.all() else: - conf = models.Conf.objects.filter(owner=request.user) + conf = models.Conf.objects.filter(handle=request.user.username) + if conf.count() == 1: - handle = conf[0] + request.session['handle'] = conf[0] elif conf.count() == 0: - return render('rpkigui/conf_empty.html', {}, request) - #return http.HttpResponseRedirect('/myrpki/conf/add') + return render(request, 'app/conf_empty.html', {}) else: # Should reverse the view for this instead of hardcoding # the URL. - return http.HttpResponseRedirect( - reverse(conf_list) + '?next=' + urlquote(request.get_full_path())) - request.session[ 'handle' ] = handle + url = '%s?next=%s' % (reverse(conf_list), + urlquote(request.get_full_path())) + return http.HttpResponseRedirect(url) + return f(request, *args, **kwargs) return wrapped_fn -def render(template, context, request): - return render_to_response(template, context, - context_instance=RequestContext(request)) @handle_required -def dashboard(request, template_name='rpkigui/dashboard.html'): - '''The user's dashboard.''' - handle = request.session[ 'handle' ] - # ... pick out data for the dashboard and return it - # my parents - # the resources that my parents have given me - # the resources that I have accepted from my parents - # my children - # the resources that I have given my children - # my roas - - # get list of ASNs used in my ROAs - roa_asns = [r.asn for r in handle.roas.all()] - asns=[] - for a in models.Asn.objects.filter(from_cert__parent__in=handle.parents.all()): - f = AllocationTree.AllocationTreeAS(a) - if f.unallocated(): - asns.append(f) - - prefixes = [] - for p in models.AddressRange.objects.filter(from_cert__parent__in=handle.parents.all()): - f = AllocationTree.AllocationTreeIP.from_prefix(p) - if f.unallocated(): - prefixes.append(f) - - asns.sort(key=lambda x: x.range.min) - prefixes.sort(key=lambda x: x.range.min) - - return render(template_name, { 'conf': handle, 'asns': asns, 'ars': prefixes }, request) - -@login_required +def generic_import(request, queryset, configure, form_class=None, + template_name=None, post_import_redirect=None): + """ + Generic view function for importing XML files used in the setup + process. + + queryset + queryset containing all objects of the type being imported + + configure + method on Zookeeper to invoke with the imported XML file + + form_class + specifies the form to use for import. If None, uses the generic + forms.ImportForm. + + template_name + path to the html template to use to render the form. If None, defaults + to "app/<model>_import_form.html", where <model> is introspected from + the "queryset" argument. + + post_import_redirect + if None (default), the user will be redirected to the detail page for + the imported object. Otherwise, the user will be redirected to the + specified URL. + + """ + conf = request.session['handle'] + if template_name is None: + template_name = 'app/%s_import_form.html' % queryset.model.__name__.lower() + if form_class is None: + form_class = forms.ImportForm + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + tmpf = NamedTemporaryFile(prefix='import', suffix='.xml', + delete=False) + tmpf.write(form.cleaned_data['xml'].read()) + tmpf.close() + z = Zookeeper(handle=conf.handle) + handle = form.cleaned_data.get('handle') + # CharField uses an empty string for the empty value, rather than + # None. Convert to none in this case, since configure_child/parent + # expects it. + if handle == '': + handle = None + # configure_repository returns None, so can't use tuple expansion + # here. Unpack the tuple below if post_import_redirect is None. + r = configure(z, tmpf.name, handle) + # force rpkid run now + z.synchronize(conf.handle) + os.remove(tmpf.name) + if post_import_redirect: + url = post_import_redirect + else: + _, handle = r + url = queryset.get(issuer=conf, + handle=handle).get_absolute_url() + return http.HttpResponseRedirect(url) + else: + form = form_class() + + return render(request, template_name, {'form': form}) + + +@handle_required +def dashboard(request): + log = request.META['wsgi.errors'] + conf = request.session['handle'] + + used_asns = range_list.RangeList() + + # asns used in my roas + qs = models.ROARequest.objects.filter(issuer=conf) + roa_asns = set((obj.asn for obj in qs)) + used_asns.extend((resource_range_as(asn, asn) for asn in roa_asns)) + + # asns given to my children + child_asns = ChildASN.objects.filter(child__in=conf.children.all()) + used_asns.extend((resource_range_as(obj.start_as, obj.end_as) for obj in child_asns)) + + # my received asns + asns = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf) + my_asns = range_list.RangeList([resource_range_as(obj.min, obj.max) for obj in asns]) + + unused_asns = my_asns.difference(used_asns) + + used_prefixes = range_list.RangeList() + used_prefixes_v6 = range_list.RangeList() + + # prefixes used in my roas + for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf, + version='IPv4'): + used_prefixes.append(obj.as_resource_range()) + + for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf, + version='IPv6'): + used_prefixes_v6.append(obj.as_resource_range()) + + # prefixes given to my children + for obj in ChildNet.objects.filter(child__in=conf.children.all(), + version='IPv4'): + used_prefixes.append(obj.as_resource_range()) + + for obj in ChildNet.objects.filter(child__in=conf.children.all(), + version='IPv6'): + used_prefixes_v6.append(obj.as_resource_range()) + + # my received prefixes + prefixes = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf).all() + prefixes_v6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf).all() + my_prefixes = range_list.RangeList([obj.as_resource_range() for obj in prefixes]) + my_prefixes_v6 = range_list.RangeList([obj.as_resource_range() for obj in prefixes_v6]) + + unused_prefixes = my_prefixes.difference(used_prefixes) + unused_prefixes_v6 = my_prefixes_v6.difference(used_prefixes_v6) + + return render(request, 'app/dashboard.html', { + 'conf': conf, + 'unused_asns': unused_asns, + 'unused_prefixes': unused_prefixes, + 'unused_prefixes_v6': unused_prefixes_v6, + 'asns': asns, + 'prefixes': prefixes, + 'prefixes_v6': prefixes_v6}) + + +@superuser_required def conf_list(request): """Allow the user to select a handle.""" - if request.user.is_superuser: - queryset = models.Conf.objects.all() - else: - queryset = models.Conf.objects.filter(owner=request.user) + queryset = models.Conf.objects.all() return object_list(request, queryset, - template_name='rpkigui/conf_list.html', template_object_name='conf', extra_context={ 'select_url' : reverse(conf_select) }) + template_name='app/conf_list.html', + template_object_name='conf', + extra_context={'select_url': reverse(conf_select)}) + -@login_required +@superuser_required def conf_select(request): - '''Change the handle for the current session.''' + """Change the handle for the current session.""" if not 'handle' in request.GET: return http.HttpResponseRedirect('/myrpki/conf/select') handle = request.GET['handle'] next_url = request.GET.get('next', reverse(dashboard)) if next_url == '': next_url = reverse(dashboard) + request.session['handle'] = get_object_or_404(models.Conf, handle=handle) + return http.HttpResponseRedirect(next_url) - if request.user.is_superuser: - conf = models.Conf.objects.filter(handle=handle) - else: - # since the handle is passed in as a parameter, need to verify that - # the user is actually in the group - conf = models.Conf.objects.filter(handle=handle, - owner=request.user) - if conf: - request.session['handle'] = conf[0] - return http.HttpResponseRedirect(next_url) - - return http.HttpResponseRedirect(reverse(conf_list) + '?next=' + next_url) def serve_xml(content, basename): - resp = http.HttpResponse(content , mimetype='application/xml') - resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename, ) + """ + Generate a HttpResponse object with the content type set to XML. + + `content` is a string. + + `basename` is the prefix to specify for the XML filename. + + """ + resp = http.HttpResponse(content, mimetype='application/xml') + resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename,) return resp + @handle_required def conf_export(request): """Return the identity.xml for the current handle.""" - handle = request.session['handle'] - return serve_xml(glue.read_identity(handle.handle), 'identity') + conf = request.session['handle'] + z = Zookeeper(handle=conf.handle) + xml = z.generate_identity() + return serve_xml(str(xml), '%s.identity' % conf.handle) + @handle_required -def parent_view(request, parent_handle): - """Detail view for a particular parent.""" - handle = request.session['handle'] - parent = get_object_or_404(handle.parents, handle__exact=parent_handle) - return render('rpkigui/parent_view.html', { 'parent': parent }, request) - -def get_parents_or_404(handle, obj): - '''Return the Parent object(s) that the given address range derives - from, or raise a 404 error.''' - cert_set = misc.top_parent(obj).from_cert.filter(parent__in=handle.parents.all()) - if cert_set.count() == 0: - raise http.Http404, 'Object is not delegated from any parent' - return [c.parent for c in cert_set] - -@handle_required -def asn_view(request, pk): - '''view/subdivide an asn range.''' - handle = request.session['handle'] - obj = get_object_or_404(models.Asn.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, obj) - roas = handle.roas.filter(asn=obj.lo) # roas which contain this asn - unallocated = AllocationTree.AllocationTreeAS(obj).unallocated() - - return render('rpkigui/asn_view.html', - { 'asn': obj, 'parent': parent_set, 'roas': roas, - 'unallocated' : unallocated }, request) - -@handle_required -def child_view(request, child_handle): - '''Detail view of child for the currently selected handle.''' - handle = request.session['handle'] - child = get_object_or_404(handle.children, handle__exact=child_handle) - - return render('rpkigui/child_view.html', { 'child': child }, request) - -@handle_required -def child_edit(request, child_handle): - """Edit the end validity date for a resource handle's child.""" - handle = request.session['handle'] - child = get_object_or_404(handle.children, handle__exact=child_handle) +def parent_import(request): + conf = request.session['handle'] + return generic_import(request, conf.parents, Zookeeper.configure_parent) - if request.method == 'POST': - form = forms.ChildForm(request.POST, request.FILES, instance=child) - if form.is_valid(): - form.save() - glue.configure_resources(request.META['wsgi.errors'], handle) - return http.HttpResponseRedirect(child.get_absolute_url()) - else: - form = forms.ChildForm(instance=child) - - return render('rpkigui/child_form.html', { 'child': child, 'form': form }, request) - -class PrefixView(object): - '''Extensible view for address ranges/prefixes. This view can be - subclassed to add form handling for editing the prefix.''' - - form = None - form_title = None - - def __init__(self, request, pk, form_class=None): - self.handle = request.session['handle'] - self.obj = get_object_or_404(models.AddressRange.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - self.parent_set = get_parents_or_404(self.handle, self.obj) - self.form_class = form_class - self.request = request - - def __call__(self, *args, **kwargs): - if self.request.method == 'POST': - resp = self.handle_post() - else: - resp = self.handle_get() - - # allow get/post handlers to return a custom response - if resp: - return resp - - u = AllocationTree.AllocationTreeIP.from_prefix(self.obj).unallocated() - - return render('rpkigui/prefix_view.html', - { 'addr': self.obj, 'parent': self.parent_set, 'unallocated': u, - 'form': self.form, - 'form_title': self.form_title if self.form_title else 'Edit' }, - self.request) - - def handle_get(self): - '''Virtual method for extending GET handling. Default action is - to call the form class constructor with the prefix object.''' - if self.form_class: - self.form = self.form_class(self.obj) - - def form_valid(self): - '''Virtual method for handling a valid form. Called by the default - implementation of handle_post().''' - pass - - def handle_post(self): - '''Virtual method for extending POST handling. Default implementation - creates a form object using the form_class in the constructor and passing - the prefix object. If the form's is_valid() method is True, it then - invokes this class's form_valid() method.''' - resp = None - if self.form_class: - self.form = self.form_class(self.obj, self.request.POST) - if self.form.is_valid(): - resp = self.form_valid() - return resp - -@handle_required -def address_view(request, pk): - return PrefixView(request, pk)() - -class PrefixSplitView(PrefixView): - '''Class for handling the prefix split form.''' - - form_title = 'Split' - - def form_valid(self): - r = misc.parse_resource_range(self.form.cleaned_data['prefix']) - obj = models.AddressRange(lo=str(r.min), hi=str(r.max), parent=self.obj) - obj.save() - return http.HttpResponseRedirect(obj.get_absolute_url()) - -@handle_required -def prefix_split_view(request, pk): - return PrefixSplitView(request, pk, form_class=forms.PrefixSplitForm)() - -class PrefixAllocateView(PrefixView): - '''Class to handle the allocation to child form.''' - - form_title = 'Give to Child' - - def handle_get(self): - self.form = forms.PrefixAllocateForm( - self.obj.allocated.pk if self.obj.allocated else None, - self.handle.children.all()) - - def handle_post(self): - self.form = forms.PrefixAllocateForm(None, self.handle.children.all(), self.request.POST) - if self.form.is_valid(): - self.obj.allocated = self.form.cleaned_data['child'] - self.obj.save() - glue.configure_resources(self.request.META['wsgi.errors'], self.handle) - return http.HttpResponseRedirect(self.obj.get_absolute_url()) - -@handle_required -def prefix_allocate_view(request, pk): - return PrefixAllocateView(request, pk)() - -def add_roa_requests(handle, prefix, asns, max_length): - for asid in asns: - if debug: - print 'searching for a roa for AS %d containing %s-%d' % (asid, prefix, max_length) - req_set = prefix.roa_requests.filter(roa__asn=asid, max_length=max_length) - if not req_set: - if debug: - print 'no roa for AS %d containing %s-%d' % (asid, prefix, max_length) - - # find ROAs for prefixes derived from the same resource cert - # as this prefix - certs = misc.top_parent(prefix).from_cert.all() - roa_set = handle.roas.filter(asn=asid, cert__in=certs) - - # FIXME: currently only creates a ROA/request for the first - # resource cert, not all of them - if roa_set: - roa = roa_set[0] - else: - if debug: - print 'creating new roa for AS %d containg %s-%d' % (asid, prefix, max_length) - # no roa is present for this ASN, create a new one - roa = models.Roa.objects.create(asn=asid, conf=handle, - active=False, cert=certs[0]) - roa.save() - req = models.RoaRequest.objects.create(prefix=prefix, roa=roa, - max_length=max_length) - req.save() +@handle_required +def parent_list(request): + """List view for parent objects.""" + conf = request.session['handle'] + return object_list(request, queryset=conf.parents.all(), + extra_context={'create_url': reverse(parent_import), + 'create_label': 'Import'}) -class PrefixRoaView(PrefixView): - '''Class for handling the ROA creation form.''' - form_title = 'Issue ROA' +@handle_required +def parent_detail(request, pk): + """Detail view for a particular parent.""" + conf = request.session['handle'] + return object_detail(request, conf.parents.all(), object_id=pk) + - def form_valid(self): - asns = asnset(self.form.cleaned_data['asns']) - add_roa_requests(self.handle, self.obj, asns, self.form.cleaned_data['max_length']) - glue.configure_resources(self.request.META['wsgi.errors'], self.handle) - return http.HttpResponseRedirect(self.obj.get_absolute_url()) - @handle_required -def prefix_roa_view(request, pk): - return PrefixRoaView(request, pk, form_class=forms.PrefixRoaForm)() +def parent_delete(request, pk): + conf = request.session['handle'] + obj = get_object_or_404(conf.parents, pk=pk) # confirm permission + log = request.META['wsgi.errors'] + form_class = forms.UserDeleteForm + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_parent(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(parent_list)) + else: + form = form_class() + return render(request, 'app/parent_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) -class PrefixDeleteView(PrefixView): - form_title = 'Delete' - def form_valid(self): - self.obj.delete() - return http.HttpResponseRedirect(reverse(dashboard)) - @handle_required -def prefix_delete_view(request, pk): - return PrefixDeleteView(request, pk, form_class=forms.PrefixDeleteForm)() +def parent_export(request, pk): + """Export XML repository request for a given parent.""" + conf = request.session['handle'] + parent = get_object_or_404(conf.parents, pk=pk) + z = Zookeeper(handle=conf.handle) + xml = z.generate_repository_request(parent) + return serve_xml(str(xml), '%s.repository' % parent.handle) + @handle_required -def roa_request_delete_view(request, pk): - """ - Remove a ROA request from a particular prefix. - """ +def child_import(request): + conf = request.session['handle'] + return generic_import(request, conf.children, Zookeeper.configure_child) - log = request.META['wsgi.errors'] - handle = request.session['handle'] - obj = get_object_or_404(models.RoaRequest.objects, pk=pk) - prefix = obj.prefix - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, prefix) - if request.method == 'POST': - roa = obj.roa - obj.delete() - if not roa.from_roa_request.all(): - roa.delete() - glue.configure_resources(log, handle) - return http.HttpResponseRedirect(prefix.get_absolute_url()) +@handle_required +def child_list(request): + """List of children for current user.""" + conf = request.session['handle'] + return object_list(request, queryset=conf.children.all(), + template_name='app/child_list.html', + extra_context={ + 'create_url': reverse(child_import), + 'create_label': 'Import'}) - return render('rpkigui/roa_request_confirm_delete.html', { 'object': obj }, request) @handle_required -def asn_allocate_view(request, pk): +def child_add_resource(request, pk, form_class, unused_list, callback, + template_name='app/child_add_resource_form.html'): + conf = request.session['handle'] + child = models.Child.objects.get(issuer=conf, pk=pk) log = request.META['wsgi.errors'] - handle = request.session['handle'] - obj = get_object_or_404(models.Asn.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, obj) - if request.method == 'POST': - form = forms.PrefixAllocateForm(None, handle.children.all(), request.POST) + form = form_class(request.POST, request.FILES) if form.is_valid(): - obj.allocated = form.cleaned_data['child'] - obj.save() - glue.configure_resources(log, handle) - return http.HttpResponseRedirect(obj.get_absolute_url()) + callback(child, form) + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(child.get_absolute_url()) else: - form = forms.PrefixAllocateForm(obj.allocated.pk if obj.allocated else None, - handle.children.all()) + form = form_class() - return render('rpkigui/asn_view.html', { 'form': form, - 'asn': obj, 'form': form, 'parent': parent_set }, request) + return render(request, template_name, + {'object': child, 'form': form, 'unused': unused_list}) -# this is similar to handle_required, except that the handle is given in URL -def handle_or_404(request, handle): - "ensure the requested handle is available to this user" - if request.user.is_superuser: - conf_set = models.Conf.objects.filter(handle=handle) - else: - conf_set = models.Conf.objects.filter(owner=request.user, handle=handle) - if not conf_set: - raise http.Http404, 'resource handle not found' - return conf_set[0] - -def serve_file(handle, fname, content_type, error_code=404): - content, mtime = glue.read_file_from_handle(handle, fname) - resp = http.HttpResponse(content , mimetype=content_type) - resp['Content-Disposition'] = 'attachment; filename=%s' % (os.path.basename(fname), ) - resp['Last-Modified'] = email.utils.formatdate(mtime, usegmt=True) - return resp -@my_login_required -def download_csv(request, self_handle, fname): - conf = handle_or_404(request, self_handle) - return serve_file(conf.handle, fname + '.csv', 'text/csv') +def add_asn_callback(child, form): + asns = form.cleaned_data.get('asns') + r = resource_range_as.parse_str(asns) + child.asns.create(start_as=r.min, end_as=r.max) -def download_asns(request, self_handle): - return download_csv(request, self_handle, 'asns') -def download_roas(request, self_handle): - return download_csv(request, self_handle, 'roas') +def child_add_asn(request, pk): + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qs = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, forms.AddASNForm(qs), [], + add_asn_callback) -def download_prefixes(request, self_handle): - return download_csv(request, self_handle, 'prefixes') -def save_to_inbox(conf, request_type, content): - """ - Save an incoming request from a client to the incoming mailbox - for processing by a human. - """ +def add_address_callback(child, form): + address_range = form.cleaned_data.get('address_range') + try: + r = resource_range_ipv4.parse_str(address_range) + version = 'IPv4' + except BadIPResource: + r = resource_range_ipv6.parse_str(address_range) + version = 'IPv6' + child.address_ranges.create(start_ip=str(r.min), end_ip=str(r.max), + version=version) - user = conf.owner.all()[0] - filename = request_type + '.xml' - msg = email.message.Message() - msg['Date'] = email.utils.formatdate() - msg['From'] = '"%s" <%s>' % (conf.handle, user.email) - msg['Message-ID'] = email.utils.make_msgid() - msg['Subject'] = '%s for %s' % (filename, conf.handle) - msg['X-rpki-self-handle'] = conf.handle - msg['X-rpki-type'] = request_type - msg.add_header('Content-Disposition', 'attachment', filename=filename) - msg.set_type('application/x-rpki-setup') - msg.set_payload(content) +def child_add_address(request, pk): + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qsv4 = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf) + qsv6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, + forms.AddNetForm(qsv4, qsv6), + [], + callback=add_address_callback) - box = mailbox.Maildir(settings.INBOX) - box.add(msg) - box.close() - return http.HttpResponse() +@handle_required +def child_view(request, pk): + """Detail view of child for the currently selected handle.""" + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + return render(request, 'app/child_detail.html', + {'object': child, 'can_edit': True}) -def get_response(conf, request_type): - """ - If there is cached response for the given request type, simply - return it. Otherwise, look in the outbox mailbox for a response. - """ - filename = glue.confpath(conf.handle) + '/' + request_type + '.xml' - if not os.path.exists(filename): - box = mailbox.Maildir(settings.OUTBOX, factory=None) - for key, msg in box.iteritems(): - # look for parent responses for this child - if msg.get('x-rpki-type') == request_type and msg.get('x-rpki-self-handle') == conf.handle: - with open(filename, 'w') as f: - f.write(msg.get_payload()) - break - else: - return http.HttpResponse('no response found', status=503) - - box.remove(key) # remove the msg from the outbox - - return serve_file(conf.handle, request_type + '.xml', 'application/xml') - -@my_login_required -def parent_request(request, self_handle): - conf = handle_or_404(request, self_handle) +@handle_required +def child_edit(request, pk): + """Edit the end validity date for a resource handle's child.""" + log = request.META['wsgi.errors'] + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + form_class = forms.ChildForm(child) if request.method == 'POST': - return save_to_inbox(conf, 'identity', request.POST['content']) + form = form_class(request.POST, request.FILES) + if form.is_valid(): + child.valid_until = sundial.datetime.fromdatetime(form.cleaned_data.get('valid_until')) + child.save() + # remove AS & prefixes that are not selected in the form + models.ChildASN.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('as_ranges')).delete() + models.ChildNet.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('address_ranges')).delete() + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(child.get_absolute_url()) else: - return get_response(conf, 'parent') + form = form_class(initial={ + 'as_ranges': child.asns.all(), + 'address_ranges': child.address_ranges.all()}) -@my_login_required -def repository_request(request, self_handle): - conf = handle_or_404(request, self_handle) + return render(request, 'app/child_form.html', + {'object': child, 'form': form}) - if request.method == 'POST': - return save_to_inbox(conf, 'repository', request.POST['content']) - else: - return get_response(conf, 'repository') + +@handle_required +def roa_create(request): + """Present the user with a form to create a ROA. + + Doesn't use the generic create_object() form because we need to + create both the ROARequest and ROARequestPrefix objects. -@my_login_required -def myrpki_xml(request, self_handle): - """ - Handles POST of the myrpki.xml file for a given resource handle. - As a special case for resource handles hosted by APNIC, stash a - copy of the first xml message in the rpki inbox mailbox as this - will be required to complete the parent-child setup. """ - conf = handle_or_404(request, self_handle) - log = request.META['wsgi.errors'] + conf = request.session['handle'] if request.method == 'POST': - fname = glue.confpath(self_handle, '/myrpki.xml') + form = forms.ROARequest(request.POST, request.FILES, conf=conf) + if form.is_valid(): + asn = form.cleaned_data.get('asn') + rng = form._as_resource_range() # FIXME calling "private" method + max_prefixlen = int(form.cleaned_data.get('max_prefixlen')) + + # find list of matching routes + routes = [] + match = roa_match(rng) + for route, roas in match: + validate_route(route, roas) + # tweak the validation status due to the presence of the + # new ROA. Don't need to check the prefix bounds here + # because all the matches routes will be covered by this + # new ROA + if route.status == 'unknown': + # if the route was previously unknown (no covering + # ROAs), then: + # if the AS matches, it is valid, otherwise invalid + if (route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen): + route.status = 'valid' + route.status_label = 'success' + else: + route.status = 'invalid' + route.status_label = 'important' + elif route.status == 'invalid': + # if the route was previously invalid, but this new ROA + # matches the ASN, it is now valid + if route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen: + route.status = 'valid' + route.status_label = 'success' + + routes.append(route) + + prefix = str(rng) + form = forms.ROARequestConfirm(initial={'asn': asn, + 'prefix': prefix, + 'max_prefixlen': max_prefixlen}) + return render(request, 'app/roarequest_confirm_form.html', + {'form': form, + 'asn': asn, + 'prefix': prefix, + 'max_prefixlen': max_prefixlen, + 'routes': routes}) + else: + form = forms.ROARequest() - if not os.path.exists(fname): - print >>log, 'Saving a copy of myrpki.xml for handle %s to inbox' % conf.handle - save_to_inbox(conf, 'myrpki', request.POST['content']) + return render(request, 'app/roarequest_form.html', {'form': form}) - print >>log, 'writing %s' % fname - with open(fname, 'w') as myrpki_xml : - myrpki_xml.write(request.POST['content']) - # FIXME: used to run configure_daemons here, but it takes too - # long with many hosted handles. rpkidemo still needs a way - # to do initial bpki setup with rpkid! +@handle_required +def roa_create_confirm(request): + conf = request.session['handle'] + log = request.META['wsgi.errors'] - return http.HttpResponse('<p>success</p>') + if request.method == 'POST': + form = forms.ROARequestConfirm(request.POST, request.FILES) + if form.is_valid(): + asn = form.cleaned_data.get('asn') + prefix = form.cleaned_data.get('prefix') + rng = glue.str_to_resource_range(prefix) + max_prefixlen = form.cleaned_data.get('max_prefixlen') + + roarequests = models.ROARequest.objects.filter(issuer=conf, + asn=asn) + if roarequests: + # FIXME need to handle the case where there are + # multiple ROAs for the same AS due to prefixes + # delegated from different resource certs. + roa = roarequests[0] + else: + roa = models.ROARequest.objects.create(issuer=conf, + asn=asn) + v = 'IPv4' if isinstance(rng, resource_range_ipv4) else 'IPv6' + roa.prefixes.create(version=v, prefix=str(rng.min), + prefixlen=rng.prefixlen(), + max_prefixlen=max_prefixlen) + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(reverse(roa_list)) else: - return serve_file(self_handle, 'myrpki.xml', 'application/xml') + return http.HttpResponseRedirect(reverse(roa_create)) + -def login(request): +@handle_required +def roa_list(request): """ - A version of django.contrib.auth.views.login that will return an - error response when the user/password is bad. This is needed for - use with rpkidemo to properly detect errors. The django login - view will return 200 with the login page when the login fails, - which is not desirable when using rpkidemo. + Display a list of ROARequestPrefix objects for the current resource + handle. + """ - log = request.META['wsgi.errors'] - if request.method == 'POST': - username = request.POST['username'] - password = request.POST['password'] - print >>log, 'login request for user %s' % username - user = auth.authenticate(username=username, password=password) - if user is not None and user.is_active: - auth.login(request, user) - return http.HttpResponse('<p>login succeeded</p>') - print >>log, 'failed login attempt for user %s\n' % username - return http.HttpResponseForbidden('<p>bad username or password</p>') - else: - return http.HttpResponse('<p>This should never been seen by a human</p>') + conf = request.session['handle'] + qs = models.ROARequestPrefix.objects.filter(roa_request__issuer=conf).order_by('prefix') + return object_list(request, queryset=qs, + template_name='app/roa_request_list.html', + extra_context={'create_url': reverse(roa_create)}) + @handle_required -def roa_request_view(request, pk): - """not yet implemented""" - return +def roa_detail(request, pk): + """Not implemented. + + This is a placeholder so that + models.ROARequestPrefix.get_absolute_url works. The only reason it + exist is so that the /delete URL works. + + """ + pass + @handle_required -def roa_view(request, pk): - """not yet implemented""" - return +def roa_delete(request, pk): + """Handles deletion of a single ROARequestPrefix object. + + Uses a form for double confirmation, displaying how the route + validation status may change as a result. + + """ + + conf = request.session['handle'] + obj = get_object_or_404(models.ROARequestPrefix.objects, + roa_request__issuer=conf, pk=pk) + + if request.method == 'POST': + roa = obj.roa_request + obj.delete() + # if this was the last prefix on the ROA, delete the ROA request + if not roa.prefixes.exists(): + roa.delete() + Zookeeper(handle=conf.handle).run_rpkid_now() + return http.HttpResponseRedirect(reverse(roa_list)) + + ### Process GET ### + + match = roa_match(obj.as_resource_range()) + + roa_pfx = obj.as_roa_prefix() + + pfx = 'prefixes' if isinstance(roa_pfx, roa_prefix_ipv4) else 'prefixes_v6' + args = {'%s__prefix_min' % pfx: roa_pfx.min(), + '%s__prefix_max' % pfx: roa_pfx.max(), + '%s__max_length' % pfx: roa_pfx.max_prefixlen} + # exclude ROAs which seem to match this request and display the result + routes = [] + for route, roas in match: + qs = roas.exclude(asid=obj.roa_request.asn, **args) + validate_route(route, qs) + routes.append(route) + + return render(request, 'app/roa_request_confirm_delete.html', + {'object': obj, 'routes': routes}) + + @handle_required -def ghostbusters_list(request): +def ghostbuster_list(request): """ Display a list of all ghostbuster requests for the current Conf. + """ conf = request.session['handle'] + qs = models.GhostbusterRequest.objects.filter(issuer=conf) + return object_list(request, queryset=qs) - return object_list(request, queryset=conf.ghostbusters.all(), template_name='rpkigui/ghostbuster_list.html') @handle_required def ghostbuster_view(request, pk): """ Display an individual ghostbuster request. + """ conf = request.session['handle'] + qs = models.GhostbusterRequest.objects.filter(issuer=conf) + return object_detail(request, queryset=qs, object_id=pk, + extra_context={'can_edit': True}) - return object_detail(request, queryset=conf.ghostbusters.all(), object_id=pk, template_name='rpkigui/ghostbuster_detail.html') @handle_required def ghostbuster_delete(request, pk): - conf = request.session['handle'] - - # verify that the object is owned by this conf - obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + """ + Handle deletion of a GhostbusterRequest object. - # modeled loosely on the generic delete_object() view. + """ + conf = request.session['handle'] + log = request.META['wsgi.errors'] + form_class = forms.UserDeleteForm # FIXME + # Ensure the GhosbusterRequest object belongs to the current user. + obj = get_object_or_404(models.GhostbusterRequest, issuer=conf, pk=pk) if request.method == 'POST': - obj.delete() - glue.configure_resources(request.META['wsgi.errors'], conf) - return http.HttpResponseRedirect(reverse(ghostbusters_list)) + form = form_class(request.POST, request.FILES) + if form.is_valid(): + obj.delete() + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(reverse(ghostbuster_list)) else: - return render('rpkigui/ghostbuster_confirm_delete.html', { 'object': obj }, request) + form = form_class() + return render(request, 'app/ghostbusterrequest_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) + def _ghostbuster_edit(request, obj=None): """ Common code for create/edit. + """ conf = request.session['handle'] - form_class = forms.GhostbusterForm(conf.parents.all()) + form_class = forms.GhostbusterRequestForm if request.method == 'POST': - form = form_class(request.POST, request.FILES, instance=obj) + form = form_class(conf, request.POST, request.FILES, instance=obj) if form.is_valid(): # use commit=False for the creation case, otherwise form.save() # will fail due to schema constraint violation because conf is # NULL obj = form.save(commit=False) - obj.conf = conf + obj.issuer = conf + obj.vcard = glue.ghostbuster_to_vcard(obj) obj.save() - glue.configure_resources(request.META['wsgi.errors'], conf) + Zookeeper(handle=conf.handle).run_rpkid_now() return http.HttpResponseRedirect(obj.get_absolute_url()) else: - form = form_class(instance=obj) - return render('rpkigui/ghostbuster_form.html', { 'form': form }, request) + form = form_class(conf, instance=obj) + return render(request, 'app/ghostbuster_form.html', + {'form': form, 'object': obj}) + @handle_required def ghostbuster_edit(request, pk): conf = request.session['handle'] # verify that the object is owned by this conf - obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + obj = get_object_or_404(models.GhostbusterRequest, pk=pk, issuer=conf) return _ghostbuster_edit(request, obj) + @handle_required def ghostbuster_create(request): return _ghostbuster_edit(request) + @handle_required def refresh(request): - "Query rpkid, update the db, and redirect back to the dashboard." - glue.list_received_resources(request.META['wsgi.errors'], request.session['handle']) - return http.HttpResponseRedirect(reverse(dashboard)) + """ + Query rpkid, update the db, and redirect back to the dashboard. -@handle_required -def import_parent(request): - conf = request.session['handle'] - log = request.META['wsgi.errors'] + """ + glue.list_received_resources(request.META['wsgi.errors'], + request.session['handle']) + return http.HttpResponseRedirect(reverse(dashboard)) - if request.method == 'POST': - form = forms.ImportParentForm(conf, request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='parent', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_parent(log, conf, form.cleaned_data['handle'], f) - os.remove(tmpf.name) +@handle_required +def child_response(request, pk): + """ + Export the XML file containing the output of the configure_child + to send back to the client. - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ImportParentForm(conf) + """ + conf = request.session['handle'] + child = get_object_or_404(models.Child, issuer=conf, pk=pk) + z = Zookeeper(handle=conf.handle) + xml = z.generate_parental_response(child) + resp = serve_xml(str(xml), child.handle) + return resp - return render('rpkigui/import_parent_form.html', { 'form': form }, request) @handle_required -def import_repository(request): +def child_delete(request, pk): conf = request.session['handle'] - log = request.META['wsgi.errors'] - + # verify this child belongs to the current user + obj = get_object_or_404(conf.children, pk=pk) + form_class = forms.UserDeleteForm # FIXME if request.method == 'POST': - form = forms.ImportRepositoryForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='repository', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_repository(log, conf, f) - - os.remove(tmpf.name) - - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(handle=conf.handle) + z.delete_child(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(child_list)) else: - form = forms.ImportRepositoryForm() + form = form_class() + return render(request, 'app/child_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) + + +def roa_match(rng): + """Return a list of tuples of matching routes and roas.""" + if isinstance(rng, resource_range_ipv6): + route_manager = models.RouteOriginV6.objects + pfx = 'prefixes_v6' + else: + route_manager = models.RouteOrigin.objects + pfx = 'prefixes' - return render('rpkigui/import_repository_form.html', { 'form': form }, request) + rv = [] + for obj in route_manager.filter(prefix_min__gte=rng.min, prefix_max__lte=rng.max): + # This is a bit of a gross hack, since the foreign keys for v4 and v6 + # prefixes have different names. + args = {'%s__prefix_min__lte' % pfx: obj.prefix_min, + '%s__prefix_max__gte' % pfx: obj.prefix_max} + roas = ROA.objects.filter(**args) + rv.append((obj, roas)) -@handle_required -def import_pubclient(request): - conf = request.session['handle'] - log = request.META['wsgi.errors'] + return rv - if request.method == 'POST': - form = forms.ImportPubClientForm(request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='pubclient', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_pubclient(log, conf, f) - os.remove(tmpf.name) +def validate_route(route, roas): + """Annotate the route object with its validation status. - return http.HttpResponseRedirect(reverse(dashboard)) + `roas` is a queryset containing ROAs which cover `route`. + + """ + pfx = 'prefixes' if isinstance(route, models.RouteOrigin) else 'prefixes_v6' + args = {'asid': route.asn, + '%s__prefix_min__lte' % pfx: route.prefix_min, + '%s__prefix_max__gte' % pfx: route.prefix_max, + '%s__max_length__gte' % pfx: route.prefixlen()} + + # 2. if the candidate ROA set is empty, end with unknown + if not roas.exists(): + route.status = 'unknown' + route.status_label = 'warning' + # 3. if any candidate roa matches the origin AS and max_length, end with + # valid + # + # AS0 is always invalid. + elif route.asn != 0 and roas.filter(**args).exists(): + route.status_label = 'success' + route.status = 'valid' + # 4. otherwise the route is invalid else: - form = forms.ImportPubClientForm() + route.status_label = 'important' + route.status = 'invalid' + + return route - return render('rpkigui/import_pubclient_form.html', { 'form': form }, request) @handle_required -def import_child(request): +def route_view(request): """ - Import a repository response. + Display a list of global routing table entries which match resources + listed in received certificates. + """ conf = request.session['handle'] log = request.META['wsgi.errors'] - if request.method == 'POST': - form = forms.ImportChildForm(conf, request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='identity', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_child(log, conf, form.cleaned_data['handle'], f) + routes = [] + for p in models.ResourceRangeAddressV4.objects.filter(cert__parent__in=conf.parents.all()): + r = p.as_resource_range() + print >>log, 'querying for routes matching %s' % r + routes.extend([validate_route(*x) for x in roa_match(r)]) + for p in models.ResourceRangeAddressV6.objects.filter(cert__parent__in=conf.parents.all()): + r = p.as_resource_range() + print >>log, 'querying for routes matching %s' % r + routes.extend([validate_route(*x) for x in roa_match(r)]) - os.remove(tmpf.name) + ts = dict((attr['name'], attr['ts']) for attr in models.Timestamp.objects.values()) + return render(request, 'app/routes_view.html', + {'routes': routes, 'timestamp': ts}) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ImportChildForm(conf) - return render('rpkigui/import_child_form.html', { 'form': form }, request) +def route_detail(request, pk): + pass -@login_required -def initialize(request): - """ - Initialize a new user account. - """ - if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST) - if form.is_valid(): - glue.initialize_handle(request.META['wsgi.errors'], handle=request.user.username, owner=request.user) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.GenericConfirmationForm() - return render('rpkigui/initialize_form.html', { 'form': form }, request) +def route_roa_list(request, pk): + """Show a list of ROAs that match a given route.""" + object = get_object_or_404(models.RouteOrigin, pk=pk) + # select accepted ROAs which cover this route + qs = ROAPrefixV4.objects.filter(prefix_min__lte=object.prefix_min, + prefix_max__gte=object.prefix_max).select_related() + return object_list(request, qs, template_name='app/route_roa_list.html') + @handle_required -def child_wizard(request): - """ - Wizard mode to create a new locally hosted child. - """ +def repository_list(request): conf = request.session['handle'] - log = request.META['wsgi.errors'] - if not request.user.is_superuser: - return http.HttpResponseForbidden() + qs = models.Repository.objects.filter(issuer=conf) + return object_list(request, queryset=qs, + template_name='app/repository_list.html', + extra_context={ + 'create_url': reverse(repository_import), + 'create_label': u'Import'}) - if request.method == 'POST': - form = forms.ChildWizardForm(conf, request.POST) - if form.is_valid(): - glue.create_child(log, conf, form.cleaned_data['handle']) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ChildWizardForm(conf) - - return render('rpkigui/child_wizard_form.html', { 'form': form }, request) @handle_required -def export_child_response(request, child_handle): - """ - Export the XML file containing the output of the configure_child - to send back to the client. - """ +def repository_detail(request, pk): conf = request.session['handle'] - log = request.META['wsgi.errors'] - return serve_xml(glue.read_child_response(log, conf, child_handle), child_handle) + qs = models.Repository.objects.filter(issuer=conf) + return object_detail(request, queryset=qs, object_id=pk, + template_name='app/repository_detail.html') -@handle_required -def export_child_repo_response(request, child_handle): - """ - Export the XML file containing the output of the configure_child - to send back to the client. - """ - conf = request.session['handle'] - log = request.META['wsgi.errors'] - return serve_xml(glue.read_child_repo_response(log, conf, child_handle), child_handle) @handle_required -def update_bpki(request): - conf = request.session['handle'] +def repository_delete(request, pk): log = request.META['wsgi.errors'] - + conf = request.session['handle'] + # Ensure the repository being deleted belongs to the current user. + obj = get_object_or_404(models.Repository, issuer=conf, pk=pk) if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - glue.update_bpki(log, conf) - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_repository(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(repository_list)) else: - form = forms.GenericConfirmationForm() + form = form_class() + return render(request, 'app/repository_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) - return render('rpkigui/update_bpki_form.html', { 'form': form }, request) @handle_required -def child_delete(request, child_handle): - conf = request.session['handle'] +def repository_import(request): + """Import XML response file from repository operator.""" + return generic_import(request, + models.Repository.objects, + Zookeeper.configure_repository, + form_class=forms.ImportRepositoryForm, + post_import_redirect=reverse(repository_list)) + + +@superuser_required +def client_list(request): + return object_list(request, queryset=models.Client.objects.all(), + extra_context={ + 'create_url': reverse(client_import), + 'create_label': u'Import'}) + + +@superuser_required +def client_detail(request, pk): + return object_detail(request, queryset=models.Client.objects, object_id=pk) + + +@superuser_required +def client_delete(request, pk): log = request.META['wsgi.errors'] - child = get_object_or_404(conf.children, handle__exact=child_handle) - + obj = get_object_or_404(models.Client, pk=pk) + form_class = forms.UserDeleteForm # FIXME if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - glue.delete_child(log, conf, child_handle) - child.delete() - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(logstream=log) + z.delete_publication_client(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(client_list)) else: - form = forms.GenericConfirmationForm() + form = form_class() + return render(request, 'app/client_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) - return render('rpkigui/child_delete_form.html', { 'form': form , 'object': child }, request) -@handle_required -def parent_delete(request, parent_handle): - conf = request.session['handle'] +@superuser_required +def client_import(request): + return generic_import(request, models.Client.objects, + Zookeeper.configure_publication_client, + form_class=forms.ImportClientForm, + post_import_redirect=reverse(client_list)) + + +@superuser_required +def client_export(request, pk): + """Return the XML file resulting from a configure_publication_client + request. + + """ + client = get_object_or_404(models.Client, pk=pk) + z = Zookeeper() + xml = z.generate_repository_response(client) + return serve_xml(str(xml), '%s.repo' % z.handle) + + +@superuser_required +def user_list(request): + """Display a list of all the RPKI handles managed by this server.""" + # create a list of tuples of (Conf, User) + users = [] + for conf in models.Conf.objects.all(): + try: + u = User.objects.get(username=conf.handle) + except User.DoesNotExist: + u = None + users.append((conf, u)) + return render(request, 'app/user_list.html', {'users': users}) + + +@superuser_required +def user_detail(request): + """Placeholder for Conf.get_absolute_url().""" + pass + + +@superuser_required +def user_delete(request, pk): + conf = models.Conf.objects.get(pk=pk) log = request.META['wsgi.errors'] - parent = get_object_or_404(conf.parents, handle__exact=parent_handle) + if request.method == 'POST': + form = forms.UserDeleteForm(request.POST) + if form.is_valid(): + User.objects.filter(username=conf.handle).delete() + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_self() + z.synchronize() + return http.HttpResponseRedirect(reverse(user_list)) + else: + form = forms.UserDeleteForm() + return render(request, 'app/user_confirm_delete.html', + {'object': conf, 'form': form}) + + +@superuser_required +def user_edit(request, pk): + conf = get_object_or_404(models.Conf, pk=pk) + # in the old model, there may be users with a different name, so create a + # new user object if it is missing. + try: + user = User.objects.get(username=conf.handle) + except User.DoesNotExist: + user = User(username=conf.handle) if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = forms.UserEditForm(request.POST) if form.is_valid(): - glue.delete_parent(log, conf, parent_handle) - parent.delete() - return http.HttpResponseRedirect(reverse(dashboard)) + pw = form.cleaned_data.get('pw') + if pw: + user.set_password(pw) + user.email = form.cleaned_data.get('email') + user.save() + return http.HttpResponseRedirect(reverse(user_list)) else: - form = forms.GenericConfirmationForm() + form = forms.UserEditForm(initial={'email': user.email}) + return render(request, 'app/user_edit_form.html', + {'object': user, 'form': form}) - return render('rpkigui/parent_form.html', { 'form': form , - 'parent': parent, 'submit_label': 'Delete' }, request) -@login_required -def destroy_handle(request, handle): - """ - Completely remove a hosted resource handle. +@handle_required +def user_create(request): """ + Wizard mode to create a new locally hosted child. - log = request.META['wsgi.errors'] - + """ if not request.user.is_superuser: return http.HttpResponseForbidden() - conf = get_object_or_404(models.Conf, handle=handle) - + log = request.META['wsgi.errors'] if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = forms.UserCreateForm(request.POST, request.FILES) if form.is_valid(): - glue.destroy_handle(log, handle) - return render('rpkigui/generic_result.html', - { 'operation': 'Destroy ' + handle, - 'result': 'Succeeded' }, request) - else: - form = forms.GenericConfirmationForm() + handle = form.cleaned_data.get('handle') + pw = form.cleaned_data.get('password') + email = form.cleaned_data.get('email') + parent = form.cleaned_data.get('parent') + + User.objects.create_user(handle, email, pw) + + zk_child = Zookeeper(handle=handle, logstream=log) + identity_xml = zk_child.initialize() + if parent: + # FIXME etree_wrapper should allow us to deal with file objects + t = NamedTemporaryFile(delete=False) + t.close() + + identity_xml.save(t.name) + zk_parent = Zookeeper(handle=parent.handle, logstream=log) + parent_response, _ = zk_parent.configure_child(t.name) + parent_response.save(t.name) + repo_req, _ = zk_child.configure_parent(t.name) + repo_req.save(t.name) + repo_resp, _ = zk_parent.configure_publication_client(t.name) + repo_resp.save(t.name) + zk_child.configure_repository(t.name) + os.remove(t.name) + zk_child.synchronize() - return render('rpkigui/destroy_handle_form.html', { 'form': form , - 'handle': handle }, request) + return http.HttpResponseRedirect(reverse(dashboard)) + else: + conf = request.session['handle'] + form = forms.UserCreateForm(initial={'parent': conf}) -# vim:sw=4 ts=8 expandtab + return render(request, 'app/user_create_form.html', {'form': form}) diff --git a/rpkid/rpki/gui/cacheview/admin.py b/rpkid/rpki/gui/cacheview/admin.py deleted file mode 100644 index 05bab881..00000000 --- a/rpkid/rpki/gui/cacheview/admin.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -$Id$ - -Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" - -from django.contrib import admin -from rpki.gui.cacheview import models - -class ASRangeAdmin(admin.ModelAdmin): - pass - -class AddressRangeAdmin(admin.ModelAdmin): - pass - -class CertAdmin(admin.ModelAdmin): - pass - -class ROAPrefixAdmin(admin.ModelAdmin): - pass - -class ROAAdmin(admin.ModelAdmin): - pass - -class GhostbusterAdmin(admin.ModelAdmin): - pass - -class ValidationLabelAdmin(admin.ModelAdmin): pass - -class ValidationStatus_CertAdmin(admin.ModelAdmin): pass - -class ValidationStatus_ROAAdmin(admin.ModelAdmin): pass - -class ValidationStatus_GhostbusterAdmin(admin.ModelAdmin): pass - -admin.site.register(models.AddressRange, AddressRangeAdmin) -admin.site.register(models.ASRange, AddressRangeAdmin) -admin.site.register(models.Cert, CertAdmin) -admin.site.register(models.Ghostbuster, GhostbusterAdmin) -admin.site.register(models.ROA, ROAAdmin) -admin.site.register(models.ROAPrefix, ROAPrefixAdmin) -admin.site.register(models.ValidationLabel, ValidationLabelAdmin) -admin.site.register(models.ValidationStatus_Cert, ValidationStatus_CertAdmin) -admin.site.register(models.ValidationStatus_ROA, ValidationStatus_ROAAdmin) -admin.site.register(models.ValidationStatus_Ghostbuster, ValidationStatus_GhostbusterAdmin) - -# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py index 077a28ff..4be45b5c 100644 --- a/rpkid/rpki/gui/cacheview/models.py +++ b/rpkid/rpki/gui/cacheview/models.py @@ -1,106 +1,87 @@ -""" -Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -""" +# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' from datetime import datetime import time from django.db import models -from rpki.resource_set import resource_range_ipv4, resource_range_ipv6 -from rpki.exceptions import MustBePrefix +import rpki.ipaddrs +import rpki.resource_set +import rpki.gui.models + class TelephoneField(models.CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = 255 models.CharField.__init__(self, *args, **kwargs) -class AddressRange(models.Model): - family = models.IntegerField() - min = models.IPAddressField(db_index=True) - max = models.IPAddressField(db_index=True) - - class Meta: - ordering = ('family', 'min', 'max') - unique_together = ('family', 'min', 'max') +class AddressRange(rpki.gui.models.PrefixV4): @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.addressrange_detail', [str(self.pk)]) - def __unicode__(self): - if self.min == self.max: - return u'%s' % self.min - - if self.family == 4: - r = resource_range_ipv4.from_strings(self.min, self.max) - elif self.family == 6: - r = resource_range_ipv6.from_strings(self.min, self.max) - - try: - prefixlen = r.prefixlen() - except MustBePrefix: - return u'%s-%s' % (self.min, self.max) - return u'%s/%d' % (self.min, prefixlen) -class ASRange(models.Model): - min = models.PositiveIntegerField(db_index=True) - max = models.PositiveIntegerField(db_index=True) - - class Meta: - ordering = ('min', 'max') - #unique_together = ('min', 'max') +class AddressRangeV6(rpki.gui.models.PrefixV6): + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.addressrange_detail_v6', + [str(self.pk)]) - def __unicode__(self): - if self.min == self.max: - return u'AS%d' % self.min - else: - return u'AS%s-%s' % (self.min, self.max) +class ASRange(rpki.gui.models.ASN): @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.asrange_detail', [str(self.pk)]) kinds = list(enumerate(('good', 'warn', 'bad'))) -kinds_dict = dict((v,k) for k,v in kinds) +kinds_dict = dict((v, k) for k, v in kinds) + class ValidationLabel(models.Model): """ Represents a specific error condition defined in the rcynic XML output file. """ - label = models.CharField(max_length=30, db_index=True, unique=True) + label = models.CharField(max_length=79, db_index=True, unique=True) status = models.CharField(max_length=255) kind = models.PositiveSmallIntegerField(choices=kinds) def __unicode__(self): return self.label - class Meta: - verbose_name_plural = 'ValidationLabels' + +class RepositoryObject(models.Model): + """ + Represents a globally unique RPKI repository object, specified by its URI. + """ + uri = models.URLField(unique=True, db_index=True) generations = list(enumerate(('current', 'backup'))) generations_dict = dict((val, key) for (key, val) in generations) + class ValidationStatus(models.Model): - timestamp = models.DateTimeField() + timestamp = models.DateTimeField() generation = models.PositiveSmallIntegerField(choices=generations, null=True) - status = models.ForeignKey('ValidationLabel') + status = models.ForeignKey(ValidationLabel) + repo = models.ForeignKey(RepositoryObject, related_name='statuses') - class Meta: - abstract = True class SignedObject(models.Model): """ @@ -108,24 +89,20 @@ class SignedObject(models.Model): The signing certificate is ommitted here in order to give a proper value for the 'related_name' attribute. """ - # attributes from rcynic's output XML file - uri = models.URLField(unique=True, db_index=True) + repo = models.ForeignKey(RepositoryObject, related_name='cert', unique=True) # on-disk file modification time - mtime = models.PositiveIntegerField(default=0) + mtime = models.PositiveIntegerField(default=0) # SubjectName - name = models.CharField(max_length=255) + name = models.CharField(max_length=255) # value from the SKI extension - keyid = models.CharField(max_length=50, db_index=True) + keyid = models.CharField(max_length=60, db_index=True) # validity period from EE cert which signed object not_before = models.DateTimeField() - not_after = models.DateTimeField() - - class Meta: - abstract = True + not_after = models.DateTimeField() def mtime_as_datetime(self): """ @@ -133,13 +110,6 @@ class SignedObject(models.Model): """ return datetime.utcfromtimestamp(self.mtime + time.timezone) - def is_valid(self): - """ - Returns a boolean value indicating whether this object has passed - validation checks. - """ - return bool(self.statuses.filter(status=ValidationLabel.objects.get(label="object_accepted"))) - def status_id(self): """ Returns a HTML class selector for the current object based on its validation status. @@ -149,70 +119,90 @@ class SignedObject(models.Model): for x in reversed(kinds): if self.statuses.filter(generation=generations_dict['current'], status__kind=x[0]): return x[1] - return None # should not happen + return None # should not happen def __unicode__(self): return u'%s' % self.name + class Cert(SignedObject): """ Object representing a resource certificate. """ addresses = models.ManyToManyField(AddressRange, related_name='certs') - asns = models.ManyToManyField(ASRange, related_name='certs') - issuer = models.ForeignKey('Cert', related_name='children', null=True, blank=True) - sia = models.CharField(max_length=255) + addresses_v6 = models.ManyToManyField(AddressRangeV6, related_name='certs') + asns = models.ManyToManyField(ASRange, related_name='certs') + issuer = models.ForeignKey('self', related_name='children', null=True) + sia = models.CharField(max_length=255) @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.cert_detail', [str(self.pk)]) -class ValidationStatus_Cert(ValidationStatus): - cert = models.ForeignKey('Cert', related_name='statuses') class ROAPrefix(models.Model): - family = models.PositiveIntegerField() - prefix = models.IPAddressField() - bits = models.PositiveIntegerField() - max_length = models.PositiveIntegerField() + "Abstract base class for ROA mixin." + + max_length = models.PositiveSmallIntegerField() class Meta: - ordering = ['family', 'prefix', 'bits', 'max_length'] + abstract = True + + def as_roa_prefix(self): + "Return value as a rpki.resource_set.roa_prefix_ip object." + rng = self.as_resource_range() + return self.roa_cls(rng.prefix_min, rng.prefixlen(), self.max_length) def __unicode__(self): - if self.bits == self.max_length: - return u'%s/%d' % (self.prefix, self.bits) - else: - return u'%s/%d-%d' % (self.prefix, self.bits, self.max_length) + p = self.as_resource_range() + if p.prefixlen() == self.max_length: + return str(p) + return '%s-%s' % (str(p), self.max_length) + + +# ROAPrefix is declared first, so subclass picks up __unicode__ from it. +class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4): + "One v4 prefix in a ROA." + + roa_cls = rpki.resource_set.roa_prefix_ipv4 + + class Meta: + ordering = ('prefix_min',) + + +# ROAPrefix is declared first, so subclass picks up __unicode__ from it. +class ROAPrefixV6(ROAPrefix, rpki.gui.models.PrefixV6): + "One v6 prefix in a ROA." + + roa_cls = rpki.resource_set.roa_prefix_ipv6 + + class Meta: + ordering = ('prefix_min',) + class ROA(SignedObject): - asid = models.PositiveIntegerField() - prefixes = models.ManyToManyField(ROAPrefix, related_name='roas') - issuer = models.ForeignKey('Cert', related_name='roas') + asid = models.PositiveIntegerField() + prefixes = models.ManyToManyField(ROAPrefixV4, related_name='roas') + prefixes_v6 = models.ManyToManyField(ROAPrefixV6, related_name='roas') + issuer = models.ForeignKey('Cert', related_name='roas') @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)]) class Meta: - ordering = ['asid'] + ordering = ('asid',) def __unicode__(self): return u'ROA for AS%d' % self.asid - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)]) - -class ValidationStatus_ROA(ValidationStatus): - roa = models.ForeignKey('ROA', related_name='statuses') class Ghostbuster(SignedObject): - full_name = models.CharField(max_length=40) + full_name = models.CharField(max_length=40) email_address = models.EmailField(blank=True, null=True) - organization = models.CharField(blank=True, null=True, max_length=255) - telephone = TelephoneField(blank=True, null=True) - issuer = models.ForeignKey('Cert', related_name='ghostbusters') + organization = models.CharField(blank=True, null=True, max_length=255) + telephone = TelephoneField(blank=True, null=True) + issuer = models.ForeignKey('Cert', related_name='ghostbusters') @models.permalink def get_absolute_url(self): @@ -226,8 +216,3 @@ class Ghostbuster(SignedObject): if self.email_address: return self.email_address return self.telephone - -class ValidationStatus_Ghostbuster(ValidationStatus): - gbr = models.ForeignKey('Ghostbuster', related_name='statuses') - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/models.py b/rpkid/rpki/gui/models.py new file mode 100644 index 00000000..749f335f --- /dev/null +++ b/rpkid/rpki/gui/models.py @@ -0,0 +1,132 @@ +""" +$Id$ + +Copyright (C) 2012 SPARTA, Inc. a Parsons Company + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Common classes for reuse in apps. +""" + +import struct + +from django.db import models + +import rpki.resource_set +import rpki.ipaddrs + +class IPv6AddressField(models.Field): + "Field large enough to hold a 128-bit unsigned integer." + + __metaclass__ = models.SubfieldBase + + def db_type(self, connection): + return 'binary(16)' + + def to_python(self, value): + if isinstance(value, rpki.ipaddrs.v6addr): + return value + x = struct.unpack('!QQ', value) + return rpki.ipaddrs.v6addr((x[0] << 64) | x[1]) + + def get_db_prep_value(self, value, connection, prepared): + return struct.pack('!QQ', (long(value) >> 64) & 0xFFFFFFFFFFFFFFFFL, long(value) & 0xFFFFFFFFFFFFFFFFL) + +class IPv4AddressField(models.Field): + "Wrapper around rpki.ipaddrs.v4addr." + + __metaclass__ = models.SubfieldBase + + def db_type(self, connection): + return 'int UNSIGNED' + + def to_python(self, value): + if isinstance(value, rpki.ipaddrs.v4addr): + return value + return rpki.ipaddrs.v4addr(value) + + def get_db_prep_value(self, value, connection, prepared): + return long(value) + +class Prefix(models.Model): + """Common implementation for models with an IP address range. + + Expects that `range_cls` is set to the appropriate subclass of + rpki.resource_set.resource_range_ip.""" + + def as_resource_range(self): + """ + Returns the prefix as a rpki.resource_set.resource_range_ip object. + """ + return self.range_cls(self.prefix_min, self.prefix_max) + + def prefixlen(self): + "Returns the prefix length for the prefix in this object." + return self.as_resource_range().prefixlen() + + def get_prefix_display(self): + "Return a string representatation of this IP prefix." + return str(self.as_resource_range()) + + def __unicode__(self): + """This method may be overridden by subclasses. The default + implementation calls get_prefix_display(). """ + return self.get_prefix_display() + + class Meta: + abstract = True + + # default sort order reflects what "sh ip bgp" outputs + ordering = ('prefix_min',) + +class PrefixV4(Prefix): + "IPv4 Prefix." + + range_cls = rpki.resource_set.resource_range_ipv4 + + prefix_min = IPv4AddressField(db_index=True, null=False) + prefix_max = IPv4AddressField(db_index=True, null=False) + + class Meta(Prefix.Meta): + abstract = True + +class PrefixV6(Prefix): + "IPv6 Prefix." + + range_cls = rpki.resource_set.resource_range_ipv6 + + prefix_min = IPv6AddressField(db_index=True, null=False) + prefix_max = IPv6AddressField(db_index=True, null=False) + + class Meta(Prefix.Meta): + abstract = True + +class ASN(models.Model): + """Represents a range of ASNs. + + This model is abstract, and is intended to be reused by applications.""" + + min = models.PositiveIntegerField(null=False) + max = models.PositiveIntegerField(null=False) + + class Meta: + abstract = True + ordering = ('min', 'max') + + def as_resource_range(self): + return rpki.resource_set.resource_range_as(self.min, self.max) + + def __unicode__(self): + return u'AS%s' % self.as_resource_range() + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/routeview/__init__.py b/rpkid/rpki/gui/routeview/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/routeview/__init__.py diff --git a/rpkid/rpki/gui/routeview/models.py b/rpkid/rpki/gui/routeview/models.py new file mode 100644 index 00000000..321fde5d --- /dev/null +++ b/rpkid/rpki/gui/routeview/models.py @@ -0,0 +1,46 @@ +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id' + +from django.db.models import PositiveIntegerField +import rpki.gui.models + + +class RouteOrigin(rpki.gui.models.PrefixV4): + "Represents an IPv4 BGP routing table entry." + + asn = PositiveIntegerField(help_text='origin AS', null=False) + + def __unicode__(self): + return u"AS%d's route origin for %s" % (self.asn, + self.get_prefix_display()) + + class Meta: + # sort by increasing mask length (/16 before /24) + ordering = ('prefix_min', '-prefix_max') + + +class RouteOriginV6(rpki.gui.models.PrefixV6): + "Represents an IPv6 BGP routing table entry." + + asn = PositiveIntegerField(help_text='origin AS', null=False) + + def __unicode__(self): + return u"AS%d's route origin for %s" % (self.asn, + self.get_prefix_display()) + + class Meta: + ordering = ('prefix_min', '-prefix_max') diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py index 70ea4056..d643ad27 100644 --- a/rpkid/rpki/gui/urls.py +++ b/rpkid/rpki/gui/urls.py @@ -1,21 +1,19 @@ -# $Id$ - -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -Permission to use, copy, modify, and distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. - -""" +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' from django.conf.urls.defaults import * @@ -24,17 +22,22 @@ admin.autodiscover() urlpatterns = patterns('', - # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' # to INSTALLED_APPS to enable admin documentation: - (r'^admin/doc/', include('django.contrib.admindocs.urls')), + #(r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: - (r'^admin/', include(admin.site.urls)), + #(r'^admin/', include(admin.site.urls)), (r'^rpki/', include('rpki.gui.app.urls')), (r'^cacheview/', include('rpki.gui.cacheview.urls')), (r'^accounts/login/$', 'django.contrib.auth.views.login'), (r'^accounts/logout/$', 'django.contrib.auth.views.logout', - { 'next_page': '/rpki/' }), + {'next_page': '/rpki/'}), + + # !!!REMOVE THIS BEFORE COMMITTING!!! + # for testing with the django test webserver + (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', + {'document_root': '/usr/local/share/rpki/media'}), ) diff --git a/rpkid/rpki/http.py b/rpkid/rpki/http.py index 0df7e6f2..a0055ac9 100644 --- a/rpkid/rpki/http.py +++ b/rpkid/rpki/http.py @@ -534,7 +534,7 @@ class http_server(http_stream): raise except Exception, e: rpki.log.traceback() - self.send_error(500, "Unhandled exception %s" % e) + self.send_error(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) else: self.send_error(code = error[0], reason = error[1]) diff --git a/rpkid/rpki/ipaddrs.py b/rpkid/rpki/ipaddrs.py index 531bcbb9..a192f92b 100644 --- a/rpkid/rpki/ipaddrs.py +++ b/rpkid/rpki/ipaddrs.py @@ -57,6 +57,8 @@ class v4addr(long): """ Construct a v4addr object. """ + if isinstance(x, unicode): + x = x.encode("ascii") if isinstance(x, str): return cls.from_bytes(socket.inet_pton(socket.AF_INET, ".".join(str(int(i)) for i in x.split(".")))) else: @@ -94,6 +96,8 @@ class v6addr(long): """ Construct a v6addr object. """ + if isinstance(x, unicode): + x = x.encode("ascii") if isinstance(x, str): return cls.from_bytes(socket.inet_pton(socket.AF_INET6, x)) else: diff --git a/rpkid/portal-gui/apache/rpki.wsgi.in b/rpkid/rpki/irdb/__init__.py index 8cddef95..3eb6fab7 100644 --- a/rpkid/portal-gui/apache/rpki.wsgi.in +++ b/rpkid/rpki/irdb/__init__.py @@ -1,23 +1,23 @@ -# $Id$ """ -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +Django really wants its models packaged as a models module within a +Python package, so humor it. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY -AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - - -This is an example wsgi application for use with mod_wsgi and apache. - """ -import django.core.handlers.wsgi -application = django.core.handlers.wsgi.WSGIHandler() +from rpki.irdb.models import * +from rpki.irdb.zookeeper import Zookeeper diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py new file mode 100644 index 00000000..3aaebdcf --- /dev/null +++ b/rpkid/rpki/irdb/models.py @@ -0,0 +1,585 @@ +""" +IR Database, Django-style. + +This is the back-end code's interface to the database. It's intended +to be usable by command line programs and other scripts, not just +Django GUI code, so be careful. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import django.db.models +import rpki.x509 +import rpki.sundial +import rpki.resource_set +import rpki.ipaddrs +import socket + +## @var ip_version_choices +# Choice argument for fields implementing IP version numbers. + +ip_version_choices = ((4, "IPv4"), (6, "IPv6")) + +## @var ca_certificate_lifetime +# Lifetime for a BPKI CA certificate. + +ca_certificate_lifetime = rpki.sundial.timedelta(days = 3652) + +## @var crl_interval +# Expected interval between BPKI CRL updates + +crl_interval = rpki.sundial.timedelta(days = 1) + +## @var ee_certificate_lifetime +# Lifetime for a BPKI EE certificate. + +ee_certificate_lifetime = rpki.sundial.timedelta(days = 60) + +### + +# Field types + +class HandleField(django.db.models.CharField): + """ + A handle field type. + """ + + description = 'A "handle" in one of the RPKI protocols' + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 120 + django.db.models.CharField.__init__(self, *args, **kwargs) + +class EnumField(django.db.models.PositiveSmallIntegerField): + """ + An enumeration type that uses strings in Python and small integers + in SQL. + """ + + description = "An enumeration type" + + __metaclass__ = django.db.models.SubfieldBase + + def __init__(self, *args, **kwargs): + if isinstance(kwargs["choices"], (tuple, list)) and isinstance(kwargs["choices"][0], str): + kwargs["choices"] = tuple(enumerate(kwargs["choices"], 1)) + django.db.models.PositiveSmallIntegerField.__init__(self, *args, **kwargs) + self.enum_i2s = dict(self.flatchoices) + self.enum_s2i = dict((v, k) for k, v in self.flatchoices) + + def to_python(self, value): + return self.enum_i2s.get(value, value) + + def get_prep_value(self, value): + return self.enum_s2i.get(value, value) + +class SundialField(django.db.models.DateTimeField): + """ + A field type for our customized datetime objects. + """ + __metaclass__ = django.db.models.SubfieldBase + + description = "A datetime type using our customized datetime objects" + + def to_python(self, value): + if isinstance(value, rpki.sundial.pydatetime.datetime): + return rpki.sundial.datetime.fromdatetime( + django.db.models.DateTimeField.to_python(self, value)) + else: + return value + + def get_prep_value(self, value): + if isinstance(value, rpki.sundial.datetime): + return value.to_sql() + else: + return value + +### + +# Kludge to work around Django 1.2 problem. +# +# This should be a simple abstract base class DERField which we then +# subclass with trivial customization for specific kinds of DER +# objects. Sadly, subclassing of user defined field classes doesn't +# work in Django 1.2 with the django.db.models.SubfieldBase metaclass, +# so instead we fake it by defining methods externally and defining +# each concrete class as a direct subclass of django.db.models.Field. +# +# The bug has been fixed in Django 1.3, so we can revert this to the +# obvious form once we're ready to require Django 1.3 or later. The +# fix may have been backported to the 1.2 branch, but trying to test +# for it is likely more work than just working around it. +# +# See https://code.djangoproject.com/ticket/10728 for details. + +def DERField_init(self, *args, **kwargs): + kwargs["serialize"] = False + kwargs["blank"] = True + kwargs["default"] = None + django.db.models.Field.__init__(self, *args, **kwargs) + +def DERField_db_type(self, connection): + if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql": + return "bytea" + else: + return "BLOB" + +def DERField_to_python(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, str): + return self.rpki_type(DER = value) + else: + return value + +def DERField_get_prep_value(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, self.rpki_type): + return value.get_DER() + else: + return value + +def DERField(cls): + cls.__init__ = DERField_init + cls.db_type = DERField_db_type + cls.to_python = DERField_to_python + cls.get_prep_value = DERField_get_prep_value + return cls + +@DERField +class CertificateField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "X.509 certificate" + rpki_type = rpki.x509.X509 + +@DERField +class RSAKeyField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "RSA keypair" + rpki_type = rpki.x509.RSA + +@DERField +class CRLField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "Certificate Revocation List" + rpki_type = rpki.x509.CRL + +@DERField +class PKCS10Field(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "PKCS #10 certificate request" + rpki_type = rpki.x509.PKCS10 + +@DERField +class SignedReferralField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "CMS signed object containing XML" + rpki_type = rpki.x509.SignedReferral + +### + +# Custom managers + +class CertificateManager(django.db.models.Manager): + + def get_or_certify(self, **kwargs): + """ + Sort of like .get_or_create(), but for models containing + certificates which need to be generated based on other fields. + + Takes keyword arguments like .get(), checks for existing object. + If none, creates a new one; if found an existing object but some + of the non-key fields don't match, updates the existing object. + Runs certification method for new or updated objects. Returns a + tuple consisting of the object and a boolean indicating whether + anything has changed. + """ + + changed = False + + try: + obj = self.get(**self._get_or_certify_keys(kwargs)) + + except self.model.DoesNotExist: + obj = self.model(**kwargs) + changed = True + + else: + for k in kwargs: + if getattr(obj, k) != kwargs[k]: + setattr(obj, k, kwargs[k]) + changed = True + + if changed: + obj.avow() + obj.save() + + return obj, changed + + def _get_or_certify_keys(self, kwargs): + assert len(self.model._meta.unique_together) == 1 + return dict((k, kwargs[k]) for k in self.model._meta.unique_together[0]) + +class ResourceHolderCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "handle" : kwargs["handle"] } + +class ServerCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "pk" : 1 } + +class ResourceHolderEEManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "issuer" : kwargs["issuer"] } + +### + +class CA(django.db.models.Model): + certificate = CertificateField() + private_key = RSAKeyField() + latest_crl = CRLField() + + # Might want to bring these into line with what rpkid does. Current + # variables here were chosen to map easily to what OpenSSL command + # line tool was keeping on disk. + + next_serial = django.db.models.BigIntegerField(default = 1) + next_crl_number = django.db.models.BigIntegerField(default = 1) + last_crl_update = SundialField() + next_crl_update = SundialField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + now = rpki.sundial.now() + notAfter = now + ca_certificate_lifetime + self.certificate = rpki.x509.X509.bpki_self_certify( + keypair = self.private_key, + subject_name = self.subject_name, + serial = self.next_serial, + now = now, + notAfter = notAfter) + self.next_serial += 1 + self.generate_crl() + return self.certificate + + def certify(self, subject_name, subject_key, validity_interval, is_ca, pathLenConstraint = None): + now = rpki.sundial.now() + notAfter = now + validity_interval + result = self.certificate.bpki_certify( + keypair = self.private_key, + subject_name = subject_name, + subject_key = subject_key, + serial = self.next_serial, + now = now, + notAfter = notAfter, + is_ca = is_ca, + pathLenConstraint = pathLenConstraint) + self.next_serial += 1 + return result + + def revoke(self, cert): + Revocations.objects.create( + issuer = self, + revoked = rpki.sundial.now(), + serial = cert.certificate.getSerial(), + expires = cert.certificate.getNotAfter() + crl_interval) + cert.delete() + self.generate_crl() + + def generate_crl(self): + now = rpki.sundial.now() + self.revocations.filter(expires__lt = now).delete() + revoked = [(r.serial, rpki.sundial.datetime.fromdatetime(r.revoked).toASN1tuple(), ()) + for r in self.revocations.all()] + self.latest_crl = rpki.x509.CRL.generate( + keypair = self.private_key, + issuer = self.certificate, + serial = self.next_crl_number, + thisUpdate = now, + nextUpdate = now + crl_interval, + revokedCertificates = revoked) + self.last_crl_update = now + self.next_crl_update = now + crl_interval + self.next_crl_number += 1 + +class ServerCA(CA): + objects = ServerCAManager() + + def __unicode__(self): + return "" + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI server CA" % socket.gethostname()) + +class ResourceHolderCA(CA): + handle = HandleField(unique = True) + objects = ResourceHolderCAManager() + + def __unicode__(self): + return self.handle + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI resource CA" % self.handle) + +class Certificate(django.db.models.Model): + + certificate = CertificateField() + objects = CertificateManager() + + class Meta: + abstract = True + unique_together = ("issuer", "handle") + + def revoke(self): + self.issuer.revoke(self) + +class CrossCertification(Certificate): + handle = HandleField() + ta = CertificateField() + + class Meta: + abstract = True + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.ta.getSubject(), + subject_key = self.ta.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 0) + + def __unicode__(self): + return self.handle + +class HostedCA(Certificate): + issuer = django.db.models.ForeignKey(ServerCA) + hosted = django.db.models.OneToOneField(ResourceHolderCA, related_name = "hosted_by") + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.hosted.certificate.getSubject(), + subject_key = self.hosted.certificate.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 1) + + class Meta: + unique_together = ("issuer", "hosted") + + def __unicode__(self): + return self.hosted_ca.handle + +class Revocation(django.db.models.Model): + serial = django.db.models.BigIntegerField() + revoked = SundialField() + expires = SundialField() + + class Meta: + abstract = True + unique_together = ("issuer", "serial") + +class ServerRevocation(Revocation): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "revocations") + +class ResourceHolderRevocation(Revocation): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "revocations") + +class EECertificate(Certificate): + private_key = RSAKeyField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + self.certificate = self.issuer.certify( + subject_name = self.subject_name, + subject_key = self.private_key.get_RSApublic(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + +class ServerEE(EECertificate): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "ee_certificates") + purpose = EnumField(choices = ("rpkid", "pubd", "irdbd", "irbe")) + + class Meta: + unique_together = ("issuer", "purpose") + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI %s EE" % (socket.gethostname(), self.get_purpose_display())) + +class Referral(EECertificate): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "referral_certificate") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI Referral EE" % self.issuer.handle) + +class Turtle(django.db.models.Model): + service_uri = django.db.models.CharField(max_length = 255) + +class Rootd(EECertificate, Turtle): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "rootd") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI rootd EE" % self.issuer.handle) + +class BSC(Certificate): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "bscs") + handle = HandleField() + pkcs10 = PKCS10Field() + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.pkcs10.getSubject(), + subject_key = self.pkcs10.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + + def __unicode__(self): + return self.handle + +class Child(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "children") + name = django.db.models.TextField(null = True, blank = True) + valid_until = SundialField() + + @property + def resource_bag(self): + asns = rpki.resource_set.resource_set_as.from_django( + (a.start_as, a.end_as) for a in self.asns.all()) + ipv4 = rpki.resource_set.resource_set_ipv4.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv4')) + ipv6 = rpki.resource_set.resource_set_ipv6.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv6')) + return rpki.resource_set.resource_bag( + valid_until = self.valid_until, asn = asns, v4 = ipv4, v6 = ipv6) + + # Writing of .setter method deferred until something needs it. + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ChildASN(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "asns") + start_as = django.db.models.BigIntegerField() + end_as = django.db.models.BigIntegerField() + + def as_resource_range(self): + return rpki.resource_set.resource_range_as(self.start_as, self.end_as) + + class Meta: + unique_together = ("child", "start_as", "end_as") + +class ChildNet(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "address_ranges") + start_ip = django.db.models.CharField(max_length = 40) + end_ip = django.db.models.CharField(max_length = 40) + version = EnumField(choices = ip_version_choices) + + def as_resource_range(self): + return rpki.resource_set.resource_range_ip.from_strings(self.start_ip, self.end_ip) + + class Meta: + unique_together = ("child", "start_ip", "end_ip", "version") + +class Parent(CrossCertification, Turtle): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "parents") + parent_handle = HandleField() + child_handle = HandleField() + repository_type = EnumField(choices = ("none", "offer", "referral")) + referrer = HandleField(null = True, blank = True) + referral_authorization = SignedReferralField(null = True, blank = True) + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ROARequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "roa_requests") + asn = django.db.models.BigIntegerField() + + @property + def roa_prefix_bag(self): + v4 = rpki.resource_set.roa_prefix_set_ipv4.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv4')) + v6 = rpki.resource_set.roa_prefix_set_ipv6.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv6')) + return rpki.resource_set.roa_prefix_bag(v4 = v4, v6 = v6) + + # Writing of .setter method deferred until something needs it. + +class ROARequestPrefix(django.db.models.Model): + roa_request = django.db.models.ForeignKey(ROARequest, related_name = "prefixes") + version = EnumField(choices = ip_version_choices) + prefix = django.db.models.CharField(max_length = 40) + prefixlen = django.db.models.PositiveSmallIntegerField() + max_prefixlen = django.db.models.PositiveSmallIntegerField() + + def as_roa_prefix(self): + if self.version == 'IPv4': + return rpki.resource_set.roa_prefix_ipv4(rpki.ipaddrs.v4addr(self.prefix), self.prefixlen, self.max_prefixlen) + else: + return rpki.resource_set.roa_prefix_ipv6(rpki.ipaddrs.v6addr(self.prefix), self.prefixlen, self.max_prefixlen) + + def as_resource_range(self): + return self.as_roa_prefix().to_resource_range() + + class Meta: + unique_together = ("roa_request", "version", "prefix", "prefixlen", "max_prefixlen") + +class GhostbusterRequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "ghostbuster_requests") + parent = django.db.models.ForeignKey(Parent, related_name = "ghostbuster_requests", null = True) + vcard = django.db.models.TextField() + +class Repository(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "repositories") + client_handle = HandleField() + service_uri = django.db.models.CharField(max_length = 255) + sia_base = django.db.models.TextField() + turtle = django.db.models.OneToOneField(Turtle, related_name = "repository") + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class Client(CrossCertification): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "clients") + sia_base = django.db.models.TextField() + parent_handle = HandleField() + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") diff --git a/rpkid/rpki/irdb/zookeeper.py b/rpkid/rpki/irdb/zookeeper.py new file mode 100644 index 00000000..33f5264e --- /dev/null +++ b/rpkid/rpki/irdb/zookeeper.py @@ -0,0 +1,1264 @@ +""" +Management code for the IRDB. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import subprocess +import csv +import re +import os +import getopt +import sys +import base64 +import time +import glob +import copy +import warnings +import rpki.config +import rpki.cli +import rpki.sundial +import rpki.log +import rpki.oids +import rpki.http +import rpki.resource_set +import rpki.relaxng +import rpki.exceptions +import rpki.left_right +import rpki.x509 +import rpki.async +import rpki.irdb +import django.db.transaction + +from lxml.etree import (Element, SubElement, ElementTree, + fromstring as ElementFromString, + tostring as ElementToString) + +from rpki.csv_utils import (csv_reader, csv_writer, BadCSVSyntax) + + + +# XML namespace and protocol version for OOB setup protocol. The name +# is historical and may change before we propose this as the basis for +# a standard. + +myrpki_namespace = "http://www.hactrn.net/uris/rpki/myrpki/" +myrpki_version = "2" +myrpki_namespaceQName = "{" + myrpki_namespace + "}" + +myrpki_section = "myrpki" +irdbd_section = "irdbd" +rpkid_section = "rpkid" +pubd_section = "pubd" +rootd_section = "rootd" + +# A whole lot of exceptions + +class MissingHandle(Exception): "Missing handle" +class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon." +class BadXMLMessage(Exception): "Bad XML message." +class PastExpiration(Exception): "Expiration date has already passed." +class CantRunRootd(Exception): "Can't run rootd." + + + +def B64Element(e, tag, obj, **kwargs): + """ + Create an XML element containing Base64 encoded data taken from a + DER object. + """ + + if e is None: + se = Element(tag, **kwargs) + else: + se = SubElement(e, tag, **kwargs) + if e is not None and e.text is None: + e.text = "\n" + se.text = "\n" + obj.get_Base64() + se.tail = "\n" + return se + +class PEM_writer(object): + """ + Write PEM files to disk, keeping track of which ones we've already + written and setting the file mode appropriately. + """ + + def __init__(self, logstream = None): + self.wrote = set() + self.logstream = logstream + + def __call__(self, filename, obj): + filename = os.path.realpath(filename) + if filename in self.wrote: + return + tempname = filename + if not filename.startswith("/dev/"): + tempname += ".%s.tmp" % os.getpid() + mode = 0400 if filename.endswith(".key") else 0444 + if self.logstream is not None: + self.logstream.write("Writing %s\n" % filename) + f = os.fdopen(os.open(tempname, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode), "w") + f.write(obj.get_PEM()) + f.close() + if tempname != filename: + os.rename(tempname, filename) + self.wrote.add(filename) + + + + +def etree_read(filename): + """ + Read an etree from a file, verifying then stripping XML namespace + cruft. + """ + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + for i in e.getiterator(): + if i.tag.startswith(myrpki_namespaceQName): + i.tag = i.tag[len(myrpki_namespaceQName):] + else: + raise BadXMLMessage, "XML tag %r is not in namespace %r" % (i.tag, myrpki_namespace) + return e + + +class etree_wrapper(object): + """ + Wrapper for ETree objects so we can return them as function results + without requiring the caller to understand much about them. + + """ + + def __init__(self, e, msg = None, debug = False): + self.msg = msg + e = copy.deepcopy(e) + e.set("version", myrpki_version) + for i in e.getiterator(): + if i.tag[0] != "{": + i.tag = myrpki_namespaceQName + i.tag + assert i.tag.startswith(myrpki_namespaceQName) + if debug: + print ElementToString(e) + rpki.relaxng.myrpki.assertValid(e) + self.etree = e + + def __str__(self): + return ElementToString(self.etree) + + def save(self, filename, logstream = None): + filename = os.path.realpath(filename) + tempname = filename + if not filename.startswith("/dev/"): + tempname += ".%s.tmp" % os.getpid() + ElementTree(self.etree).write(tempname) + if tempname != filename: + os.rename(tempname, filename) + if logstream is not None: + logstream.write("Wrote %s\n" % filename) + if self.msg is not None: + logstream.write(self.msg + "\n") + + + +class Zookeeper(object): + + ## @var show_xml + # Whether to show XML for debugging + + show_xml = False + + def __init__(self, cfg = None, handle = None, logstream = None): + + if cfg is None: + cfg = rpki.config.parser() + + if handle is None: + handle = cfg.get("handle", section = myrpki_section) + + self.cfg = cfg + + self.logstream = logstream + + self.run_rpkid = cfg.getboolean("run_rpkid", section = myrpki_section) + self.run_pubd = cfg.getboolean("run_pubd", section = myrpki_section) + self.run_rootd = cfg.getboolean("run_rootd", section = myrpki_section) + + if self.run_rootd and (not self.run_pubd or not self.run_rpkid): + raise CantRunRootd, "Can't run rootd unless also running rpkid and pubd" + + self.default_repository = cfg.get("default_repository", "", section = myrpki_section) + self.pubd_contact_info = cfg.get("pubd_contact_info", "", section = myrpki_section) + + self.rsync_module = cfg.get("publication_rsync_module", section = myrpki_section) + self.rsync_server = cfg.get("publication_rsync_server", section = myrpki_section) + + self.reset_identity(handle) + + + def reset_identity(self, handle): + """ + Select handle of current resource holding entity. + """ + + if handle is None: + raise MissingHandle + self.handle= handle + + + def set_logstream(self, logstream): + """ + Set log stream for this Zookeeper. The log stream is a file-like + object, or None to suppress all logging. + """ + + self.logstream = logstream + + + def log(self, msg): + """ + Send some text to this Zookeeper's log stream, if one is set. + """ + + if self.logstream is not None: + self.logstream.write(msg) + self.logstream.write("\n") + + + @property + def resource_ca(self): + """ + Get ResourceHolderCA object associated with current handle. + """ + + assert self.handle is not None + try: + return rpki.irdb.ResourceHolderCA.objects.get(handle = self.handle) + except rpki.irdb.ResourceHolderCA.DoesNotExist: + return None + + + @property + def server_ca(self): + """ + Get ServerCA object. + """ + + try: + return rpki.irdb.ServerCA.objects.get() + except rpki.irdb.ServerCA.DoesNotExist: + return None + + + @django.db.transaction.commit_on_success + def initialize(self): + """ + Initialize an RPKI installation. Reads the configuration file, + creates the BPKI and EntityDB directories, generates the initial + BPKI certificates, and creates an XML file describing the + resource-holding aspect of this RPKI installation. + """ + + resource_ca, created = rpki.irdb.ResourceHolderCA.objects.get_or_certify(handle = self.handle) + + if self.run_rpkid or self.run_pubd: + server_ca, created = rpki.irdb.ServerCA.objects.get_or_certify() + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irbe") + + if self.run_rpkid: + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "rpkid") + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irdbd") + + if self.run_pubd: + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "pubd") + + return self.generate_identity() + + + def generate_identity(self): + """ + Generate identity XML. Broken out of .initialize() because it's + easier for the GUI this way. + """ + + e = Element("identity", handle = self.handle) + B64Element(e, "bpki_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = 'This is the "identity" file you will need to send to your parent') + + + @django.db.transaction.commit_on_success + def delete_self(self): + """ + Delete the ResourceHolderCA object corresponding to the current handle. + This corresponds to deleting an rpkid <self/> object. + + This code assumes the normal Django cascade-on-delete behavior, + that is, we assume that deleting the ResourceHolderCA object + deletes all the subordinate objects that refer to it via foreign + key relationships. + """ + + resource_ca = self.resource_ca + if resource_ca is not None: + resource_ca.delete() + else: + self.log("No such ResourceHolderCA \"%s\"" % self.handle) + + + @django.db.transaction.commit_on_success + def configure_rootd(self): + + assert self.run_rpkid and self.run_pubd and self.run_rootd + + rpki.irdb.Rootd.objects.get_or_certify( + issuer = self.resource_ca, + service_uri = "http://localhost:%s/" % self.cfg.get("rootd_server_port", section = myrpki_section)) + + return self.generate_rootd_repository_offer() + + + def generate_rootd_repository_offer(self): + """ + Generate repository offer for rootd. Split out of + configure_rootd() because that's easier for the GUI. + """ + + # The following assumes we'll set up the respository manually. + # Not sure this is a reasonable assumption, particularly if we + # ever fix rootd to use the publication protocol. + + try: + self.resource_ca.repositories.get(handle = self.handle) + return None + + except rpki.irdb.Repository.DoesNotExist: + e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle) + B64Element(e, "bpki_client_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository') + + + def write_bpki_files(self): + """ + Write out BPKI certificate, key, and CRL files for daemons that + need them. + """ + + writer = PEM_writer(self.logstream) + + if self.run_rpkid: + rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid") + writer(self.cfg.get("bpki-ta", section = rpkid_section), self.server_ca.certificate) + writer(self.cfg.get("rpkid-key", section = rpkid_section), rpkid.private_key) + writer(self.cfg.get("rpkid-cert", section = rpkid_section), rpkid.certificate) + writer(self.cfg.get("irdb-cert", section = rpkid_section), + self.server_ca.ee_certificates.get(purpose = "irdbd").certificate) + writer(self.cfg.get("irbe-cert", section = rpkid_section), + self.server_ca.ee_certificates.get(purpose = "irbe").certificate) + + if self.run_pubd: + pubd = self.server_ca.ee_certificates.get(purpose = "pubd") + writer(self.cfg.get("bpki-ta", section = pubd_section), self.server_ca.certificate) + writer(self.cfg.get("pubd-key", section = pubd_section), pubd.private_key) + writer(self.cfg.get("pubd-cert", section = pubd_section), pubd.certificate) + writer(self.cfg.get("irbe-cert", section = pubd_section), + self.server_ca.ee_certificates.get(purpose = "irbe").certificate) + + if self.run_rootd: + rootd = rpki.irdb.ResourceHolderCA.objects.get(handle = self.cfg.get("handle", section = myrpki_section)).rootd + writer(self.cfg.get("bpki-ta", section = rootd_section), self.server_ca.certificate) + writer(self.cfg.get("rootd-bpki-crl", section = rootd_section), self.server_ca.latest_crl) + writer(self.cfg.get("rootd-bpki-key", section = rootd_section), rootd.private_key) + writer(self.cfg.get("rootd-bpki-cert", section = rootd_section), rootd.certificate) + writer(self.cfg.get("child-bpki-cert", section = rootd_section), rootd.issuer.certificate) + + + @django.db.transaction.commit_on_success + def update_bpki(self): + """ + Update BPKI certificates. Assumes an existing RPKI installation. + + Basic plan here is to reissue all BPKI certificates we can, right + now. In the long run we might want to be more clever about only + touching ones that need maintenance, but this will do for a start. + + We also reissue CRLs for all CAs. + + Most likely this should be run under cron. + """ + + for model in (rpki.irdb.ServerCA, + rpki.irdb.ResourceHolderCA, + rpki.irdb.ServerEE, + rpki.irdb.Referral, + rpki.irdb.Rootd, + rpki.irdb.HostedCA, + rpki.irdb.BSC, + rpki.irdb.Child, + rpki.irdb.Parent, + rpki.irdb.Client, + rpki.irdb.Repository): + for obj in model.objects.all(): + self.log("Regenerating certificate %s" % obj.certificate.getSubject()) + obj.avow() + obj.save() + + self.log("Regenerating Server CRL") + self.server_ca.generate_crl() + self.server_ca.save() + + for ca in rpki.irdb.ResourceHolderCA.objects.all(): + self.log("Regenerating CRL for %s" % ca.handle) + ca.generate_crl() + ca.save() + + + @django.db.transaction.commit_on_success + def configure_child(self, filename, child_handle = None): + """ + Configure a new child of this RPKI entity, given the child's XML + identity file as an input. Extracts the child's data from the + XML, cross-certifies the child's resource-holding BPKI + certificate, and generates an XML file describing the relationship + between the child and this parent, including this parent's BPKI + data and up-down protocol service URI. + """ + + c = etree_read(filename) + + if child_handle is None: + child_handle = c.get("handle") + + valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365) + + self.log("Child calls itself %r, we call it %r" % (c.get("handle"), child_handle)) + + child, created = rpki.irdb.Child.objects.get_or_certify( + issuer = self.resource_ca, + handle = child_handle, + ta = rpki.x509.X509(Base64 = c.findtext("bpki_ta")), + valid_until = valid_until) + + return self.generate_parental_response(child), child_handle + + + @django.db.transaction.commit_on_success + def generate_parental_response(self, child): + """ + Generate parental response XML. Broken out of .configure_child() + for GUI. + """ + + service_uri = "http://%s:%s/up-down/%s/%s" % ( + self.cfg.get("rpkid_server_host", section = myrpki_section), + self.cfg.get("rpkid_server_port", section = myrpki_section), + self.handle, child.handle) + + e = Element("parent", parent_handle = self.handle, child_handle = child.handle, + service_uri = service_uri, valid_until = str(child.valid_until)) + B64Element(e, "bpki_resource_ta", self.resource_ca.certificate) + B64Element(e, "bpki_child_ta", child.ta) + + try: + if self.default_repository: + repo = self.resource_ca.repositories.get(handle = self.default_repository) + else: + repo = self.resource_ca.repositories.get() + except rpki.irdb.Repository.DoesNotExist: + repo = None + + if repo is None: + self.log("Couldn't find any usable repositories, not giving referral") + + elif repo.handle == self.handle: + SubElement(e, "repository", type = "offer") + + else: + proposed_sia_base = repo.sia_base + child.handle + "/" + referral_cert, created = rpki.irdb.Referral.objects.get_or_certify(issuer = self.resource_ca) + auth = rpki.x509.SignedReferral() + auth.set_content(B64Element(None, myrpki_namespaceQName + "referral", child.ta, + version = myrpki_version, + authorized_sia_base = proposed_sia_base)) + auth.schema_check() + auth.sign(referral_cert.private_key, referral_cert.certificate, self.resource_ca.latest_crl) + + r = SubElement(e, "repository", type = "referral") + B64Element(r, "authorization", auth, referrer = repo.client_handle) + SubElement(r, "contact_info") + + return etree_wrapper(e, msg = "Send this file back to the child you just configured") + + + @django.db.transaction.commit_on_success + def delete_child(self, child_handle): + """ + Delete a child of this RPKI entity. + """ + + assert child_handle is not None + try: + self.resource_ca.children.get(handle = child_handle).delete() + except rpki.irdb.Child.DoesNotExist: + self.log("No such child \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_parent(self, filename, parent_handle = None): + """ + Configure a new parent of this RPKI entity, given the output of + the parent's configure_child command as input. Reads the parent's + response XML, extracts the parent's BPKI and service URI + information, cross-certifies the parent's BPKI data into this + entity's BPKI, and checks for offers or referrals of publication + service. If a publication offer or referral is present, we + generate a request-for-service message to that repository, in case + the user wants to avail herself of the referral or offer. + """ + + p = etree_read(filename) + + if parent_handle is None: + parent_handle = p.get("parent_handle") + + r = p.find("repository") + + repository_type = "none" + referrer = None + referral_authorization = None + + if r is not None: + repository_type = r.get("type") + + if repository_type == "referral": + a = r.find("authorization") + referrer = a.get("referrer") + referral_authorization = rpki.x509.SignedReferral(Base64 = a.text) + + self.log("Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle)) + self.log("Parent calls us %r" % p.get("child_handle")) + + parent, created = rpki.irdb.Parent.objects.get_or_certify( + issuer = self.resource_ca, + handle = parent_handle, + child_handle = p.get("child_handle"), + parent_handle = p.get("parent_handle"), + service_uri = p.get("service_uri"), + ta = rpki.x509.X509(Base64 = p.findtext("bpki_resource_ta")), + repository_type = repository_type, + referrer = referrer, + referral_authorization = referral_authorization) + + return self.generate_repository_request(parent), parent_handle + + + def generate_repository_request(self, parent): + """ + Generate repository request for a given parent. + """ + + e = Element("repository", handle = self.handle, + parent_handle = parent.handle, type = parent.repository_type) + if parent.repository_type == "referral": + B64Element(e, "authorization", parent.referral_authorization, referrer = parent.referrer) + SubElement(e, "contact_info") + B64Element(e, "bpki_client_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = "This is the file to send to the repository operator") + + + @django.db.transaction.commit_on_success + def delete_parent(self, parent_handle): + """ + Delete a parent of this RPKI entity. + """ + + assert parent_handle is not None + try: + self.resource_ca.parents.get(handle = parent_handle).delete() + except rpki.irdb.Parent.DoesNotExist: + self.log("No such parent \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_publication_client(self, filename, sia_base = None): + """ + Configure publication server to know about a new client, given the + client's request-for-service message as input. Reads the client's + request for service, cross-certifies the client's BPKI data, and + generates a response message containing the repository's BPKI data + and service URI. + """ + + client = etree_read(filename) + + client_ta = rpki.x509.X509(Base64 = client.findtext("bpki_client_ta")) + + if sia_base is None and client.get("handle") == self.handle and self.resource_ca.certificate == client_ta: + self.log("This looks like self-hosted publication") + sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, self.handle) + + if sia_base is None and client.get("type") == "referral": + self.log("This looks like a referral, checking") + try: + auth = client.find("authorization") + referrer = self.server_ca.clients.get(handle = auth.get("referrer")) + referral_cms = rpki.x509.SignedReferral(Base64 = auth.text) + referral_xml = referral_cms.unwrap(ta = (referrer.certificate, self.server_ca.certificate)) + if rpki.x509.X509(Base64 = referral_xml.text) != client_ta: + raise BadXMLMessage, "Referral trust anchor does not match" + sia_base = referral_xml.get("authorized_sia_base") + except rpki.irdb.Client.DoesNotExist: + self.log("We have no record of the client (%s) alleged to have made this referral" % auth.get("referrer")) + + if sia_base is None and client.get("type") == "offer" and client.get("parent_handle") == self.handle: + self.log("This looks like an offer, client claims to be our child, checking") + try: + child = self.resource_ca.children.get(ta = client_ta) + except rpki.irdb.Child.DoesNotExist: + self.log("Can't find a child matching this client") + else: + sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module, + self.handle, client.get("handle")) + + # If we still haven't figured out what to do with this client, it + # gets a top-level tree of its own, no attempt at nesting. + + if sia_base is None: + self.log("Don't know where to nest this client, defaulting to top-level") + sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle")) + + if not sia_base.startswith("rsync://"): + raise BadXMLMessage, "Malformed sia_base parameter %r, should start with 'rsync://'" % sia_base + + client_handle = "/".join(sia_base.rstrip("/").split("/")[4:]) + + parent_handle = client.get("parent_handle") + + self.log("Client calls itself %r, we call it %r" % (client.get("handle"), client_handle)) + self.log("Client says its parent handle is %r" % parent_handle) + + client, created = rpki.irdb.Client.objects.get_or_certify( + issuer = self.server_ca, + handle = client_handle, + parent_handle = parent_handle, + ta = client_ta, + sia_base = sia_base) + + return self.generate_repository_response(client), client_handle + + + def generate_repository_response(self, client): + """ + Generate repository response XML to a given client. + """ + + service_uri = "http://%s:%s/client/%s" % ( + self.cfg.get("pubd_server_host", section = myrpki_section), + self.cfg.get("pubd_server_port", section = myrpki_section), + client.handle) + + e = Element("repository", type = "confirmed", + client_handle = client.handle, + parent_handle = client.parent_handle, + sia_base = client.sia_base, + service_uri = service_uri) + + B64Element(e, "bpki_server_ta", self.server_ca.certificate) + B64Element(e, "bpki_client_ta", client.ta) + SubElement(e, "contact_info").text = self.pubd_contact_info + return etree_wrapper(e, msg = "Send this file back to the publication client you just configured") + + + @django.db.transaction.commit_on_success + def delete_publication_client(self, client_handle): + """ + Delete a publication client of this RPKI entity. + """ + + assert client_handle is not None + try: + self.server_ca.clients.get(handle = client_handle).delete() + except rpki.irdb.Client.DoesNotExist: + self.log("No such client \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_repository(self, filename, parent_handle = None): + """ + Configure a publication repository for this RPKI entity, given the + repository's response to our request-for-service message as input. + Reads the repository's response, extracts and cross-certifies the + BPKI data and service URI, and links the repository data with the + corresponding parent data in our local database. + """ + + r = etree_read(filename) + + if parent_handle is None: + parent_handle = r.get("parent_handle") + + self.log("Repository calls us %r" % (r.get("client_handle"))) + self.log("Repository response associated with parent_handle %r" % parent_handle) + + try: + if parent_handle == self.handle: + turtle = self.resource_ca.rootd + else: + turtle = self.resource_ca.parents.get(handle = parent_handle) + + except (rpki.irdb.Parent.DoesNotExist, rpki.irdb.Rootd.DoesNotExist): + self.log("Could not find parent %r in our database" % parent_handle) + + else: + rpki.irdb.Repository.objects.get_or_certify( + issuer = self.resource_ca, + handle = parent_handle, + client_handle = r.get("client_handle"), + service_uri = r.get("service_uri"), + sia_base = r.get("sia_base"), + ta = rpki.x509.X509(Base64 = r.findtext("bpki_server_ta")), + turtle = turtle) + + + @django.db.transaction.commit_on_success + def delete_repository(self, repository_handle): + """ + Delete a repository of this RPKI entity. + """ + + assert repository_handle is not None + try: + self.resource_ca.repositories.get(handle = arg).delete() + except rpki.irdb.Repository.DoesNotExist: + self.log("No such repository \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def renew_children(self, child_handle, valid_until = None): + """ + Update validity period for one child entity or, if child_handle is + None, for all child entities. + """ + + if child_handle is None: + children = self.resource_ca.children.all() + else: + children = self.resource_ca.children.filter(handle = child_handle) + + if valid_until is None: + valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365) + else: + valid_until = rpki.sundial.fromXMLtime(valid_until) + if valid_until < rpki.sundial.now(): + raise PastExpiration, "Specified new expiration time %s has passed" % valid_until + + self.log("New validity date %s" % valid_until) + + for child in children: + child.valid_until = valid_until + child.save() + + + @django.db.transaction.commit_on_success + def load_prefixes(self, filename): + """ + Whack IRDB to match prefixes.csv. + """ + + grouped4 = {} + grouped6 = {} + + for handle, prefix in csv_reader(filename, columns = 2): + grouped = grouped6 if ":" in prefix else grouped4 + if handle not in grouped: + grouped[handle] = [] + grouped[handle].append(prefix) + + primary_keys = [] + + for version, grouped, rset in ((4, grouped4, rpki.resource_set.resource_set_ipv4), + (6, grouped6, rpki.resource_set.resource_set_ipv6)): + for handle, prefixes in grouped.iteritems(): + child = self.resource_ca.children.get(handle = handle) + for prefix in rset(",".join(prefixes)): + obj, created = rpki.irdb.ChildNet.objects.get_or_create( + child = child, + start_ip = str(prefix.min), + end_ip = str(prefix.max), + version = version) + primary_keys.append(obj.pk) + + q = rpki.irdb.ChildNet.objects + q = q.filter(child__issuer__exact = self.resource_ca) + q = q.exclude(pk__in = primary_keys) + q.delete() + + + @django.db.transaction.commit_on_success + def load_asns(self, filename): + """ + Whack IRDB to match asns.csv. + """ + + grouped = {} + + for handle, asn in csv_reader(filename, columns = 2): + if handle not in grouped: + grouped[handle] = [] + grouped[handle].append(asn) + + primary_keys = [] + + for handle, asns in grouped.iteritems(): + child = self.resource_ca.children.get(handle = handle) + for asn in rpki.resource_set.resource_set_as(",".join(asns)): + obj, created = rpki.irdb.ChildASN.objects.get_or_create( + child = child, + start_as = str(asn.min), + end_as = str(asn.max)) + primary_keys.append(obj.pk) + + q = rpki.irdb.ChildASN.objects + q = q.filter(child__issuer__exact = self.resource_ca) + q = q.exclude(pk__in = primary_keys) + q.delete() + + + @django.db.transaction.commit_on_success + def load_roa_requests(self, filename): + """ + Whack IRDB to match roa.csv. + """ + + grouped = {} + + # format: p/n-m asn group + for pnm, asn, group in csv_reader(filename, columns = 3): + key = (asn, group) + if key not in grouped: + grouped[key] = [] + grouped[key].append(pnm) + + # Deleting and recreating all the ROA requests is inefficient, + # but rpkid's current representation of ROA requests is wrong + # (see #32), so it's not worth a lot of effort here as we're + # just going to have to rewrite this soon anyway. + + self.resource_ca.roa_requests.all().delete() + + for key, pnms in grouped.iteritems(): + asn, group = key + + roa_request = self.resource_ca.roa_requests.create(asn = asn) + + for pnm in pnms: + if ":" in pnm: + p = rpki.resource_set.roa_prefix_ipv6.parse_str(pnm) + v = 6 + else: + p = rpki.resource_set.roa_prefix_ipv4.parse_str(pnm) + v = 4 + roa_request.prefixes.create( + version = v, + prefix = str(p.prefix), + prefixlen = int(p.prefixlen), + max_prefixlen = int(p.max_prefixlen)) + + + def call_rpkid(self, *pdus): + """ + Issue a call to rpkid, return result. + + Implementation is a little silly, constructs a wrapper object, + invokes it once, then throws it away. Hard to do better without + rewriting a bit of the HTTP code, as we want to be sure we're + using the current BPKI certificate and key objects. + """ + + url = "http://%s:%s/left-right" % ( + self.cfg.get("rpkid_server_host", section = myrpki_section), + self.cfg.get("rpkid_server_port", section = myrpki_section)) + + rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid") + irbe = self.server_ca.ee_certificates.get(purpose = "irbe") + + call_rpkid = rpki.async.sync_wrapper(rpki.http.caller( + proto = rpki.left_right, + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = self.server_ca.certificate, + server_cert = rpkid.certificate, + url = url, + debug = self.show_xml)) + + return call_rpkid(*pdus) + + + def run_rpkid_now(self): + """ + Poke rpkid to immediately run the cron job for the current handle. + + This method is used by the gui when a user has changed something in the + IRDB (ghostbuster, roa) which does not require a full synchronize() call, + to force the object to be immediately issued. + """ + + self.call_rpkid(rpki.left_right.self_elt.make_pdu( + action = "set", self_handle = self.handle, run_now = "yes")) + + + def call_pubd(self, *pdus): + """ + Issue a call to pubd, return result. + + Implementation is a little silly, constructs a wrapper object, + invokes it once, then throws it away. Hard to do better without + rewriting a bit of the HTTP code, as we want to be sure we're + using the current BPKI certificate and key objects. + """ + + url = "http://%s:%s/control" % ( + self.cfg.get("pubd_server_host", section = myrpki_section), + self.cfg.get("pubd_server_port", section = myrpki_section)) + + pubd = self.server_ca.ee_certificates.get(purpose = "pubd") + irbe = self.server_ca.ee_certificates.get(purpose = "irbe") + + call_pubd = rpki.async.sync_wrapper(rpki.http.caller( + proto = rpki.publication, + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = self.server_ca.certificate, + server_cert = pubd.certificate, + url = url, + debug = self.show_xml)) + + return call_pubd(*pdus) + + + def check_error_report(self, pdus): + """ + Check a response from rpkid or pubd for error_report PDUs, log and + throw exceptions as needed. + """ + + if any(isinstance(pdu, (rpki.left_right.report_error_elt, rpki.publication.report_error_elt)) for pdu in pdus): + for pdu in pdus: + if isinstance(pdu, rpki.left_right.report_error_elt): + self.log("rpkid reported failure: %s" % pdu.error_code) + elif isinstance(pdu, rpki.publication.report_error_elt): + self.log("pubd reported failure: %s" % pdu.error_code) + else: + continue + if pdu.error_text: + self.log(pdu.error_text) + raise CouldntTalkToDaemon + + + @django.db.transaction.commit_on_success + def synchronize(self, *handles_to_poke): + """ + Configure RPKI daemons with the data built up by the other + commands in this program. Most commands which modify the IRDB + should call this when they're done. + + Any arguments given are handles to be sent to rpkid at the end of + the synchronization run with a <self run_now="yes"/> operation. + """ + + # We can use a single BSC for everything -- except BSC key + # rollovers. Drive off that bridge when we get to it. + + bsc_handle = "bsc" + + # Default values for CRL parameters are low, for testing. Not + # quite as low as they once were, too much expired CRL whining. + + self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60, + section = myrpki_section) + self_regen_margin = self.cfg.getint("self_regen_margin", self_crl_interval / 4, + section = myrpki_section) + + # Make sure that pubd's BPKI CRL is up to date. + + if self.run_pubd: + self.call_pubd(rpki.publication.config_elt.make_pdu( + action = "set", + bpki_crl = self.server_ca.latest_crl)) + + for ca in rpki.irdb.ResourceHolderCA.objects.all(): + + # See what rpkid and pubd already have on file for this entity. + + if self.run_pubd: + pubd_reply = self.call_pubd(rpki.publication.client_elt.make_pdu(action = "list")) + client_pdus = dict((x.client_handle, x) for x in pubd_reply if isinstance(x, rpki.publication.client_elt)) + + rpkid_reply = self.call_rpkid( + rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = ca.handle), + rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = ca.handle), + rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = ca.handle), + rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = ca.handle), + rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = ca.handle)) + + self_pdu = rpkid_reply[0] + bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt)) + repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt)) + parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt)) + child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt)) + + pubd_query = [] + rpkid_query = [] + + self_cert, created = rpki.irdb.HostedCA.objects.get_or_certify( + issuer = self.server_ca, + hosted = ca) + + # There should be exactly one <self/> object per hosted entity, by definition + + if (isinstance(self_pdu, rpki.left_right.report_error_elt) or + self_pdu.crl_interval != self_crl_interval or + self_pdu.regen_margin != self_regen_margin or + self_pdu.bpki_cert != self_cert.certificate): + rpkid_query.append(rpki.left_right.self_elt.make_pdu( + action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set", + tag = "self", + self_handle = ca.handle, + bpki_cert = ca.certificate, + crl_interval = self_crl_interval, + regen_margin = self_regen_margin)) + + # In general we only need one <bsc/> per <self/>. BSC objects + # are a little unusual in that the keypair and PKCS #10 + # subelement is generated by rpkid, so complete setup requires + # two round trips. + + bsc_pdu = bsc_pdus.pop(bsc_handle, None) + + if bsc_pdu is None: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "create", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + generate_keypair = "yes")) + + elif bsc_pdu.pkcs10_request is None: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "set", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + generate_keypair = "yes")) + + rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu( + action = "destroy", self_handle = ca.handle, bsc_handle = b) for b in bsc_pdus) + + # If we've already got actions queued up, run them now, so we + # can finish setting up the BSC before anything tries to use it. + + if rpkid_query: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(action = "list", tag = "bsc", self_handle = ca.handle)) + rpkid_reply = self.call_rpkid(*rpkid_query) + bsc_pdus = dict((x.bsc_handle, x) + for x in rpkid_reply + if isinstance(x, rpki.left_right.bsc_elt) and x.action == "list") + bsc_pdu = bsc_pdus.pop(bsc_handle, None) + self.check_error_report(rpkid_reply) + + rpkid_query = [] + + assert bsc_pdu.pkcs10_request is not None + + bsc, created = rpki.irdb.BSC.objects.get_or_certify( + issuer = ca, + handle = bsc_handle, + pkcs10 = bsc_pdu.pkcs10_request) + + if bsc_pdu.signing_cert != bsc.certificate or bsc_pdu.signing_cert_crl != ca.latest_crl: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "set", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + signing_cert = bsc.certificate, + signing_cert_crl = ca.latest_crl)) + + # At present we need one <repository/> per <parent/>, not because + # rpkid requires that, but because pubd does. pubd probably should + # be fixed to support a single client allowed to update multiple + # trees, but for the moment the easiest way forward is just to + # enforce a 1:1 mapping between <parent/> and <repository/> objects + + for repository in ca.repositories.all(): + + repository_pdu = repository_pdus.pop(repository.handle, None) + + if (repository_pdu is None or + repository_pdu.bsc_handle != bsc_handle or + repository_pdu.peer_contact_uri != repository.service_uri or + repository_pdu.bpki_cert != repository.certificate): + rpkid_query.append(rpki.left_right.repository_elt.make_pdu( + action = "create" if repository_pdu is None else "set", + tag = repository.handle, + self_handle = ca.handle, + repository_handle = repository.handle, + bsc_handle = bsc_handle, + peer_contact_uri = repository.service_uri, + bpki_cert = repository.certificate)) + + rpkid_query.extend(rpki.left_right.repository_elt.make_pdu( + action = "destroy", self_handle = ca.handle, repository_handle = r) for r in repository_pdus) + + # <parent/> setup code currently assumes 1:1 mapping between + # <repository/> and <parent/>, and further assumes that the handles + # for an associated pair are the identical (that is: + # parent.repository_handle == parent.parent_handle). + + for parent in ca.parents.all(): + + parent_pdu = parent_pdus.pop(parent.handle, None) + + if (parent_pdu is None or + parent_pdu.bsc_handle != bsc_handle or + parent_pdu.repository_handle != parent.handle or + parent_pdu.peer_contact_uri != parent.service_uri or + parent_pdu.sia_base != parent.repository.sia_base or + parent_pdu.sender_name != parent.child_handle or + parent_pdu.recipient_name != parent.parent_handle or + parent_pdu.bpki_cms_cert != parent.certificate): + rpkid_query.append(rpki.left_right.parent_elt.make_pdu( + action = "create" if parent_pdu is None else "set", + tag = parent.handle, + self_handle = ca.handle, + parent_handle = parent.handle, + bsc_handle = bsc_handle, + repository_handle = parent.handle, + peer_contact_uri = parent.service_uri, + sia_base = parent.repository.sia_base, + sender_name = parent.child_handle, + recipient_name = parent.parent_handle, + bpki_cms_cert = parent.certificate)) + + try: + + parent_pdu = parent_pdus.pop(ca.handle, None) + + if (parent_pdu is None or + parent_pdu.bsc_handle != bsc_handle or + parent_pdu.repository_handle != ca.handle or + parent_pdu.peer_contact_uri != ca.rootd.service_uri or + parent_pdu.sia_base != ca.rootd.repository.sia_base or + parent_pdu.sender_name != ca.handle or + parent_pdu.recipient_name != ca.handle or + parent_pdu.bpki_cms_cert != ca.rootd.certificate): + rpkid_query.append(rpki.left_right.parent_elt.make_pdu( + action = "create" if parent_pdu is None else "set", + tag = ca.handle, + self_handle = ca.handle, + parent_handle = ca.handle, + bsc_handle = bsc_handle, + repository_handle = ca.handle, + peer_contact_uri = ca.rootd.service_uri, + sia_base = ca.rootd.repository.sia_base, + sender_name = ca.handle, + recipient_name = ca.handle, + bpki_cms_cert = ca.rootd.certificate)) + + except rpki.irdb.Rootd.DoesNotExist: + pass + + rpkid_query.extend(rpki.left_right.parent_elt.make_pdu( + action = "destroy", self_handle = ca.handle, parent_handle = p) for p in parent_pdus) + + # Children are simpler than parents, because they call us, so no URL + # to construct and figuring out what certificate to use is their + # problem, not ours. + + for child in ca.children.all(): + + child_pdu = child_pdus.pop(child.handle, None) + + if (child_pdu is None or + child_pdu.bsc_handle != bsc_handle or + child_pdu.bpki_cert != child.certificate): + rpkid_query.append(rpki.left_right.child_elt.make_pdu( + action = "create" if child_pdu is None else "set", + tag = child.handle, + self_handle = ca.handle, + child_handle = child.handle, + bsc_handle = bsc_handle, + bpki_cert = child.certificate)) + + rpkid_query.extend(rpki.left_right.child_elt.make_pdu( + action = "destroy", self_handle = ca.handle, child_handle = c) for c in child_pdus) + + # Publication setup. + + # Um, why are we doing this per resource holder? + + if self.run_pubd: + + for client in self.server_ca.clients.all(): + + client_pdu = client_pdus.pop(client.handle, None) + + if (client_pdu is None or + client_pdu.base_uri != client.sia_base or + client_pdu.bpki_cert != client.certificate): + pubd_query.append(rpki.publication.client_elt.make_pdu( + action = "create" if client_pdu is None else "set", + client_handle = client.handle, + bpki_cert = client.certificate, + base_uri = client.sia_base)) + + pubd_query.extend(rpki.publication.client_elt.make_pdu( + action = "destroy", client_handle = p) for p in client_pdus) + + # Poke rpkid to run immediately for any requested handles. + + rpkid_query.extend(rpki.left_right.self_elt.make_pdu( + action = "set", self_handle = h, run_now = "yes") for h in handles_to_poke) + + # If we changed anything, ship updates off to daemons + + if rpkid_query: + rpkid_reply = self.call_rpkid(*rpkid_query) + bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt)) + if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request: + bsc_req = bsc_pdus[bsc_handle].pkcs10_request + self.check_error_report(rpkid_reply) + + if pubd_query: + assert self.run_pubd + pubd_reply = self.call_pubd(*pubd_query) + self.check_error_report(pubd_reply) + + # Finally, clean up any <self/> objects rpkid might be holding + # that don't match ResourceCA object. + + rpkid_reply = self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "list")) + self.check_error_report(rpkid_reply) + + self_handles = set(s.self_handle for s in rpkid_reply) + ca_handles = set(ca.handle for ca in rpki.irdb.ResourceHolderCA.objects.all()) + assert ca_handles <= self_handles + + rpkid_query = [rpki.left_right.self_elt.make_pdu(action = "destroy", self_handle = handle) + for handle in (self_handles - ca_handles)] + rpkid_reply = self.call_rpkid(*rpkid_query) + self.check_error_report(rpkid_reply) diff --git a/rpkid/rpki/irdbd.py b/rpkid/rpki/irdbd.py index c2e01287..28e26b07 100644 --- a/rpkid/rpki/irdbd.py +++ b/rpkid/rpki/irdbd.py @@ -5,7 +5,7 @@ Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ] $Id$ -Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -38,160 +38,90 @@ import sys, os, time, getopt, urlparse, warnings import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509 -from rpki.mysql_import import MySQLdb - class main(object): - def handle_list_resources(self, q_pdu, r_msg): - + child = rpki.irdb.Child.objects.get(issuer__handle__exact = q_pdu.self_handle, + handle = q_pdu.child_handle) + resources = child.resource_bag r_pdu = rpki.left_right.list_resources_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle r_pdu.child_handle = q_pdu.child_handle - - self.cur.execute( - "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s", - (q_pdu.self_handle, q_pdu.child_handle)) - - if self.cur.rowcount != 1: - raise rpki.exceptions.NotInDatabase, \ - "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \ - % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle) - - registrant_id, valid_until = self.cur.fetchone() - - r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") - - r_pdu.asn = rpki.resource_set.resource_set_as.from_sql( - self.cur, - "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s", - (registrant_id,)) - - r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql( - self.cur, - "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4", - (registrant_id,)) - - r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql( - self.cur, - "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6", - (registrant_id,)) - + r_pdu.valid_until = child.valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") + r_pdu.asn = resources.asn + r_pdu.ipv4 = resources.v4 + r_pdu.ipv6 = resources.v6 r_msg.append(r_pdu) - def handle_list_roa_requests(self, q_pdu, r_msg): - - self.cur.execute( - "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s", - (q_pdu.self_handle,)) - - for roa_request_id, asn in self.cur.fetchall(): - + for request in rpki.irdb.ROARequest.objects.filter(issuer__handle__exact = q_pdu.self_handle): + prefix_bag = request.roa_prefix_bag r_pdu = rpki.left_right.list_roa_requests_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle - r_pdu.asn = asn - - r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql( - self.cur, - "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4", - (roa_request_id,)) - - r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql( - self.cur, - "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6", - (roa_request_id,)) - + r_pdu.asn = request.asn + r_pdu.ipv4 = prefix_bag.v4 + r_pdu.ipv6 = prefix_bag.v6 r_msg.append(r_pdu) - def handle_list_ghostbuster_requests(self, q_pdu, r_msg): - - self.cur.execute( - "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s", - (q_pdu.self_handle, q_pdu.parent_handle)) - - vcards = [result[0] for result in self.cur.fetchall()] - - if not vcards: - - self.cur.execute( - "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL", - (q_pdu.self_handle,)) - - vcards = [result[0] for result in self.cur.fetchall()] - - for vcard in vcards: + ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter( + issuer__handle__exact = q_pdu.self_handle, + parent__handle__exact = q_pdu.parent_handle) + if ghostbusters.count() == 0: + ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter( + issuer__handle__exact = q_pdu.self_handle, + parent = None) + for ghostbuster in ghostbusters: r_pdu = rpki.left_right.list_ghostbuster_requests_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle r_pdu.parent_handle = q_pdu.parent_handle - r_pdu.vcard = vcard + r_pdu.vcard = ghostbuster.vcard r_msg.append(r_pdu) - - handle_dispatch = { - rpki.left_right.list_resources_elt : handle_list_resources, - rpki.left_right.list_roa_requests_elt : handle_list_roa_requests, - rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests} - - def handler(self, query, path, cb): try: - - self.db.ping(True) - + q_pdu = None r_msg = rpki.left_right.msg.reply() - + self.start_new_transaction() + serverCA = rpki.irdb.ServerCA.objects.get() + rpkid = serverCA.ee_certificates.get(purpose = "rpkid") try: - - q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert)) - + q_msg = rpki.left_right.cms_msg(DER = query).unwrap((serverCA.certificate, rpkid.certificate)) if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query(): - raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg - + raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_msg) for q_pdu in q_msg: - - try: - - try: - h = self.handle_dispatch[type(q_pdu)] - except KeyError: - raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu - else: - h(self, q_pdu, r_msg) - - except (rpki.async.ExitNow, SystemExit): - raise - - except Exception, e: - rpki.log.traceback() - r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) - + self.dispatch(q_pdu, r_msg) except (rpki.async.ExitNow, SystemExit): raise - except Exception, e: rpki.log.traceback() - r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) - - cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert)) - + if q_pdu is None: + r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) + else: + r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) + irdbd = serverCA.ee_certificates.get(purpose = "irdbd") + cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, irdbd.private_key, irdbd.certificate)) except (rpki.async.ExitNow, SystemExit): raise - except Exception, e: rpki.log.traceback() - - # We only get here in cases where we couldn't or wouldn't generate - # <report_error/>, so just return HTTP failure. - cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) + def dispatch(self, q_pdu, r_msg): + try: + handler = self.dispatch_vector[type(q_pdu)] + except KeyError: + raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_pdu) + else: + handler(q_pdu, r_msg) + + def __init__(self, **kwargs): - def __init__(self): + global rpki + from django.conf import settings os.environ["TZ"] = "UTC" time.tzset() @@ -208,31 +138,69 @@ class main(object): elif o in ("-d", "--debug"): rpki.log.use_syslog = False if argv: - raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv + raise rpki.exceptions.CommandParseFailure("Unexpected arguments %s" % argv) rpki.log.init("irdbd") - self.cfg = rpki.config.parser(cfg_file, "irdbd") + cfg = rpki.config.parser(cfg_file, "irdbd") - startup_msg = self.cfg.get("startup-message", "") + startup_msg = cfg.get("startup-message", "") if startup_msg: rpki.log.info(startup_msg) - self.cfg.set_global_flags() - - self.db = MySQLdb.connect(user = self.cfg.get("sql-username"), - db = self.cfg.get("sql-database"), - passwd = self.cfg.get("sql-password")) - - self.cur = self.db.cursor() - self.db.autocommit(True) - - self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta")) - self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert")) - self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert")) - self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key")) - - u = urlparse.urlparse(self.cfg.get("http-url")) + cfg.set_global_flags() + + # Do -not- turn on DEBUG here except for short-lived tests, + # otherwise irdbd will eventually run out of memory and crash. + # + # If you must enable debugging, use django.db.reset_queries() to + # clear the query list manually, but it's probably better just to + # run with debugging disabled, since that's the expectation for + # production code. + # + # https://docs.djangoproject.com/en/dev/faq/models/#why-is-django-leaking-memory + + settings.configure( + DATABASES = { + "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : cfg.get("sql-database"), + "USER" : cfg.get("sql-username"), + "PASSWORD" : cfg.get("sql-password"), + "HOST" : "", + "PORT" : "" }}, + INSTALLED_APPS = ("rpki.irdb",),) + + import rpki.irdb + + # Entirely too much fun with read-only access to transactional databases. + # + # http://stackoverflow.com/questions/3346124/how-do-i-force-django-to-ignore-any-caches-and-reload-data + # http://devblog.resolversystems.com/?p=439 + # http://groups.google.com/group/django-users/browse_thread/thread/e25cec400598c06d + # http://stackoverflow.com/questions/1028671/python-mysqldb-update-query-fails + # http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # It turns out that MySQL is doing us a favor with this weird + # transactional behavior on read, because without it there's a + # race condition if multiple updates are committed to the IRDB + # while we're in the middle of processing a query. Note that + # proper transaction management by the committers doesn't protect + # us, this is a transactional problem on read. So we need to use + # explicit transaction management. Since irdbd is a read-only + # consumer of IRDB data, this means we need to commit an empty + # transaction at the beginning of processing each query, to reset + # the transaction isolation snapshot. + + import django.db.transaction + self.start_new_transaction = django.db.transaction.commit_manually(django.db.transaction.commit) + + self.dispatch_vector = { + rpki.left_right.list_resources_elt : self.handle_list_resources, + rpki.left_right.list_roa_requests_elt : self.handle_list_roa_requests, + rpki.left_right.list_ghostbuster_requests_elt : self.handle_list_ghostbuster_requests } + + u = urlparse.urlparse(cfg.get("http-url")) assert u.scheme in ("", "http") and \ u.username is None and \ @@ -241,6 +209,7 @@ class main(object): u.query == "" and \ u.fragment == "" - rpki.http.server(host = u.hostname or "localhost", - port = u.port or 443, - handlers = ((u.path, self.handler),)) + rpki.http.server( + host = u.hostname or "localhost", + port = u.port or 443, + handlers = ((u.path, self.handler),)) diff --git a/rpkid/rpki/left_right.py b/rpkid/rpki/left_right.py index 7cb18f8c..17d665c9 100644 --- a/rpkid/rpki/left_right.py +++ b/rpkid/rpki/left_right.py @@ -405,7 +405,7 @@ class self_elt(data_elt): def list_failed(e): rpki.log.traceback() - rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s" % (parent, e)) + rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s (%r)" % (parent, e, e)) parent_iterator() rpki.up_down.list_pdu.query(parent, got_list, list_failed) diff --git a/rpkid/rpki/myrpki.py b/rpkid/rpki/myrpki.py index 2fa2f8cb..ec36371c 100644 --- a/rpkid/rpki/myrpki.py +++ b/rpkid/rpki/myrpki.py @@ -793,9 +793,17 @@ class CA(object): Write PEM certificate to file, then cross-certify. """ fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid()) + der = base64.b64decode(b64) + if True: + try: + text = self.run_openssl("x509", "-inform", "DER", "-noout", + "-issuer", "-subject", stdin = der) + except: + text = "" + print "fxcert():", self.dir, filename, text try: self.run_openssl("x509", "-inform", "DER", "-out", fn, - stdin = base64.b64decode(b64)) + stdin = der) return self.xcert(fn, path_restriction) finally: if not filename and os.path.exists(fn): diff --git a/rpkid/rpki/oids.py b/rpkid/rpki/oids.py index 2557d7cf..3fbe214c 100644 --- a/rpkid/rpki/oids.py +++ b/rpkid/rpki/oids.py @@ -67,6 +67,7 @@ oid2name = { (2, 5, 29, 35) : "authorityKeyIdentifier", (2, 5, 29, 37) : "extendedKeyUsage", (2, 5, 4, 3) : "commonName", + (2, 5, 4, 5) : "serialNumber", } ## @var name2oid diff --git a/rpkid/rpki/old_irdbd.py b/rpkid/rpki/old_irdbd.py new file mode 100644 index 00000000..c63ce9e2 --- /dev/null +++ b/rpkid/rpki/old_irdbd.py @@ -0,0 +1,249 @@ +""" +IR database daemon. + +Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ] + +This is the old (pre-Django) version of irdbd, still used by smoketest +and perhaps still useful as a minimal example. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import sys, os, time, getopt, urlparse, warnings +import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng +import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509 + +from rpki.mysql_import import MySQLdb + +class main(object): + + + def handle_list_resources(self, q_pdu, r_msg): + + r_pdu = rpki.left_right.list_resources_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.child_handle = q_pdu.child_handle + + self.cur.execute( + "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s", + (q_pdu.self_handle, q_pdu.child_handle)) + + if self.cur.rowcount != 1: + raise rpki.exceptions.NotInDatabase, \ + "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \ + % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle) + + registrant_id, valid_until = self.cur.fetchone() + + r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") + + r_pdu.asn = rpki.resource_set.resource_set_as.from_sql( + self.cur, + "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s", + (registrant_id,)) + + r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql( + self.cur, + "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4", + (registrant_id,)) + + r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql( + self.cur, + "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6", + (registrant_id,)) + + r_msg.append(r_pdu) + + + def handle_list_roa_requests(self, q_pdu, r_msg): + + self.cur.execute( + "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s", + (q_pdu.self_handle,)) + + for roa_request_id, asn in self.cur.fetchall(): + + r_pdu = rpki.left_right.list_roa_requests_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.asn = asn + + r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql( + self.cur, + "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4", + (roa_request_id,)) + + r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql( + self.cur, + "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6", + (roa_request_id,)) + + r_msg.append(r_pdu) + + + def handle_list_ghostbuster_requests(self, q_pdu, r_msg): + + self.cur.execute( + "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s", + (q_pdu.self_handle, q_pdu.parent_handle)) + + vcards = [result[0] for result in self.cur.fetchall()] + + if not vcards: + + self.cur.execute( + "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL", + (q_pdu.self_handle,)) + + vcards = [result[0] for result in self.cur.fetchall()] + + for vcard in vcards: + r_pdu = rpki.left_right.list_ghostbuster_requests_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.parent_handle = q_pdu.parent_handle + r_pdu.vcard = vcard + r_msg.append(r_pdu) + + + handle_dispatch = { + rpki.left_right.list_resources_elt : handle_list_resources, + rpki.left_right.list_roa_requests_elt : handle_list_roa_requests, + rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests} + + + def handler(self, query, path, cb): + try: + + self.db.ping(True) + + r_msg = rpki.left_right.msg.reply() + + try: + + q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert)) + + if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query(): + raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg + + for q_pdu in q_msg: + + try: + + try: + h = self.handle_dispatch[type(q_pdu)] + except KeyError: + raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu + else: + h(self, q_pdu, r_msg) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) + + cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert)) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + + # We only get here in cases where we couldn't or wouldn't generate + # <report_error/>, so just return HTTP failure. + + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) + + + def __init__(self): + + os.environ["TZ"] = "UTC" + time.tzset() + + cfg_file = None + + opts, argv = getopt.getopt(sys.argv[1:], "c:dh?", ["config=", "debug", "help"]) + for o, a in opts: + if o in ("-h", "--help", "-?"): + print __doc__ + sys.exit(0) + if o in ("-c", "--config"): + cfg_file = a + elif o in ("-d", "--debug"): + rpki.log.use_syslog = False + if argv: + raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv + + rpki.log.init("irdbd") + + self.cfg = rpki.config.parser(cfg_file, "irdbd") + + startup_msg = self.cfg.get("startup-message", "") + if startup_msg: + rpki.log.info(startup_msg) + + self.cfg.set_global_flags() + + self.db = MySQLdb.connect(user = self.cfg.get("sql-username"), + db = self.cfg.get("sql-database"), + passwd = self.cfg.get("sql-password")) + + self.cur = self.db.cursor() + self.db.autocommit(True) + + self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta")) + self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert")) + self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert")) + self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key")) + + u = urlparse.urlparse(self.cfg.get("http-url")) + + assert u.scheme in ("", "http") and \ + u.username is None and \ + u.password is None and \ + u.params == "" and \ + u.query == "" and \ + u.fragment == "" + + rpki.http.server(host = u.hostname or "localhost", + port = u.port or 443, + handlers = ((u.path, self.handler),)) diff --git a/rpkid/rpki/pubd.py b/rpkid/rpki/pubd.py index bde1260e..6968780d 100644 --- a/rpkid/rpki/pubd.py +++ b/rpkid/rpki/pubd.py @@ -134,7 +134,7 @@ class main(object): raise except Exception, e: rpki.log.traceback() - cb(500, reason = "Unhandled exception %s" % e) + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) client_url_regexp = re.compile("/client/([-A-Z0-9_/]+)$", re.I) diff --git a/rpkid/rpki/rcynic.py b/rpkid/rpki/rcynic.py index c2562cbd..b7e493ec 100644 --- a/rpkid/rpki/rcynic.py +++ b/rpkid/rpki/rcynic.py @@ -226,6 +226,7 @@ class rcynic_xml_iterator(object): unauthenticated_subdir = "unauthenticated"): self.rcynic_root = rcynic_root self.xml_file = xml_file + self.authenticated_subdir = os.path.join(rcynic_root, 'authenticated') self.authenticated_old_subdir = os.path.join(rcynic_root, authenticated_old_subdir) self.unauthenticated_subdir = os.path.join(rcynic_root, unauthenticated_subdir) @@ -245,8 +246,14 @@ class rcynic_xml_iterator(object): generation = validation_status.get("generation") # determine the path to this object - filename = os.path.join(self.authenticated_old_subdir if generation == 'backup' else self.unauthenticated_subdir, - self.uri_to_filename(uri)) + if status == 'object_accepted': + d = self.authenticated_subdir + elif generation == 'backup': + d = self.authenticated_old_subdir + else: + d = self.unauthenticated_subdir + + filename = os.path.join(d, self.uri_to_filename(uri)) ext = os.path.splitext(filename)[1] if ext in file_name_classes: diff --git a/rpkid/rpki/relaxng.py b/rpkid/rpki/relaxng.py index 31881329..eed3ca2c 100644 --- a/rpkid/rpki/relaxng.py +++ b/rpkid/rpki/relaxng.py @@ -6,7 +6,7 @@ import lxml.etree ## Parsed RelaxNG left_right schema left_right = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> <!-- - $Id: left-right-schema.rnc 4346 2012-02-17 01:11:06Z sra $ + $Id: left-right-schema.rnc 4403 2012-03-19 21:14:48Z sra $ RelaxNG Schema for RPKI left-right protocol. @@ -1839,3 +1839,382 @@ publication = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" en --> ''')) +## @var myrpki +## Parsed RelaxNG myrpki schema +myrpki = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> +<!-- + $Id: myrpki.rnc 3723 2011-03-14 20:43:16Z sra $ + + RelaxNG Schema for MyRPKI XML messages. + + This message protocol is on its way out, as we're in the process of + moving on from the user interface model that produced it, but even + after we finish replacing it we'll still need the schema for a while + to validate old messages when upgrading. + + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so + run the compact syntax through trang to get XML syntax. + + Copyright (C) 2009-2011 Internet Systems Consortium ("ISC") + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE + OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. +--> +<grammar ns="http://www.hactrn.net/uris/rpki/myrpki/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <define name="version"> + <value>2</value> + </define> + <define name="base64"> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </define> + <define name="object_handle"> + <data type="string"> + <param name="maxLength">255</param> + <param name="pattern">[\-_A-Za-z0-9]*</param> + </data> + </define> + <define name="pubd_handle"> + <data type="string"> + <param name="maxLength">255</param> + <param name="pattern">[\-_A-Za-z0-9/]*</param> + </data> + </define> + <define name="uri"> + <data type="anyURI"> + <param name="maxLength">4096</param> + </data> + </define> + <define name="asn"> + <data type="positiveInteger"/> + </define> + <define name="asn_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </define> + <define name="ipv4_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/.]*</param> + </data> + </define> + <define name="ipv6_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/:a-fA-F]*</param> + </data> + </define> + <define name="timestamp"> + <data type="dateTime"> + <param name="pattern">.*Z</param> + </data> + </define> + <!-- + Message formate used between configure_resources and + configure_daemons. + --> + <start combine="choice"> + <element name="myrpki"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <zeroOrMore> + <element name="roa_request"> + <attribute name="asn"> + <ref name="asn"/> + </attribute> + <attribute name="v4"> + <ref name="ipv4_list"/> + </attribute> + <attribute name="v6"> + <ref name="ipv6_list"/> + </attribute> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="child"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="valid_until"> + <ref name="timestamp"/> + </attribute> + <optional> + <attribute name="asns"> + <ref name="asn_list"/> + </attribute> + </optional> + <optional> + <attribute name="v4"> + <ref name="ipv4_list"/> + </attribute> + </optional> + <optional> + <attribute name="v6"> + <ref name="ipv6_list"/> + </attribute> + </optional> + <optional> + <element name="bpki_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="parent"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <attribute name="myhandle"> + <ref name="object_handle"/> + </attribute> + </optional> + <optional> + <attribute name="sia_base"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <element name="bpki_cms_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="repository"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <element name="bpki_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <optional> + <element name="bpki_ca_certificate"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_crl"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_bsc_certificate"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_bsc_pkcs10"> + <ref name="base64"/> + </element> + </optional> + </element> + </start> + <!-- Format of an identity.xml file. --> + <start combine="choice"> + <element name="identity"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <element name="bpki_ta"> + <ref name="base64"/> + </element> + </element> + </start> + <!-- + Format of <authorization/> element used in referrals. The Base64 + text is a <referral/> (q. v.) element signed with CMS. + --> + <define name="authorization"> + <element name="authorization"> + <attribute name="referrer"> + <ref name="pubd_handle"/> + </attribute> + <ref name="base64"/> + </element> + </define> + <!-- Format of <contact_info/> element used in referrals. --> + <define name="contact_info"> + <element name="contact_info"> + <optional> + <attribute name="uri"> + <ref name="uri"/> + </attribute> + </optional> + <data type="string"/> + </element> + </define> + <!-- Variant payload portion of a <repository/> element. --> + <define name="repository_payload"> + <choice> + <attribute name="type"> + <value>none</value> + </attribute> + <attribute name="type"> + <value>offer</value> + </attribute> + <group> + <attribute name="type"> + <value>referral</value> + </attribute> + <ref name="authorization"/> + <ref name="contact_info"/> + </group> + </choice> + </define> + <!-- <parent/> element (response from configure_child). --> + <start combine="choice"> + <element name="parent"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="valid_until"> + <ref name="timestamp"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <attribute name="child_handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <element name="bpki_resource_ta"> + <ref name="base64"/> + </element> + <element name="bpki_child_ta"> + <ref name="base64"/> + </element> + <optional> + <element name="repository"> + <ref name="repository_payload"/> + </element> + </optional> + </element> + </start> + <!-- + <repository/> element, types offer and referral + (input to configure_publication_client). + --> + <start combine="choice"> + <element name="repository"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <ref name="repository_payload"/> + <element name="bpki_client_ta"> + <ref name="base64"/> + </element> + </element> + </start> + <!-- + <repository/> element, confirmation type (output of + configure_publication_client). + --> + <start combine="choice"> + <element name="repository"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="type"> + <value>confirmed</value> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="client_handle"> + <ref name="pubd_handle"/> + </attribute> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + <attribute name="sia_base"> + <ref name="uri"/> + </attribute> + <element name="bpki_server_ta"> + <ref name="base64"/> + </element> + <element name="bpki_client_ta"> + <ref name="base64"/> + </element> + <optional> + <ref name="authorization"/> + </optional> + <optional> + <ref name="contact_info"/> + </optional> + </element> + </start> + <!-- + <referral/> element. This is the entirety of a separate message + which is signed with CMS then included ase the Base64 content of an + <authorization/> element in the main message. + --> + <start combine="choice"> + <element name="referral"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="authorized_sia_base"> + <ref name="uri"/> + </attribute> + <ref name="base64"/> + </element> + </start> +</grammar> +<!-- + Local Variables: + indent-tabs-mode: nil + End: +--> +''')) + diff --git a/rpkid/rpki/resource_set.py b/rpkid/rpki/resource_set.py index 2fd10756..be39df75 100644 --- a/rpkid/rpki/resource_set.py +++ b/rpkid/rpki/resource_set.py @@ -500,6 +500,18 @@ class resource_set(list): for (b, e) in sql.fetchall()]) @classmethod + def from_django(cls, iterable): + """ + Create resource set from a Django query. + + iterable is something which returns (min, max) pairs. + """ + + return cls(ini = [cls.range_type(cls.range_type.datum_type(b), + cls.range_type.datum_type(e)) + for (b, e) in iterable]) + + @classmethod def parse_str(cls, s): """ Parse resource set from text string (eg, XML attributes). This is @@ -983,6 +995,19 @@ class roa_prefix_set(list): return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z)) for (x, y, z) in sql.fetchall()]) + @classmethod + def from_django(cls, iterable): + """ + Create ROA prefix set from a Django query. + + iterable is something which returns (prefix, prefixlen, + max_prefixlen) triples. + """ + + return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z)) + for (x, y, z) in iterable]) + + def to_roa_tuple(self): """ Convert ROA prefix set into tuple format used by ROA ASN.1 @@ -1029,6 +1054,29 @@ class roa_prefix_set_ipv6(roa_prefix_set): # Fix back link from resource_set to roa_prefix resource_set_ipv6.roa_prefix_set_type = roa_prefix_set_ipv6 +class roa_prefix_bag(object): + """ + Container to simplify passing around the combination of an IPv4 ROA + prefix set and an IPv6 ROA prefix set. + """ + + ## @var v4 + # Set of IPv4 prefixes. + + ## @var v6 + # Set of IPv6 prefixes. + + def __init__(self, v4 = None, v6 = None): + self.v4 = v4 or roa_prefix_set_ipv4() + self.v6 = v6 or roa_prefix_set_ipv6() + + def __eq__(self, other): + return self.v4 == other.v4 and self.v6 == other.v6 + + def __ne__(self, other): + return not (self == other) + + # Test suite for set operations. if __name__ == "__main__": diff --git a/rpkid/rpki/rootd.py b/rpkid/rpki/rootd.py index 668e4027..feceffc5 100644 --- a/rpkid/rpki/rootd.py +++ b/rpkid/rpki/rootd.py @@ -257,7 +257,7 @@ class main(object): return cb(400, reason = "Could not process PDU: %s" % e) def done(r_msg): - cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert, self.rootd_bpki_crl)) + cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert)) try: q_msg.serve_top_level(None, done) @@ -345,7 +345,7 @@ class main(object): self.rpki_root_dir = self.cfg.get("rpki-root-dir") self.rpki_base_uri = self.cfg.get("rpki-base-uri", "rsync://" + self.rpki_class_name + ".invalid/") - self.rpki_root_key = rpki.x509.RSA( Auto_file = self.cfg.get("rpki-root-key")) + self.rpki_root_key = rpki.x509.RSA(Auto_update = self.cfg.get("rpki-root-key")) self.rpki_root_cert_file = self.cfg.get("rpki-root-cert") self.rpki_root_cert_uri = self.cfg.get("rpki-root-cert-uri", self.rpki_base_uri + "Root.cer") diff --git a/rpkid/rpki/rpkic.py b/rpkid/rpki/rpkic.py new file mode 100644 index 00000000..dbaee3bb --- /dev/null +++ b/rpkid/rpki/rpkic.py @@ -0,0 +1,486 @@ +""" +This is a command line configuration and control tool for rpkid et al. + +Type "help" on the prompt, or run the program with the --help option for an +overview of the available commands; type "help foo" for (more) detailed help +on the "foo" command. + + +This program is a rewrite of the old myrpki program, replacing ten +zillion XML and X.509 disk files and subprocess calls to the OpenSSL +command line tool with SQL data and direct calls to the rpki.POW +library. This version abandons all pretense that this program might +somehow work without rpki.POW, lxml, and Django installed, but since +those packages are required for rpkid anyway, this seems like a small +price to pay for major simplification of the code and better +integration with the Django-based GUI interface. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +# NB: As of this writing, I'm trying really hard to avoid having this +# program depend on a Django settings.py file. This may prove to be a +# waste of time in the long run, but for for now, this means that one +# has to be careful about exactly how and when one imports Django +# modules, or anything that imports Django modules. Bottom line is +# that we don't import such modules until we need them. + +import csv +import re +import os +import getopt +import sys +import base64 +import time +import glob +import copy +import warnings +import rpki.config +import rpki.cli +import rpki.sundial +import rpki.log +import rpki.oids +import rpki.http +import rpki.resource_set +import rpki.relaxng +import rpki.exceptions +import rpki.left_right +import rpki.x509 +import rpki.async + +class BadCommandSyntax(Exception): "Bad command line syntax." +class BadPrefixSyntax(Exception): "Bad prefix syntax." +class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon." +class BadXMLMessage(Exception): "Bad XML message." +class PastExpiration(Exception): "Expiration date has already passed." +class CantRunRootd(Exception): "Can't run rootd." + +class main(rpki.cli.Cmd): + + prompt = "rpkic> " + + completedefault = rpki.cli.Cmd.filename_complete + + def __init__(self): + os.environ["TZ"] = "UTC" + time.tzset() + + rpki.log.use_syslog = False + + cfg_file = None + handle = None + + opts, argv = getopt.getopt(sys.argv[1:], "c:hi:?", ["config=", "help", "identity="]) + for o, a in opts: + if o in ("-c", "--config"): + cfg_file = a + elif o in ("-h", "--help", "-?"): + argv = ["help"] + elif o in ("-i", "--identity"): + handle = a + + if not argv or argv[0] != "help": + rpki.log.init("rpkic") + self.read_config(cfg_file, handle) + + rpki.cli.Cmd.__init__(self, argv) + + def read_config(self, cfg_file, handle): + global rpki + + cfg = rpki.config.parser(cfg_file, "myrpki") + cfg.set_global_flags() + + from django.conf import settings + + settings.configure( + DATABASES = { "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : cfg.get("sql-database", section = "irdbd"), + "USER" : cfg.get("sql-username", section = "irdbd"), + "PASSWORD" : cfg.get("sql-password", section = "irdbd"), + "HOST" : "", + "PORT" : "", + "OPTIONS" : { "init_command": "SET storage_engine=INNODB" }}}, + INSTALLED_APPS = ("rpki.irdb",), + ) + + import rpki.irdb + + try: + rpki.irdb.models.ca_certificate_lifetime = rpki.sundial.timedelta.parse( + cfg.get("bpki_ca_certificate_lifetime", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + try: + rpki.irdb.models.ee_certificate_lifetime = rpki.sundial.timedelta.parse( + cfg.get("bpki_ee_certificate_lifetime", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + try: + rpki.irdb.models.crl_interval = rpki.sundial.timedelta.parse( + cfg.get("bpki_crl_interval", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + import django.core.management + django.core.management.call_command("syncdb", verbosity = 0, load_initial_data = False) + + self.zoo = rpki.irdb.Zookeeper(cfg = cfg, handle = handle, logstream = sys.stdout) + + def help_overview(self): + """ + Show program __doc__ string. Perhaps there's some clever way to + do this using the textwrap module, but for now something simple + and crude will suffice. + """ + + for line in __doc__.splitlines(True): + self.stdout.write(" " * 4 + line) + self.stdout.write("\n") + + def irdb_handle_complete(self, klass, text, line, begidx, endidx): + return [obj.handle for obj in klass.objects.all() if obj.handle and obj.handle.startswith(text)] + + def do_select_identity(self, arg): + """ + Select an identity handle for use with later commands. + """ + + argv = arg.split() + if len(argv) != 1: + raise BadCommandSyntax("This command expexcts one argument, not %r" % arg) + self.zoo.reset_identity(argv[0]) + + def complete_select_identity(self, *args): + return self.irdb_handle_complete(rpki.irdb.ResourceHolderCA, *args) + + + def do_initialize(self, arg): + """ + Initialize an RPKI installation. This command reads the + configuration file, creates the BPKI and EntityDB directories, + generates the initial BPKI certificates, and creates an XML file + describing the resource-holding aspect of this RPKI installation. + """ + + if arg: + raise BadCommandSyntax, "This command takes no arguments" + + r = self.zoo.initialize() + r.save("%s.identity.xml" % self.zoo.handle, + None if self.zoo.run_pubd else sys.stdout) + + if self.zoo.run_rootd and self.zoo.handle == self.zoo.cfg.get("handle"): + r = self.zoo.configure_rootd() + if r is not None: + r.save("%s.%s.repository-request.xml" % (self.zoo.handle, self.zoo.handle), sys.stdout) + + self.zoo.write_bpki_files() + + + def do_update_bpki(self, arg): + """ + Update BPKI certificates. Assumes an existing RPKI installation. + + Basic plan here is to reissue all BPKI certificates we can, right + now. In the long run we might want to be more clever about only + touching ones that need maintenance, but this will do for a start. + + We also reissue CRLs for all CAs. + + Most likely this should be run under cron. + """ + + self.zoo.update_bpki() + self.zoo.write_bpki_files() + + + def do_configure_child(self, arg): + """ + Configure a new child of this RPKI entity, given the child's XML + identity file as an input. This command extracts the child's data + from the XML, cross-certifies the child's resource-holding BPKI + certificate, and generates an XML file describing the relationship + between the child and this parent, including this parent's BPKI + data and up-down protocol service URI. + """ + + child_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["child_handle="]) + for o, a in opts: + if o == "--child_handle": + child_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for child.xml" + + r, child_handle = self.zoo.configure_child(argv[0], child_handle) + r.save("%s.%s.parent-response.xml" % (self.zoo.handle, child_handle), sys.stdout) + + + def do_delete_child(self, arg): + """ + Delete a child of this RPKI entity. + """ + + try: + self.zoo.delete_child(arg) + except rpki.irdb.Child.DoesNotExist: + print "No such child \"%s\"" % arg + + def complete_delete_child(self, *args): + return self.irdb_handle_complete(rpki.irdb.Child, *args) + + + def do_configure_parent(self, arg): + """ + Configure a new parent of this RPKI entity, given the output of + the parent's configure_child command as input. This command reads + the parent's response XML, extracts the parent's BPKI and service + URI information, cross-certifies the parent's BPKI data into this + entity's BPKI, and checks for offers or referrals of publication + service. If a publication offer or referral is present, we + generate a request-for-service message to that repository, in case + the user wants to avail herself of the referral or offer. + """ + + parent_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="]) + for o, a in opts: + if o == "--parent_handle": + parent_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for parent.xml on command line" + + r, parent_handle = self.zoo.configure_parent(argv[0], parent_handle) + r.save("%s.%s.repository-request.xml" % (self.zoo.handle, parent_handle), sys.stdout) + + + def do_delete_parent(self, arg): + """ + Delete a parent of this RPKI entity. + """ + + try: + self.zoo.delete_parent(arg) + except rpki.irdb.Parent.DoesNotExist: + print "No such parent \"%s\"" % arg + + def complete_delete_parent(self, *args): + return self.irdb_handle_complete(rpki.irdb.Parent, *args) + + + def do_configure_publication_client(self, arg): + """ + Configure publication server to know about a new client, given the + client's request-for-service message as input. This command reads + the client's request for service, cross-certifies the client's + BPKI data, and generates a response message containing the + repository's BPKI data and service URI. + """ + + sia_base = None + + opts, argv = getopt.getopt(arg.split(), "", ["sia_base="]) + for o, a in opts: + if o == "--sia_base": + sia_base = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for client.xml" + + r, client_handle = self.zoo.configure_publication_client(argv[0], sia_base) + r.save("%s.repository-response.xml" % client_handle.replace("/", "."), sys.stdout) + + + def do_delete_publication_client(self, arg): + """ + Delete a publication client of this RPKI entity. + """ + + try: + self.zoo.delete_publication_client(arg).delete() + except rpki.irdb.Client.DoesNotExist: + print "No such client \"%s\"" % arg + + def complete_delete_publication_client(self, *args): + return self.irdb_handle_complete(rpki.irdb.Client, *args) + + + def do_configure_repository(self, arg): + """ + Configure a publication repository for this RPKI entity, given the + repository's response to our request-for-service message as input. + This command reads the repository's response, extracts and + cross-certifies the BPKI data and service URI, and links the + repository data with the corresponding parent data in our local + database. + """ + + parent_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="]) + for o, a in opts: + if o == "--parent_handle": + parent_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for repository.xml on command line" + + self.zoo.configure_repository(argv[0], parent_handle) + + def do_delete_repository(self, arg): + """ + Delete a repository of this RPKI entity. + + This should check that the XML file it's deleting really is a + repository, but doesn't, yet. + """ + + try: + self.zoo.delete_repository(arg) + except rpki.irdb.Repository.DoesNotExist: + print "No such repository \"%s\"" % arg + + def complete_delete_repository(self, *args): + return self.irdb_handle_complete(rpki.irdb.Repository, *args) + + + def do_delete_self(self, arg): + """ + Delete the current RPKI entity (<self/> object). + """ + + self.zoo.delete_self() + + + def do_renew_child(self, arg): + """ + Update validity period for one child entity. + """ + + valid_until = None + + opts, argv = getopt.getopt(arg.split(), "", ["valid_until"]) + for o, a in opts: + if o == "--valid_until": + valid_until = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify child handle" + + self.zoo.renew_children(argv[0], valid_until) + + def complete_renew_child(self, *args): + return self.irdb_handle_complete(rpki.irdb.Child, *args) + + + def do_renew_all_children(self, arg): + """ + Update validity period for all child entities. + """ + + valid_until = None + + opts, argv = getopt.getopt(arg.split(), "", ["valid_until"]) + for o, a in opts: + if o == "--valid_until": + valid_until = a + + if len(argv) != 0: + raise BadCommandSyntax, "Unexpected arguments" + + self.zoo.renew_children(None, valid_until) + + + def do_load_prefixes(self, arg): + """ + Load prefixes into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify prefixes.csv filename") + + self.zoo.load_prefixes(argv[0]) + + + def do_show_child_resources(self, arg): + """ + Show resources assigned to children. + """ + + if arg.strip(): + raise BadCommandSyntax("This command takes no arguments") + + for child in self.zoo.resource_ca.children.all(): + resources = child.resource_bag + + print "Child:", child.handle + if resources.asn: + print " ASN:", resources.asn + if resources.v4: + print " IPv4:", resources.v4 + if resources.v6: + print " IPv6:", resources.v6 + + + def do_load_asns(self, arg): + """ + Load ASNs into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify asns.csv filename") + + self.zoo.load_asns(argv[0]) + + + def do_load_roa_requests(self, arg): + """ + Load ROA requests into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify roa.csv filename") + + self.zoo.load_roa_requests(argv[0]) + + + def do_synchronize(self, arg): + """ + Whack daemons to match IRDB. + + This command may be replaced by implicit synchronization embedded + in of other commands, haven't decided yet. + """ + + if arg: + raise BadCommandSyntax("Unexpected argument(s): %r" % arg) + + self.zoo.synchronize() diff --git a/rpkid/rpki/rpkid.py b/rpkid/rpki/rpkid.py index 76b3c81a..7501a16a 100644 --- a/rpkid/rpki/rpkid.py +++ b/rpkid/rpki/rpkid.py @@ -242,7 +242,7 @@ class main(object): raise except Exception, e: rpki.log.traceback() - cb(500, reason = "Unhandled exception %s" % e) + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I) diff --git a/rpkid/rpki/sql_schemas.py b/rpkid/rpki/sql_schemas.py index 154ab5c1..e7c65299 100644 --- a/rpkid/rpki/sql_schemas.py +++ b/rpkid/rpki/sql_schemas.py @@ -239,115 +239,6 @@ CREATE TABLE ghostbuster ( -- End: ''' -## @var irdbd -## SQL schema irdbd -irdbd = '''-- $Id: irdbd.sql 3730 2011-03-21 12:42:43Z sra $ - --- Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") --- --- Permission to use, copy, modify, and distribute this software for any --- purpose with or without fee is hereby granted, provided that the above --- copyright notice and this permission notice appear in all copies. --- --- THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH --- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY --- AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, --- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM --- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE --- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR --- PERFORMANCE OF THIS SOFTWARE. - --- Copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") --- --- Permission to use, copy, modify, and distribute this software for any --- purpose with or without fee is hereby granted, provided that the above --- copyright notice and this permission notice appear in all copies. --- --- THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH --- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY --- AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT, --- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM --- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE --- OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR --- PERFORMANCE OF THIS SOFTWARE. - --- SQL objects needed by irdbd.py. You only need this if you're using --- irdbd.py as your IRDB; if you have a "real" backend you can do --- anything you like so long as you implement the relevant portion of --- the left-right protocol. - --- DROP TABLE commands must be in correct (reverse dependency) order --- to satisfy FOREIGN KEY constraints. - -DROP TABLE IF EXISTS roa_request_prefix; -DROP TABLE IF EXISTS roa_request; -DROP TABLE IF EXISTS registrant_net; -DROP TABLE IF EXISTS registrant_asn; -DROP TABLE IF EXISTS registrant; -DROP TABLE IF EXISTS ghostbuster_request; - -CREATE TABLE registrant ( - registrant_id SERIAL NOT NULL, - registrant_handle VARCHAR(255) NOT NULL, - registrant_name TEXT, - registry_handle VARCHAR(255), - valid_until DATETIME NOT NULL, - PRIMARY KEY (registrant_id), - UNIQUE (registry_handle, registrant_handle) -) ENGINE=InnoDB; - -CREATE TABLE registrant_asn ( - registrant_asn_id SERIAL NOT NULL, - start_as BIGINT UNSIGNED NOT NULL, - end_as BIGINT UNSIGNED NOT NULL, - registrant_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (registrant_asn_id), - CONSTRAINT registrant_asn_registrant_id - FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE registrant_net ( - registrant_net_id SERIAL NOT NULL, - start_ip VARCHAR(40) NOT NULL, - end_ip VARCHAR(40) NOT NULL, - version TINYINT UNSIGNED NOT NULL, - registrant_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (registrant_net_id), - CONSTRAINT registrant_net_registrant_id - FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE roa_request ( - roa_request_id SERIAL NOT NULL, - roa_request_handle VARCHAR(255) NOT NULL, - asn BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (roa_request_id) -) ENGINE=InnoDB; - -CREATE TABLE roa_request_prefix ( - prefix VARCHAR(40) NOT NULL, - prefixlen TINYINT UNSIGNED NOT NULL, - max_prefixlen TINYINT UNSIGNED NOT NULL, - version TINYINT UNSIGNED NOT NULL, - roa_request_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (roa_request_id, prefix, prefixlen, max_prefixlen), - CONSTRAINT roa_request_prefix_roa_request_id - FOREIGN KEY (roa_request_id) REFERENCES roa_request (roa_request_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE ghostbuster_request ( - ghostbuster_request_id SERIAL NOT NULL, - self_handle VARCHAR(40) NOT NULL, - parent_handle VARCHAR(40), - vcard LONGBLOB NOT NULL, - PRIMARY KEY (ghostbuster_request_id) -) ENGINE=InnoDB; - --- Local Variables: --- indent-tabs-mode: nil --- End: -''' - ## @var pubd ## SQL schema pubd pubd = '''-- $Id: pubd.sql 3465 2010-10-07 00:59:39Z sra $ diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py index b96dec3f..955b8d97 100644 --- a/rpkid/rpki/x509.py +++ b/rpkid/rpki/x509.py @@ -47,6 +47,7 @@ import rpki.POW, rpki.POW.pkix, base64, lxml.etree, os, subprocess, sys import email.mime.application, email.utils, mailbox, time import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial import rpki.manifest, rpki.roa, rpki.log, rpki.async, rpki.ghostbuster +import rpki.relaxng def base64_with_linebreaks(der): """ @@ -120,6 +121,74 @@ def _find_xia_uri(extension, name): return location[1] return None +class X501DN(object): + """ + Class to hold an X.501 Distinguished Name. + + This is nothing like a complete implementation, just enough for our + purposes. POW has one interface to this, POW.pkix has another. In + terms of completeness in the Python representation, the POW.pkix + representation is much closer to right, but the whole thing is a + horrible mess. + + See RFC 5280 4.1.2.4 for the ASN.1 details. In brief: + + - A DN is a SEQUENCE of RDNs. + + - A RDN is a set of AttributeAndValues; in practice, multi-value + RDNs are rare, so an RDN is almost always a set with a single + element. + + - An AttributeAndValue is an OID and a value, where a whole bunch + of things including both syntax and semantics of the value are + determined by the OID. + + - The value is some kind of ASN.1 string; there are far too many + encoding options options, most of which are either strongly + discouraged or outright forbidden by the PKIX profile, but which + persist for historical reasons. The only ones PKIX actually + likes are PrintableString and UTF8String, but there are nuances + and special cases where some of the others are required. + + The RPKI profile further restricts DNs to a single mandatory + CommonName attribute with a single optional SerialNumber attribute + (not to be confused with the certificate serial number). + + BPKI certificates should (we hope) follow the general PKIX guideline + but the ones we construct ourselves are likely to be relatively + simple. + + The main purpose of this class is to hide as much as possible of + this mess from code that has to work with these wretched things. + """ + + def __init__(self, ini = None, **kwargs): + assert ini is None or not kwargs + if len(kwargs) == 1 and "CN" in kwargs: + ini = kwargs.pop("CN") + if isinstance(ini, (str, unicode)): + self.dn = (((rpki.oids.name2oid["commonName"], ("printableString", ini)),),) + elif isinstance(ini, tuple): + self.dn = ini + elif kwargs: + raise NotImplementedError("Sorry, I haven't implemented keyword arguments yet") + elif ini is not None: + raise TypeError("Don't know how to interpret %r as an X.501 DN" % (ini,), ini) + + def __str__(self): + return "".join("/" + "+".join("%s=%s" % (rpki.oids.oid2name[a[0]], a[1][1]) + for a in rdn) + for rdn in self.dn) + + def __cmp__(self, other): + return cmp(self.dn, other.dn) + + def get_POWpkix(self): + return self.dn + + def get_POW(self): + raise NotImplementedError("Sorry, I haven't written the conversion to POW format yet") + class DER_object(object): """ Virtual class to hold a generic DER object. @@ -259,6 +328,8 @@ class DER_object(object): return -1 elif other is None: return 1 + elif isinstance(other, str): + return cmp(self.get_DER(), other) else: return cmp(self.get_DER(), other.get_DER()) @@ -456,13 +527,13 @@ class X509(DER_object): """ Get the issuer of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) def getSubject(self): """ Get the subject of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getSubject()) + return X501DN(self.get_POWpkix().getSubject()) def getNotBefore(self): """ @@ -497,11 +568,60 @@ class X509(DER_object): def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter, cn = None, resources = None, is_ca = True): """ - Issue a certificate. + Issue an RPKI certificate. + """ + + assert aia is not None and crldp is not None + + return self._issue( + keypair = keypair, + subject_key = subject_key, + serial = serial, + sia = sia, + aia = aia, + crldp = crldp, + notAfter = notAfter, + cn = cn, + resources = resources, + is_ca = is_ca, + aki = self.get_SKI(), + issuer_name = self.get_POWpkix().getSubject()) + + + @classmethod + def self_certify(cls, keypair, subject_key, serial, sia, notAfter, + cn = None, resources = None): + """ + Generate a self-certified RPKI certificate. + """ + + ski = subject_key.get_SKI() + if cn is None: + cn = "".join(("%02X" % ord(i) for i in ski)) + + return cls._issue( + keypair = keypair, + subject_key = subject_key, + serial = serial, + sia = sia, + aia = None, + crldp = None, + notAfter = notAfter, + cn = cn, + resources = resources, + is_ca = True, + aki = ski, + issuer_name = (((rpki.oids.name2oid["commonName"], ("printableString", cn)),),)) + + + @staticmethod + def _issue(keypair, subject_key, serial, sia, aia, crldp, notAfter, + cn, resources, is_ca, aki, issuer_name): + """ + Common code to issue an RPKI certificate. """ now = rpki.sundial.now() - aki = self.get_SKI() ski = subject_key.get_SKI() if cn is None: @@ -512,7 +632,7 @@ class X509(DER_object): cert = rpki.POW.pkix.Certificate() cert.setVersion(2) cert.setSerial(serial) - cert.setIssuer(self.get_POWpkix().getSubject()) + cert.setIssuer(issuer_name) cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),)) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) @@ -520,10 +640,15 @@ class X509(DER_object): exts = [ ["subjectKeyIdentifier", False, ski], ["authorityKeyIdentifier", False, (aki, (), None)], - ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)], - ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)], ["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ] + + if crldp is not None: + exts.append(["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)]) + + if aia is not None: + exts.append(["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)]) + if is_ca: exts.append(["basicConstraints", True, (1, None)]) exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)]) @@ -555,33 +680,96 @@ class X509(DER_object): return X509(POWpkix = cert) - def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0): + def bpki_cross_certify(self, keypair, source_cert, serial, notAfter, + now = None, pathLenConstraint = 0): + """ + Issue a BPKI certificate with values taking from an existing certificate. + """ + return self.bpki_certify( + keypair = keypair, + subject_name = source_cert.getSubject(), + subject_key = source_cert.getPublicKey(), + serial = serial, + notAfter = notAfter, + now = now, + pathLenConstraint = pathLenConstraint, + is_ca = True) + + @classmethod + def bpki_self_certify(cls, keypair, subject_name, serial, notAfter, + now = None, pathLenConstraint = None): + """ + Issue a self-signed BPKI CA certificate. + """ + return cls._bpki_certify( + keypair = keypair, + issuer_name = subject_name, + subject_name = subject_name, + subject_key = keypair.get_RSApublic(), + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = True) + + def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, is_ca, + now = None, pathLenConstraint = None): + """ + Issue a normal BPKI certificate. + """ + assert keypair.get_RSApublic() == self.getPublicKey() + return self._bpki_certify( + keypair = keypair, + issuer_name = self.getSubject(), + subject_name = subject_name, + subject_key = subject_key, + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = is_ca) + + @classmethod + def _bpki_certify(cls, keypair, issuer_name, subject_name, subject_key, + serial, now, notAfter, pathLenConstraint, is_ca): """ - Issue a certificate with values taking from an existing certificate. - This is used to construct some kinds oF BPKI certificates. + Issue a BPKI certificate. This internal method does the real + work, after one of the wrapper methods has extracted the relevant + fields. """ if now is None: now = rpki.sundial.now() - assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0 + issuer_key = keypair.get_RSApublic() + + assert (issuer_key == subject_key) == (issuer_name == subject_name) + assert is_ca or issuer_name != subject_name + assert is_ca or pathLenConstraint is None + assert pathLenConstraint is None or (isinstance(pathLenConstraint, (int, long)) and + pathLenConstraint >= 0) + + extensions = [ + (rpki.oids.name2oid["subjectKeyIdentifier" ], False, subject_key.get_SKI())] + if issuer_key != subject_key: + extensions.append( + (rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer_key.get_SKI(), (), None))) + if is_ca: + extensions.append( + (rpki.oids.name2oid["basicConstraints" ], True, (1, pathLenConstraint))) cert = rpki.POW.pkix.Certificate() cert.setVersion(2) cert.setSerial(serial) - cert.setIssuer(self.get_POWpkix().getSubject()) - cert.setSubject(source_cert.get_POWpkix().getSubject()) + cert.setIssuer(issuer_name.get_POWpkix()) + cert.setSubject(subject_name.get_POWpkix()) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) - cert.tbs.subjectPublicKeyInfo.set( - source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get()) - cert.setExtensions(( - (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()), - (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)), - (rpki.oids.name2oid["basicConstraints" ], True, (1, 0)))) + cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER()) + cert.setExtensions(extensions) cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST) - return X509(POWpkix = cert) + return cls(POWpkix = cert) @classmethod def normalize_chain(cls, chain): @@ -628,6 +816,12 @@ class PKCS10(DER_object): self.POWpkix = req return self.POWpkix + def getSubject(self): + """ + Extract the subject name from this certification request. + """ + return X501DN(self.get_POWpkix().certificationRequestInfo.subject.get()) + def getPublicKey(self): """ Extract the public key from this certification request. @@ -1262,7 +1456,10 @@ class XML_CMS_object(CMS_object): Wrap an XML PDU in CMS and return its DER encoding. """ rpki.log.trace() - self.set_content(msg.toXML()) + if self.saxify is None: + self.set_content(msg) + else: + self.set_content(msg.toXML()) self.schema_check() self.sign(keypair, certs, crls) if self.dump_outbound_cms: @@ -1277,7 +1474,22 @@ class XML_CMS_object(CMS_object): self.dump_inbound_cms.dump(self) self.verify(ta) self.schema_check() - return self.saxify(self.get_content()) + if self.saxify is None: + return self.get_content() + else: + return self.saxify(self.get_content()) + + ## @var saxify + # SAX handler hook. Subclasses can set this to a SAX handler, in + # which case .unwrap() will call it and return the result. + # Otherwise, .unwrap() just returns a verified element tree. + + saxify = None + +class SignedReferral(XML_CMS_object): + encoding = "us-ascii" + schema = rpki.relaxng.myrpki + saxify = None class Ghostbuster(CMS_object): """ @@ -1373,7 +1585,7 @@ class CRL(DER_object): """ Get issuer value of this CRL. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) @classmethod def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"): diff --git a/rpkid/rpkic.py b/rpkid/rpkic.py new file mode 100644 index 00000000..6ef3a67b --- /dev/null +++ b/rpkid/rpkic.py @@ -0,0 +1,21 @@ +""" +$Id$ + +Copyright (C) 2010-2011 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +if __name__ == "__main__": + import rpki.rpkic + rpki.rpkic.main() diff --git a/rpkid/setup.py b/rpkid/setup.py index 75d6069c..348aa544 100644 --- a/rpkid/setup.py +++ b/rpkid/setup.py @@ -63,8 +63,10 @@ setup(name = "rpkitoolkit", description = "RPKI Toolkit", license = "BSD", url = "http://www.rpki.net/", - packages = ["rpki", "rpki.POW", "rpki.gui", "rpki.gui.app", "rpki.gui.cacheview" ], + packages = ["rpki", "rpki.POW", "rpki.irdb", + "rpki.gui", "rpki.gui.app", "rpki.gui.cacheview", + "rpki.gui.routeview" ], ext_modules = [pow], - package_data = { 'rpki.gui.app' : ['templates/*.html', 'templates/*/*.html'], - 'rpki.gui.cacheview' : [ 'templates/*/*.html' ] }, + package_data = {'rpki.gui.app': ['templates/*.html', 'templates/*/*.html', 'templatetags/*.py'], + 'rpki.gui.cacheview': ['templates/*/*.html']}, data_files = data_files) diff --git a/rpkid/tests/old_irdbd.py b/rpkid/tests/old_irdbd.py new file mode 100644 index 00000000..3fa84b80 --- /dev/null +++ b/rpkid/tests/old_irdbd.py @@ -0,0 +1,21 @@ +""" +$Id$ + +Copyright (C) 2010-2012 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +if __name__ == "__main__": + import rpki.old_irdbd + rpki.old_irdbd.main() diff --git a/rpkid/irdbd.sql b/rpkid/tests/old_irdbd.sql index bf324cd8..bf324cd8 100644 --- a/rpkid/irdbd.sql +++ b/rpkid/tests/old_irdbd.sql diff --git a/rpkid/tests/smoketest.py b/rpkid/tests/smoketest.py index 32f78726..3cb90d11 100644 --- a/rpkid/tests/smoketest.py +++ b/rpkid/tests/smoketest.py @@ -124,8 +124,8 @@ pubd_name = cfg.get("pubd_name", "pubd") prog_python = cfg.get("prog_python", sys.executable) prog_rpkid = cfg.get("prog_rpkid", "../../rpkid.py") -prog_irdbd = cfg.get("prog_irdbd", "../../irdbd.py") -prog_poke = cfg.get("prog_poke", "../../testpoke.py") +prog_irdbd = cfg.get("prog_irdbd", "../old_irdbd.py") +prog_poke = cfg.get("prog_poke", "../testpoke.py") prog_rootd = cfg.get("prog_rootd", "../../rootd.py") prog_pubd = cfg.get("prog_pubd", "../../pubd.py") prog_rsyncd = cfg.get("prog_rsyncd", "rsync") @@ -135,7 +135,7 @@ prog_openssl = cfg.get("prog_openssl", "../../../openssl/openssl/apps/openss rcynic_stats = cfg.get("rcynic_stats", "echo ; ../../../rcynic/show.sh %s.xml ; echo" % rcynic_name) rpki_sql_file = cfg.get("rpki_sql_file", "../rpkid.sql") -irdb_sql_file = cfg.get("irdb_sql_file", "../irdbd.sql") +irdb_sql_file = cfg.get("irdb_sql_file", "old_irdbd.sql") pub_sql_file = cfg.get("pub_sql_file", "../pubd.sql") startup_delay = int(cfg.get("startup_delay", "10")) @@ -868,7 +868,12 @@ class allocation(object): except IOError: serial = 1 - x = parent.cross_certify(keypair, child, serial, notAfter, now) + x = parent.bpki_cross_certify( + keypair = keypair, + source_cert = child, + serial = serial, + notAfter = notAfter, + now = now) f = open(serial_file, "w") f.write("%02x\n" % (serial + 1)) @@ -1265,16 +1270,12 @@ def mangle_sql(filename): """ Mangle an SQL file into a sequence of SQL statements. """ - - # There is no pretty way to do this. Just shut your eyes, it'll be - # over soon. - + words = [] f = open(filename) - statements = " ".join(" ".join(word for word in line.expandtabs().split(" ") if word) - for line in [line.strip(" \t\n") for line in f.readlines()] - if line and not line.startswith("--")).rstrip(";").split(";") + for line in f: + words.extend(line.partition("--")[0].split()) f.close() - return [stmt.strip() for stmt in statements] + return " ".join(words).strip(";").split(";") bpki_cert_fmt_1 = '''\ [ req ] diff --git a/rpkid/tests/sql-cleaner.py b/rpkid/tests/sql-cleaner.py index 5c772bc4..5db122e1 100644 --- a/rpkid/tests/sql-cleaner.py +++ b/rpkid/tests/sql-cleaner.py @@ -3,7 +3,7 @@ $Id$ -Copyright (C) 2009--2010 Internet Systems Consortium ("ISC") +Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -18,7 +18,8 @@ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -import subprocess, rpki.config +import rpki.config, rpki.sql_schemas +from rpki.mysql_import import MySQLdb cfg = rpki.config.parser(None, "yamltest", allow_missing = True) @@ -26,8 +27,30 @@ for name in ("rpkid", "irdbd", "pubd"): username = cfg.get("%s_sql_username" % name, name[:4]) password = cfg.get("%s_sql_password" % name, "fnord") + + schema = [] + for line in getattr(rpki.sql_schemas, name, "").splitlines(): + schema.extend(line.partition("--")[0].split()) + schema = " ".join(schema).strip(";").split(";") + schema = [statement.strip() for statement in schema if statement and "DROP TABLE" not in statement] for i in xrange(12): - subprocess.check_call( - ("mysql", "-u", username, "-p" + password, "%s%d" % (name[:4], i)), - stdin = open("../%s.sql" % name)) + + database = "%s%d" % (name[:4], i) + + db = MySQLdb.connect(user = username, db = database, passwd = password) + cur = db.cursor() + + cur.execute("SHOW TABLES") + tables = [r[0] for r in cur.fetchall()] + + cur.execute("SET foreign_key_checks = 0") + for table in tables: + cur.execute("DROP TABLE %s" % table) + cur.execute("SET foreign_key_checks = 1") + + for statement in schema: + cur.execute(statement) + + cur.close() + db.close() diff --git a/rpkid/tests/yamltest-test-all.sh b/rpkid/tests/yamltest-test-all.sh index f6a05237..46f3c59e 100644 --- a/rpkid/tests/yamltest-test-all.sh +++ b/rpkid/tests/yamltest-test-all.sh @@ -1,7 +1,7 @@ #!/bin/sh - # $Id$ -# Copyright (C) 2009-2010 Internet Systems Consortium ("ISC") +# Copyright (C) 2009-2012 Internet Systems Consortium ("ISC") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above @@ -17,18 +17,18 @@ set -x -export TZ=UTC MYRPKI_RNG=$(pwd)/myrpki.rng +export TZ=UTC test -z "$STY" && exec screen -L sh $0 screen -X split screen -X focus -runtime=$((30 * 60)) +: ${runtime=900} for yaml in smoketest.*.yaml do - rm -rf test + rm -rf test rcynic-data python sql-cleaner.py screen python yamltest.py -p yamltest.pid $yaml now=$(date +%s) @@ -42,9 +42,13 @@ do date ../../rcynic/rcynic ../../rcynic/show.sh + ../../utils/scan_roas/scan_roas rcynic-data/authenticated date done - test -r yamltest.pid && kill -INT $(cat yamltest.pid) - sleep 30 + if test -r yamltest.pid + then + kill -INT $(cat yamltest.pid) + sleep 30 + fi make backup done diff --git a/rpkid/tests/yamltest.py b/rpkid/tests/yamltest.py index ecd00af2..2d7e90d6 100644 --- a/rpkid/tests/yamltest.py +++ b/rpkid/tests/yamltest.py @@ -1,6 +1,6 @@ """ Test framework, using the same YAML test description format as -smoketest.py, but using the myrpki.py tool to do all the back-end +smoketest.py, but using the rpkic.py tool to do all the back-end work. Reads YAML file, generates .csv and .conf files, runs daemons and waits for one of them to exit. @@ -10,7 +10,7 @@ Still to do: - Implement smoketest.py-style delta actions, that is, modify the allocation database under control of the YAML file, dump out new - .csv files, and run myrpki.py again to feed resulting changes into + .csv files, and run rpkic.py again to feed resulting changes into running daemons. $Id$ @@ -46,7 +46,8 @@ PERFORMANCE OF THIS SOFTWARE. """ import subprocess, re, os, getopt, sys, yaml, signal, time -import rpki.resource_set, rpki.sundial, rpki.config, rpki.log, rpki.myrpki +import rpki.resource_set, rpki.sundial, rpki.config, rpki.log +import rpki.csv_utils, rpki.x509 # Nasty regular expressions for parsing config files. Sadly, while # the Python ConfigParser supports writing config files, it does so in @@ -67,15 +68,11 @@ this_dir = os.getcwd() test_dir = cleanpath(this_dir, "yamltest.dir") rpkid_dir = cleanpath(this_dir, "..") -prog_myrpki = cleanpath(rpkid_dir, "myrpki.py") -prog_rpkid = cleanpath(rpkid_dir, "rpkid.py") -prog_irdbd = cleanpath(rpkid_dir, "irdbd.py") -prog_pubd = cleanpath(rpkid_dir, "pubd.py") -prog_rootd = cleanpath(rpkid_dir, "rootd.py") - -prog_openssl = cleanpath(this_dir, "../../openssl/openssl/apps/openssl") -if not os.path.exists(prog_openssl): - prog_openssl = "openssl" +prog_rpkic = cleanpath(rpkid_dir, "rpkic") +prog_rpkid = cleanpath(rpkid_dir, "rpkid") +prog_irdbd = cleanpath(rpkid_dir, "irdbd") +prog_pubd = cleanpath(rpkid_dir, "pubd") +prog_rootd = cleanpath(rpkid_dir, "rootd") class roa_request(object): """ @@ -116,14 +113,14 @@ class allocation_db(list): def __init__(self, yaml): list.__init__(self) self.root = allocation(yaml, self) - assert self.root.is_root() + assert self.root.is_root if self.root.crl_interval is None: self.root.crl_interval = 24 * 60 * 60 if self.root.regen_margin is None: self.root.regen_margin = 24 * 60 * 60 for a in self: if a.sia_base is None: - if a.runs_pubd(): + if a.runs_pubd: base = "rsync://localhost:%d/rpki/" % a.rsync_port else: base = a.parent.sia_base @@ -134,14 +131,13 @@ class allocation_db(list): a.crl_interval = a.parent.crl_interval if a.regen_margin is None: a.regen_margin = a.parent.regen_margin - a.client_handle = "/".join(a.sia_base.rstrip("/").split("/")[3:]) self.root.closure() self.map = dict((a.name, a) for a in self) for a in self: - if a.is_hosted(): + if a.is_hosted: a.hosted_by = self.map[a.hosted_by] a.hosted_by.hosts.append(a) - assert not a.is_root() and not a.hosted_by.is_hosted() + assert not a.is_root and not a.hosted_by.is_hosted def dump(self): """ @@ -154,12 +150,12 @@ class allocation_db(list): class allocation(object): """ One entity in our allocation database. Every entity in the database - is assumed to hold resources, so needs at least myrpki services. + is assumed to hold resources, so needs at least rpkic services. Entities that don't have the hosted_by property run their own copies of rpkid, irdbd, and pubd, so they also need myirbe services. """ - base_port = 4400 + base_port = None parent = None crl_interval = None regen_margin = None @@ -218,14 +214,14 @@ class allocation(object): self.base.v6 = self.base.v6.union(r.v6.to_resource_set()) self.hosted_by = yaml.get("hosted_by") self.hosts = [] - if not self.is_hosted(): + if not self.is_hosted: self.engine = self.allocate_engine() self.rpkid_port = self.allocate_port() self.irdbd_port = self.allocate_port() - if self.runs_pubd(): + if self.runs_pubd: self.pubd_port = self.allocate_port() self.rsync_port = self.allocate_port() - if self.is_root(): + if self.is_root: self.rootd_port = self.allocate_port() def closure(self): @@ -253,55 +249,56 @@ class allocation(object): if self.kids: s += " Kids: %s\n" % ", ".join(k.name for k in self.kids) if self.parent: s += " Up: %s\n" % self.parent.name if self.sia_base: s += " SIA: %s\n" % self.sia_base - if self.is_hosted(): s += " Host: %s\n" % self.hosted_by.name + if self.is_hosted: s += " Host: %s\n" % self.hosted_by.name if self.hosts: s += " Hosts: %s\n" % ", ".join(h.name for h in self.hosts) for r in self.roa_requests: s += " ROA: %s\n" % r - if not self.is_hosted(): s += " IPort: %s\n" % self.irdbd_port - if self.runs_pubd(): s += " PPort: %s\n" % self.pubd_port - if not self.is_hosted(): s += " RPort: %s\n" % self.rpkid_port - if self.runs_pubd(): s += " SPort: %s\n" % self.rsync_port - if self.is_root(): s += " TPort: %s\n" % self.rootd_port + if not self.is_hosted: s += " IPort: %s\n" % self.irdbd_port + if self.runs_pubd: s += " PPort: %s\n" % self.pubd_port + if not self.is_hosted: s += " RPort: %s\n" % self.rpkid_port + if self.runs_pubd: s += " SPort: %s\n" % self.rsync_port + if self.is_root: s += " TPort: %s\n" % self.rootd_port return s + " Until: %s\n" % self.resources.valid_until + @property def is_root(self): """ Is this the root node? """ return self.parent is None + @property def is_hosted(self): """ Is this entity hosted? """ return self.hosted_by is not None + @property def runs_pubd(self): """ Does this entity run a pubd? """ - return self.is_root() or not (self.is_hosted() or only_one_pubd) + return self.is_root or not (self.is_hosted or only_one_pubd) def path(self, *names): """ Construct pathnames in this entity's test directory. """ - return cleanpath(test_dir, self.name, *names) + return cleanpath(test_dir, self.host.name, *names) def csvout(self, fn): """ - Open and log a CSV output file. We use delimiter and dialect - settings imported from the myrpki module, so that we automatically - write CSV files in the right format. + Open and log a CSV output file. """ path = self.path(fn) print "Writing", path - return rpki.myrpki.csv_writer(path) + return rpki.csv_utils.csv_writer(path) def up_down_url(self): """ Construct service URL for this node's parent. """ - parent_port = self.parent.hosted_by.rpkid_port if self.parent.is_hosted() else self.parent.rpkid_port + parent_port = self.parent.hosted_by.rpkid_port if self.parent.is_hosted else self.parent.rpkid_port return "http://localhost:%d/up-down/%s/%s" % (parent_port, self.parent.name, self.name) def dump_asns(self, fn): @@ -312,37 +309,7 @@ class allocation(object): for k in self.kids: f.writerows((k.name, a) for a in k.resources.asn) f.close() - - def dump_children(self, fn): - """ - Write children CSV file. - """ - f = self.csvout(fn) - f.writerows((k.name, k.resources.valid_until, k.path("bpki/resources/ca.cer")) - for k in self.kids) - f.close() - - def dump_parents(self, fn): - """ - Write parents CSV file. - """ - f = self.csvout(fn) - if self.is_root(): - f.writerow(("rootd", - "http://localhost:%d/" % self.rootd_port, - self.path("bpki/servers/ca.cer"), - self.path("bpki/servers/ca.cer"), - self.name, - self.sia_base)) - else: - parent_host = self.parent.hosted_by if self.parent.is_hosted() else self.parent - f.writerow((self.parent.name, - self.up_down_url(), - self.parent.path("bpki/resources/ca.cer"), - parent_host.path("bpki/servers/ca.cer"), - self.name, - self.sia_base)) - f.close() + self.run_rpkic("load_asns", fn) def dump_prefixes(self, fn): """ @@ -352,43 +319,45 @@ class allocation(object): for k in self.kids: f.writerows((k.name, p) for p in (k.resources.v4 + k.resources.v6)) f.close() + self.run_rpkic("load_prefixes", fn) def dump_roas(self, fn): """ Write ROA CSV file. """ - group = self.name if self.is_root() else self.parent.name + group = self.name if self.is_root else self.parent.name f = self.csvout(fn) for r in self.roa_requests: f.writerows((p, r.asn, group) for p in (r.v4 + r.v6 if r.v4 and r.v6 else r.v4 or r.v6 or ())) f.close() + self.run_rpkic("load_roa_requests", fn) - def dump_clients(self, fn, db): - """ - Write pubclients CSV file. - """ - if self.runs_pubd(): - f = self.csvout(fn) - f.writerows((s.client_handle, s.path("bpki/resources/ca.cer"), s.sia_base) - for s in (db if only_one_pubd else [self] + self.kids)) - f.close() - - def find_pubd(self): + @property + def pubd(self): """ Walk up tree until we find somebody who runs pubd. """ s = self - path = [s] - while not s.runs_pubd(): + while not s.runs_pubd: s = s.parent - path.append(s) - return s, ".".join(i.name for i in reversed(path)) + return s - def find_host(self): + @property + def client_handle(self): """ - Figure out who hosts this entity. + Work out what pubd configure_publication_client will call us. """ + path = [] + s = self + while not s.runs_pubd: + path.append(s) + s = s.parent + path.append(s) + return ".".join(i.name for i in reversed(path)) + + @property + def host(self): return self.hosted_by or self def dump_conf(self, fn): @@ -396,13 +365,10 @@ class allocation(object): Write configuration file for OpenSSL and RPKI tools. """ - s, ignored = self.find_pubd() - r = { "handle" : self.name, - "run_rpkid" : str(not self.is_hosted()), - "run_pubd" : str(self.runs_pubd()), - "run_rootd" : str(self.is_root()), - "openssl" : prog_openssl, + "run_rpkid" : str(not self.is_hosted), + "run_pubd" : str(self.runs_pubd), + "run_rootd" : str(self.is_root), "irdbd_sql_database" : "irdb%d" % self.engine, "irdbd_sql_username" : "irdb", "rpkid_sql_database" : "rpki%d" % self.engine, @@ -415,8 +381,8 @@ class allocation(object): "pubd_sql_database" : "pubd%d" % self.engine, "pubd_sql_username" : "pubd", "pubd_server_host" : "localhost", - "pubd_server_port" : str(s.pubd_port), - "publication_rsync_server" : "localhost:%s" % s.rsync_port } + "pubd_server_port" : str(self.pubd.pubd_port), + "publication_rsync_server" : "localhost:%s" % self.pubd.rsync_port } r.update(config_overrides) @@ -442,7 +408,7 @@ class allocation(object): Write rsyncd configuration file. """ - if self.runs_pubd(): + if self.runs_pubd: f = open(self.path(fn), "w") print "Writing", f.name f.writelines(s + "\n" for s in @@ -457,40 +423,26 @@ class allocation(object): "comment = RPKI test")) f.close() - def run_configure_daemons(self): + def run_rpkic(self, *args): """ - Run configure_daemons if this entity is not hosted by another engine. + Run rpkic for this entity. """ - if self.is_hosted(): - print "%s is hosted, skipping configure_daemons" % self.name - else: - files = [h.path("myrpki.xml") for h in self.hosts] - self.run_myrpki("configure_daemons", *[f for f in files if os.path.exists(f)]) - - def run_configure_resources(self): - """ - Run configure_resources for this entity. - """ - self.run_myrpki("configure_resources") - - def run_myrpki(self, *args): - """ - Run myrpki.py for this entity. - """ - print 'Running "%s" for %s' % (" ".join(("myrpki",) + args), self.name) - subprocess.check_call((sys.executable, prog_myrpki) + args, cwd = self.path()) + cmd = (prog_rpkic, "-i", self.name, "-c", self.path("rpki.conf")) + args + print 'Running "%s"' % " ".join(cmd) + subprocess.check_call(cmd, cwd = self.host.path()) def run_python_daemon(self, prog): """ Start a Python daemon and return a subprocess.Popen object representing the running daemon. """ - basename = os.path.basename(prog) - p = subprocess.Popen((sys.executable, prog, "-d", "-c", self.path("rpki.conf")), + cmd = (prog, "-d", "-c", self.path("rpki.conf")) + log = os.path.splitext(os.path.basename(prog))[0] + ".log" + p = subprocess.Popen(cmd, cwd = self.path(), - stdout = open(self.path(os.path.splitext(basename)[0] + ".log"), "w"), + stdout = open(self.path(log), "w"), stderr = subprocess.STDOUT) - print "Running %s for %s: pid %d process %r" % (basename, self.name, p.pid, p) + print 'Running %s for %s: pid %d process %r' % (" ".join(cmd), self.name, p.pid, p) return p def run_rpkid(self): @@ -526,18 +478,6 @@ class allocation(object): print "Running rsyncd for %s: pid %d process %r" % (self.name, p.pid, p) return p - def run_openssl(self, *args, **kwargs): - """ - Run OpenSSL - """ - env = { "PATH" : os.environ["PATH"], - "BPKI_DIRECTORY" : self.path("bpki/servers"), - "OPENSSL_CONF" : "/dev/null", - "RANDFILE" : ".OpenSSL.whines.unless.I.set.this" } - env.update(kwargs) - subprocess.check_call((prog_openssl,) + args, cwd = self.path(), env = env) - - os.environ["TZ"] = "UTC" time.tzset() @@ -580,7 +520,7 @@ try: cfg = rpki.config.parser(cfg_file, "yamltest", allow_missing = True) only_one_pubd = cfg.getboolean("only_one_pubd", True) - prog_openssl = cfg.get("openssl", prog_openssl) + allocation.base_port = cfg.getint("base_port", 4400) config_overrides = dict( (k, cfg.get(k)) @@ -604,45 +544,61 @@ try: # Show what we loaded - db.dump() + #db.dump() # Set up each entity in our test for d in db: - os.makedirs(d.path()) - d.dump_asns("asns.csv") - d.dump_prefixes("prefixes.csv") - d.dump_roas("roas.csv") - d.dump_conf("rpki.conf") - d.dump_rsyncd("rsyncd.conf") - if False: - d.dump_children("children.csv") - d.dump_parents("parents.csv") - d.dump_clients("pubclients.csv", db) + if not d.is_hosted: + os.makedirs(d.path()) + os.makedirs(d.path("bpki/resources")) + os.makedirs(d.path("bpki/servers")) + d.dump_conf("rpki.conf") + if d.runs_pubd: + d.dump_rsyncd("rsyncd.conf") # Initialize BPKI and generate self-descriptor for each entity. for d in db: - d.run_myrpki("initialize") + d.run_rpkic("initialize") # Create publication directories. for d in db: - if d.is_root() or d.runs_pubd(): + if d.is_root or d.runs_pubd: os.makedirs(d.path("publication")) # Create RPKI root certificate. print "Creating rootd RPKI root certificate" - # Should use req -subj here to set subject name. Later. - db.root.run_openssl("x509", "-req", "-sha256", "-outform", "DER", - "-signkey", "bpki/servers/ca.key", - "-in", "bpki/servers/ca.req", - "-out", "publication/root.cer", - "-extfile", "rpki.conf", - "-extensions", "rootd_x509_extensions") + root_resources = rpki.resource_set.resource_bag( + asn = rpki.resource_set.resource_set_as("0-4294967295"), + v4 = rpki.resource_set.resource_set_ipv4("0.0.0.0/0"), + v6 = rpki.resource_set.resource_set_ipv6("::/0")) + + root_key = rpki.x509.RSA.generate() + + root_uri = "rsync://localhost:%d/rpki/" % db.root.pubd.rsync_port + + root_sia = ((rpki.oids.name2oid["id-ad-caRepository"], ("uri", root_uri)), + (rpki.oids.name2oid["id-ad-rpkiManifest"], ("uri", root_uri + "root.mnf"))) + + root_cert = rpki.x509.X509.self_certify( + keypair = root_key, + subject_key = root_key.get_RSApublic(), + serial = 1, + sia = root_sia, + notAfter = rpki.sundial.now() + rpki.sundial.timedelta(days = 365), + resources = root_resources) + f = open(db.root.path("publication/root.cer"), "wb") + f.write(root_cert.get_DER()) + f.close() + + f = open(db.root.path("bpki/servers/root.key"), "wb") + f.write(root_key.get_DER()) + f.close() # From here on we need to pay attention to initialization order. We # used to do all the pre-configure_daemons stuff before running any @@ -659,62 +615,62 @@ try: print print "Configuring", d.name print - if d.is_root(): - d.run_myrpki("configure_publication_client", d.path("entitydb", "repositories", "%s.xml" % d.name)) + if d.is_root: + assert not d.is_hosted + d.run_rpkic("configure_publication_client", + d.path("%s.%s.repository-request.xml" % (d.name, d.name))) print - d.run_myrpki("configure_repository", d.path("entitydb", "pubclients", "%s.xml" % d.name)) + d.run_rpkic("configure_repository", + d.path("%s.repository-response.xml" % d.client_handle)) print else: - d.parent.run_myrpki("configure_child", d.path("entitydb", "identity.xml")) + d.parent.run_rpkic("configure_child", d.path("%s.identity.xml" % d.name)) print - d.run_myrpki("configure_parent", d.parent.path("entitydb", "children", "%s.xml" % d.name)) + d.run_rpkic("configure_parent", + d.parent.path("%s.%s.parent-response.xml" % (d.parent.name, d.name))) print - publisher, path = d.find_pubd() - publisher.run_myrpki("configure_publication_client", d.path("entitydb", "repositories", "%s.xml" % d.parent.name)) + d.pubd.run_rpkic("configure_publication_client", + d.path("%s.%s.repository-request.xml" % (d.name, d.parent.name))) print - d.run_myrpki("configure_repository", publisher.path("entitydb", "pubclients", "%s.xml" % path)) + d.run_rpkic("configure_repository", + d.pubd.path("%s.repository-response.xml" % d.client_handle)) print - parent_host = d.parent.find_host() - if d.parent is not parent_host: - d.parent.run_configure_resources() - print - parent_host.run_configure_daemons() + d.parent.run_rpkic("synchronize") print - if publisher is not parent_host: - publisher.run_configure_daemons() + if d.pubd is not d.parent.host: + d.pubd.run_rpkic("synchronize") print print "Running daemons for", d.name - if d.is_root(): + if d.is_root: progs.append(d.run_rootd()) - if not d.is_hosted(): + if not d.is_hosted: progs.append(d.run_irdbd()) progs.append(d.run_rpkid()) - if d.runs_pubd(): + if d.runs_pubd: progs.append(d.run_pubd()) progs.append(d.run_rsyncd()) - if d.is_root() or not d.is_hosted() or d.runs_pubd(): + if d.is_root or not d.is_hosted or d.runs_pubd: print "Giving", d.name, "daemons time to start up" time.sleep(20) print assert all(p.poll() is None for p in progs) - # Run configure_daemons to set up IRDB and RPKI objects. Need to - # run a second time to push BSC certs out to rpkid. Nothing - # should happen on the third pass. Oops, when hosting we need to - # run configure_resources between passes, since only the hosted - # entity can issue the BSC, etc. + # In theory we now only need to synchronize the new entity once. + d.run_rpkic("synchronize") + # Run through list again, to be sure we catch hosted cases. + # In theory this is no longer necessary. + if False: for i in xrange(3): - d.run_configure_resources() - d.find_host().run_configure_daemons() + for d in db: + d.run_rpkic("synchronize") - # Run through list again, to be sure we catch hosted cases - - for i in xrange(3): - for d in db: - d.run_configure_resources() - d.run_configure_daemons() + # Load all the CSV files + for d in db: + d.dump_asns("%s.asns.csv" % d.name) + d.dump_prefixes("%s.prefixes.csv" % d.name) + d.dump_roas("%s.roas.csv" % d.name) print "Done initializing daemons" diff --git a/scripts/convert-from-entitydb-to-sql.py b/scripts/convert-from-entitydb-to-sql.py new file mode 100644 index 00000000..95e8e985 --- /dev/null +++ b/scripts/convert-from-entitydb-to-sql.py @@ -0,0 +1,440 @@ +""" +Merge XML entitydb and OpenSSL command-line BPKI into SQL IRDB. + +This is a work in progress, don't use it unless you really know what +you're doing. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import sys, os, time, getopt, glob, subprocess, base64 +import rpki.config, rpki.x509, rpki.relaxng, rpki.sundial +from rpki.mysql_import import MySQLdb +from lxml.etree import ElementTree + +if os.getlogin() != "sra": + sys.exit("I //said// this was a work in progress") + +cfg_file = "rpki.conf" +entitydb = "entitydb" +bpki = "bpki" +copy_csv_data = True + +opts, argv = getopt.getopt(sys.argv[1:], "c:h?", ["config=", "help"]) +for o, a in opts: + if o in ("-h", "--help", "-?"): + print __doc__ + sys.exit(0) + if o in ("-c", "--config"): + cfg_file = a +if argv: + sys.exit("Unexpected arguments %s" % argv) + +cfg = rpki.config.parser(cfg_file) + +sql_database = cfg.get("sql-database", section = "irdbd") +sql_username = cfg.get("sql-username", section = "irdbd") +sql_password = cfg.get("sql-password", section = "irdbd") + +db = MySQLdb.connect(user = sql_username, db = sql_database, passwd = sql_password) +cur = db.cursor() + +# Configure the Django model system + +from django.conf import settings + +settings.configure( + DATABASES = { "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : sql_database, + "USER" : sql_username, + "PASSWORD" : sql_password, + "HOST" : "", + "PORT" : "", + "OPTIONS" : { "init_command": "SET storage_engine=INNODB" }}}, + INSTALLED_APPS = ("rpki.irdb",), +) + +import rpki.irdb + +# Create the model-based tables if they don't already exist + +import django.core.management + +django.core.management.call_command("syncdb", verbosity = 4, load_initial_data = False) + +# From here down will be an awful lot of messing about with XML and +# X.509 data, extracting stuff from the old SQL database and whacking +# it into the new. Still working out these bits. + +xmlns = "{http://www.hactrn.net/uris/rpki/myrpki/}" + +tag_authorization = xmlns + "authorization" +tag_bpki_child_ta = xmlns + "bpki_child_ta" +tag_bpki_client_ta = xmlns + "bpki_client_ta" +tag_bpki_resource_ta = xmlns + "bpki_resource_ta" +tag_bpki_server_ta = xmlns + "bpki_server_ta" +tag_bpki_ta = xmlns + "bpki_ta" +tag_contact_info = xmlns + "contact_info" +tag_identity = xmlns + "identity" +tag_parent = xmlns + "parent" +tag_repository = xmlns + "repository" + +e = ElementTree(file = os.path.join(entitydb, "identity.xml")).getroot() +rpki.relaxng.myrpki.assertValid(e) +assert e.tag == tag_identity + +self_handle = e.get("handle") +assert self_handle == cfg.get("handle", section = "myrpki") + +# Some BPKI utillity routines + +def read_openssl_serial(filename): + f = open(filename, "r") + text = f.read() + f.close() + return int(text.strip(), 16) + +def get_or_create_ServerEE(issuer, purpose): + cer = rpki.x509.X509(Auto_file = os.path.join(bpki, "servers", purpose + ".cer")) + key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "servers", purpose + ".key")) + rpki.irdb.ServerEE.objects.get_or_create( + issuer = issuer, + purpose = purpose, + certificate = cer, + private_key = key) + +# Load BPKI CAs and directly certified EEs + +cer = rpki.x509.X509(Auto_file = os.path.join(bpki, "resources", "ca.cer")) +key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "resources", "ca.key")) +crl = rpki.x509.CRL(Auto_file = os.path.join(bpki, "resources", "ca.crl")) +serial = read_openssl_serial(os.path.join(bpki, "resources", "serial")) +crl_number = read_openssl_serial(os.path.join(bpki, "resources", "crl_number")) + +resource_ca = rpki.irdb.ResourceHolderCA.objects.get_or_create( + handle = self_handle, + certificate = cer, + private_key = key, + latest_crl = crl, + next_serial = serial, + next_crl_number = crl_number, + last_crl_update = crl.getThisUpdate().to_sql(), + next_crl_update = crl.getNextUpdate().to_sql())[0] + +if os.path.exists(os.path.join(bpki, "resources", "referral.cer")): + cer = rpki.x509.X509(Auto_file = os.path.join(bpki, "resources", "referral.cer")) + key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "resources", "referral.key")) + rpki.irdb.Referral.objects.get_or_create( + issuer = resource_ca, + certificate = cer, + private_key = key) + +run_rpkid = cfg.getboolean("run_rpkid", section = "myrpki") +run_pubd = cfg.getboolean("run_pubd", section = "myrpki") +run_rootd = cfg.getboolean("run_rootd", section = "myrpki") + +if run_rpkid or run_pubd: + cer = rpki.x509.X509(Auto_file = os.path.join(bpki, "servers", "ca.cer")) + key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "servers", "ca.key")) + crl = rpki.x509.CRL(Auto_file = os.path.join(bpki, "servers", "ca.crl")) + serial = read_openssl_serial(os.path.join(bpki, "servers", "serial")) + crl_number = read_openssl_serial(os.path.join(bpki, "servers", "crl_number")) + server_ca = rpki.irdb.ServerCA.objects.get_or_create( + certificate = cer, + private_key = key, + latest_crl = crl, + next_serial = serial, + next_crl_number = crl_number, + last_crl_update = crl.getThisUpdate().to_sql(), + next_crl_update = crl.getNextUpdate().to_sql())[0] + get_or_create_ServerEE(server_ca, "irbe") + +else: + server_ca = None + +if run_rpkid: + get_or_create_ServerEE(server_ca, "rpkid") + get_or_create_ServerEE(server_ca, "irdbd") + +if run_pubd: + get_or_create_ServerEE(server_ca, "pubd") + +# Certification model for rootd has changed. We can reuse the old +# key, but we have to recertify under a different CA than previously. +# Yes, we're pulling a key from the servers BPKI tree and certifying +# it under the resource holder CA, that's part of the change. + +if run_rootd: + rpki.irdb.Rootd.objects.get_or_certify( + issuer = resource_ca, + service_uri = "http://localhost:%s/" % cfg.get("rootd_server_port", section = "myrpki"), + private_key = rpki.x509.RSA(Auto_file = os.path.join(bpki, "servers", "rootd.key"))) + +# Load BSC certificates and requests. Yes, this currently wires in +# exactly one BSC handle, "bsc". So does the old myrpki code. Ick. + +for fn in glob.iglob(os.path.join(bpki, "resources", "bsc.*.cer")): + rpki.irdb.BSC.objects.get_or_create( + issuer = resource_ca, + handle = "bsc", + certificate = rpki.x509.X509(Auto_file = fn), + pkcs10 = rpki.x509.PKCS10(Auto_file = fn[:-4] + ".req")) + +def xcert_hash(cert): + """ + Generate the filename hash that myrpki would have generated for a + cross-certification. This is nasty, don't look. + """ + + cmd1 = ("openssl", "x509", "-noout", "-pubkey", "-subject") + cmd2 = ("openssl", "dgst", "-md5") + + env = { "PATH" : os.environ["PATH"], "OPENSSL_CONF" : "/dev/null" } + p1 = subprocess.Popen(cmd1, env = env, stdin = subprocess.PIPE, stdout = subprocess.PIPE) + p2 = subprocess.Popen(cmd2, env = env, stdin = p1.stdout, stdout = subprocess.PIPE) + p1.stdin.write(cert.get_PEM()) + p1.stdin.close() + hash = p2.stdout.read() + if p1.wait() != 0: + raise subprocess.CalledProcessError(returncode = p1.returncode, cmd = cmd1) + if p2.wait() != 0: + raise subprocess.CalledProcessError(returncode = p2.returncode, cmd = cmd2) + + hash = "".join(hash.split()) + if hash.startswith("(stdin)="): + hash = hash[len("(stdin)="):] + return hash + +# Let's try keeping track of all the xcert filenames we use, so we can +# list the ones we didn't. + +xcert_filenames = set(glob.iglob(os.path.join(bpki, "*", "xcert.*.cer"))) + +# Scrape child data out of the entitydb. + +for filename in glob.iglob(os.path.join(entitydb, "children", "*.xml")): + child_handle = os.path.splitext(os.path.split(filename)[1])[0] + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + assert e.tag == tag_parent + + ta = rpki.x509.X509(Base64 = e.findtext(tag_bpki_child_ta)) + xcfn = os.path.join(bpki, "resources", "xcert.%s.cer" % xcert_hash(ta)) + xcert_filenames.discard(xcfn) + xcert = rpki.x509.X509(Auto_file = xcfn) + + cur.execute(""" + SELECT registrant_id, valid_until FROM registrant + WHERE registry_handle = %s AND registrant_handle = %s + """, (self_handle, child_handle)) + assert cur.rowcount == 1 + registrant_id, valid_until = cur.fetchone() + + valid_until = rpki.sundial.datetime.fromdatetime(valid_until) + if valid_until != rpki.sundial.datetime.fromXMLtime(e.get("valid_until")): + print "WARNING: valid_until dates in XML and SQL do not match for child", child_handle + print " SQL:", str(valid_until) + print " XML:", str(rpki.sundial.datetime.fromXMLtime(e.get("valid_until"))) + print "Blundering onwards" + + child = rpki.irdb.Child.objects.get_or_create( + handle = child_handle, + valid_until = valid_until.to_sql(), + ta = ta, + certificate = xcert, + issuer = resource_ca)[0] + + if copy_csv_data: + + cur.execute(""" + SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s + """, (registrant_id,)) + for start_as, end_as in cur.fetchall(): + rpki.irdb.ChildASN.objects.get_or_create( + start_as = start_as, + end_as = end_as, + child = child) + + cur.execute(""" + SELECT start_ip, end_ip, version FROM registrant_net WHERE registrant_id = %s + """, (registrant_id,)) + for start_ip, end_ip, version in cur.fetchall(): + rpki.irdb.ChildNet.objects.get_or_create( + start_ip = start_ip, + end_ip = end_ip, + version = version, + child = child) + +# Scrape parent data out of the entitydb. + +for filename in glob.iglob(os.path.join(entitydb, "parents", "*.xml")): + parent_handle = os.path.splitext(os.path.split(filename)[1])[0] + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + assert e.tag == tag_parent + + if parent_handle == self_handle: + assert run_rootd + assert e.get("service_uri") == "http://localhost:%s/" % cfg.get("rootd_server_port", section = "myrpki") + continue + + ta = rpki.x509.X509(Base64 = e.findtext(tag_bpki_resource_ta)) + xcfn = os.path.join(bpki, "resources", "xcert.%s.cer" % xcert_hash(ta)) + xcert_filenames.discard(xcfn) + xcert = rpki.x509.X509(Auto_file = xcfn) + + r = e.find(tag_repository) + repository_type = r.get("type") + if repository_type == "referral": + a = r.find(tag_authorization) + referrer = a.get("referrer") + referral_authorization = base64.b64decode(a.text) + else: + referrer = None + referral_authorization = None + + parent = rpki.irdb.Parent.objects.get_or_create( + handle = parent_handle, + parent_handle = e.get("parent_handle"), + child_handle = e.get("child_handle"), + ta = ta, + certificate = xcert, + service_uri = e.get("service_uri"), + repository_type = repository_type, + referrer = referrer, + referral_authorization = referral_authorization, + issuer = resource_ca)[0] + + # While we have the parent object in hand, load any Ghostbuster + # entries specific to this parent. + + if copy_csv_data: + cur.execute(""" + SELECT vcard FROM ghostbuster_request + WHERE self_handle = %s AND parent_handle = %s + """, (self_handle, parent_handle)) + for row in cur.fetchall(): + rpki.irdb.GhostbusterRequest.objects.get_or_create( + issuer = resource_ca, + parent = parent, + vcard = row[0]) + +# Scrape repository data out of the entitydb. + +for filename in glob.iglob(os.path.join(entitydb, "repositories", "*.xml")): + repository_handle = os.path.splitext(os.path.split(filename)[1])[0] + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + assert e.tag == tag_repository + + if e.get("type") != "confirmed": + continue + + ta = rpki.x509.X509(Base64 = e.findtext(tag_bpki_server_ta)) + xcfn = os.path.join(bpki, "resources", "xcert.%s.cer" % xcert_hash(ta)) + xcert_filenames.discard(xcfn) + xcert = rpki.x509.X509(Auto_file = xcfn) + + parent_handle = e.get("parent_handle") + if parent_handle == self_handle: + turtle = resource_ca.rootd + else: + turtle = rpki.irdb.Parent.objects.get(handle = parent_handle, issuer = resource_ca) + + rpki.irdb.Repository.objects.get_or_create( + handle = repository_handle, + client_handle = e.get("client_handle"), + ta = ta, + certificate = xcert, + service_uri = e.get("service_uri"), + sia_base = e.get("sia_base"), + turtle = turtle, + issuer = resource_ca) + +# Scrape client data out of the entitydb. + +for filename in glob.iglob(os.path.join(entitydb, "pubclients", "*.xml")): + client_handle = os.path.splitext(os.path.split(filename)[1])[0].replace(".", "/") + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + assert e.tag == tag_repository + + assert e.get("type") == "confirmed" + + ta = rpki.x509.X509(Base64 = e.findtext(tag_bpki_client_ta)) + xcfn = os.path.join(bpki, "servers", "xcert.%s.cer" % xcert_hash(ta)) + xcert_filenames.discard(xcfn) + xcert = rpki.x509.X509(Auto_file = xcfn) + + rpki.irdb.Client.objects.get_or_create( + handle = client_handle, + ta = ta, + certificate = xcert, + issuer = server_ca, + sia_base = e.get("sia_base")) + +if copy_csv_data: + + # Copy over any ROA requests + + cur.execute(""" + SELECT roa_request_id, asn FROM roa_request + WHERE roa_request_handle = %s + """, (self_handle,)) + for roa_request_id, asn in cur.fetchall(): + roa_request = rpki.irdb.ROARequest.objects.get_or_create(issuer = resource_ca, asn = asn)[0] + cur.execute(""" + SELECT prefix, prefixlen, max_prefixlen, version FROM roa_request_prefix + WHERE roa_request_id = %s + """, (roa_request_id,)) + for prefix, prefixlen, max_prefixlen, version in cur.fetchall(): + rpki.irdb.ROARequestPrefix.objects.get_or_create( + roa_request = roa_request, + version = version, + prefix = prefix, + prefixlen = prefixlen, + max_prefixlen = max_prefixlen) + + # Copy over any non-parent-specific Ghostbuster requests. + + cur.execute(""" + SELECT vcard FROM ghostbuster_request + WHERE self_handle = %s AND parent_handle IS NULL + """, (self_handle,)) + for row in cur.fetchall(): + rpki.irdb.GhostbusterRequest.objects.get_or_create( + issuer = resource_ca, + parent = None, + vcard = row[0]) + +# List cross certifications we didn't use. + +if False: + for filename in sorted(xcert_filenames): + cer = rpki.x509.X509(Auto_file = filename) + #print "Unused cross-certificate:", filename, cer.getSubject() + print "Unused cross-certificate:", filename, cer.get_POW().pprint() + +# Done! + +cur.close() +db.close() |