aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Elkins <melkins@tislabs.com>2011-06-09 20:07:11 +0000
committerMichael Elkins <melkins@tislabs.com>2011-06-09 20:07:11 +0000
commit949ff01f80f57cac773ec543d13fbf412ce27780 (patch)
treee0261799e20dc4106057777437cb29219b1b2e0e
parent48ee451dad61a7e3b4222f85037db7a8b63d6fa9 (diff)
add support for browing the rcynic cache
svn path=/rpkid/portal-gui/scripts/rpkigui-rcynic.py; revision=3859
-rw-r--r--rpkid/portal-gui/scripts/rpkigui-rcynic.py216
-rw-r--r--rpkid/rpki/gui/app/admin.py2
-rw-r--r--rpkid/rpki/gui/app/urls.py1
-rw-r--r--rpkid/rpki/gui/cacheview/__init__.py0
-rw-r--r--rpkid/rpki/gui/cacheview/admin.py47
-rw-r--r--rpkid/rpki/gui/cacheview/forms.py46
-rw-r--r--rpkid/rpki/gui/cacheview/misc.py33
-rw-r--r--rpkid/rpki/gui/cacheview/models.py177
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html17
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html10
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html84
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html13
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html21
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html1
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html22
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html17
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html31
-rw-r--r--rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html25
-rw-r--r--rpkid/rpki/gui/cacheview/tests.py23
-rw-r--r--rpkid/rpki/gui/cacheview/urls.py28
-rw-r--r--rpkid/rpki/gui/cacheview/views.py132
-rw-r--r--rpkid/rpki/gui/settings.py.in3
-rw-r--r--rpkid/rpki/gui/urls.py1
-rw-r--r--rpkid/setup.py5
24 files changed, 951 insertions, 4 deletions
diff --git a/rpkid/portal-gui/scripts/rpkigui-rcynic.py b/rpkid/portal-gui/scripts/rpkigui-rcynic.py
new file mode 100644
index 00000000..2069443d
--- /dev/null
+++ b/rpkid/portal-gui/scripts/rpkigui-rcynic.py
@@ -0,0 +1,216 @@
+# $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.
+#
+
+import os, sys, time
+os.environ['DJANGO_SETTINGS_MODULE'] = 'rpki.gui.settings'
+
+from rpki.gui.cacheview import models
+from rpki.rcynic import rcynic_xml_iterator
+from rpki.sundial import datetime
+import vobject
+
+debug = True
+
+def process_object(obj, model_class):
+ """
+ do initial processing on a rcynic_object instance.
+
+ 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' % (obj.__class__.__name__, obj.uri)
+
+ q = model_class.objects.filter(uri=obj.uri)
+ if not q:
+ if debug:
+ print 'creating new db instance'
+ inst = model_class(uri=obj.uri)
+ else:
+ inst = q[0]
+
+ inst.ok = obj.ok
+ inst.status = obj.status
+ inst.timestamp = datetime.fromXMLtime(obj.timestamp).to_sql()
+
+ mtime = os.stat(obj.filename)[8]
+ if mtime != inst.mtime:
+ inst.mtime = mtime
+ inst.not_before = obj.notBefore.to_sql()
+ inst.not_after = obj.notAfter.to_sql()
+
+ # look up signing cert
+ q = models.Cert.objects.filter(keyid=obj.aki)
+ 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 True, inst
+ elif debug:
+ print 'object is unchanged'
+
+ inst.save()
+
+ return False, inst
+
+def process_rescert(cert):
+ """
+ Process a RPKI resource certificate.
+ """
+
+ refresh, obj = process_object(cert, models.Cert)
+
+ if refresh:
+ obj.name = cert.subject
+ obj.keyid = cert.ski
+ obj.save()
+
+ # resources can change when a cert is updated
+ obj.asns.clear()
+ obj.addresses.clear()
+
+ for asr in cert.resources.asn:
+ if debug:
+ sys.stderr.write('processing %s\n' % asr)
+
+ q = models.ASRange.objects.filter(min=asr.min, max=asr.max)
+ if not q:
+ obj.asns.create(min=asr.min, max=asr.max)
+ else:
+ obj.asns.add(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)
+
+ minaddr = str(rng.min)
+ maxaddr = str(rng.max)
+ q = models.AddressRange.objects.filter(family=family, min=minaddr, max=maxaddr)
+ if not q:
+ obj.addresses.create(family=family, min=minaddr, max=maxaddr)
+ else:
+ obj.addresses.add(q[0])
+
+ if debug:
+ print 'finished processing rescert at %s' % cert.uri
+
+ return obj
+
+def process_ghostbuster(gbr):
+ refresh, obj = process_object(gbr, models.Ghostbuster)
+
+ if True:
+ #if refresh:
+ vcard = vobject.readOne(gbr.vcard)
+ if debug:
+ vcard.prettyPrint()
+ if hasattr(vcard, 'fn'):
+ obj.full_name = vcard.fn.value
+ if hasattr(vcard, 'email'):
+ obj.email_address = vcard.email.value
+ if hasattr(vcard, 'tel'):
+ obj.telephone = vcard.tel.value
+ if hasattr(vcard, 'org'):
+ obj.organization = vcard.org.value[0]
+ obj.save()
+
+fam_map = { 'roa_prefix_set_ipv6': 6, 'roa_prefix_set_ipv4': 4 }
+
+def process_roa(roa):
+ refresh, obj = process_object(roa, models.ROA)
+
+ if refresh:
+ 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])
+ else:
+ obj.save()
+
+ return obj
+
+def trydelete(seq):
+ """
+ iterate over a sequence and attempt to delete each item. safely
+ ignore IntegrityError since the object may be referenced elsewhere.
+ """
+ for o in seq:
+ try:
+ o.delete()
+ except IntegrityError:
+ pass
+
+def garbage_collect(ts):
+ """
+ rcynic's XML output file tells us what is currently in the cache,
+ but not what has been removed. we save the timestamp from the first
+ entry in the XML file, and remove all objects which are older.
+ """
+ if debug:
+ print 'doing garbage collection'
+
+ for roa in models.ROA.objects.filter(timestamp__lt=ts):
+ trydelete(roa.prefixes)
+ roa.delete()
+
+ for cert in models.Cert.objects.filter(timestamp__lt=ts):
+ trydelete(cert.asns)
+ trydelete(cert.addresses)
+ cert.delete()
+
+ for gbr in models.Ghostbuster.objects.filter(timestamp__lt=ts):
+ gbr.delete()
+
+dispatch = {
+ 'rcynic_certificate': process_rescert,
+ 'rcynic_roa' : process_roa,
+ 'rcynic_ghostbuster': process_ghostbuster
+}
+
+def process_cache(root='/var/rcynic/data', xml_file='/var/rcynic/data/rcynic.xml'):
+ start = time.time()
+
+ first = True
+ ts = datetime.now()
+ for obj in rcynic_xml_iterator(root, xml_file):
+ r = dispatch[obj.__class__.__name__](obj)
+ if first:
+ first = False
+ ts = r.timestamp
+ garbage_collect(ts)
+
+ if debug:
+ stop = time.time()
+ sys.stdout.write('elapsed time %d seconds.\n' % (stop - start))
+
+if __name__ == '__main__':
+ process_cache()
+
+# vim:sw=4 ts=8
diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py
index 8b7cd24a..52dc2c87 100644
--- a/rpkid/rpki/gui/app/admin.py
+++ b/rpkid/rpki/gui/app/admin.py
@@ -58,3 +58,5 @@ 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/urls.py b/rpkid/rpki/gui/app/urls.py
index 967baeca..050a03a7 100644
--- a/rpkid/rpki/gui/app/urls.py
+++ b/rpkid/rpki/gui/app/urls.py
@@ -16,7 +16,6 @@ PERFORMANCE OF THIS SOFTWARE.
"""
from django.conf.urls.defaults import *
-from django.views.generic.list_detail import object_list
from rpki.gui.app import views
urlpatterns = patterns('',
diff --git a/rpkid/rpki/gui/cacheview/__init__.py b/rpkid/rpki/gui/cacheview/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/__init__.py
diff --git a/rpkid/rpki/gui/cacheview/admin.py b/rpkid/rpki/gui/cacheview/admin.py
new file mode 100644
index 00000000..af8b31da
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/admin.py
@@ -0,0 +1,47 @@
+"""
+$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
+
+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)
+
+# vim:sw=4 ts=8
diff --git a/rpkid/rpki/gui/cacheview/forms.py b/rpkid/rpki/gui/cacheview/forms.py
new file mode 100644
index 00000000..cc33016e
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/forms.py
@@ -0,0 +1,46 @@
+# $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 import forms
+
+from rpki.gui.cacheview.misc import parse_ipaddr
+from rpki.exceptions import BadIPResource
+from rpki.resource_set import resource_range_as
+
+class SearchForm(forms.Form):
+ asn = forms.CharField(required=False, help_text='AS or range', label='AS')
+ addr = forms.CharField(required=False, max_length=40, help_text='range/CIDR', label='IP Address')
+
+ def clean(self):
+ asn = self.cleaned_data.get('asn')
+ addr = self.cleaned_data.get('addr')
+ if (asn and addr) or ((not asn) and (not addr)):
+ raise forms.ValidationError, 'Please specify either an AS or IP range, not both'
+
+ if asn:
+ try:
+ resource_range_as.parse_str(asn)
+ except ValueError:
+ raise forms.ValidationError, 'invalid AS range'
+
+ if addr:
+ #try:
+ parse_ipaddr(addr)
+ #except BadIPResource:
+ # raise forms.ValidationError, 'invalid IP address range/prefix'
+
+ return self.cleaned_data
diff --git a/rpkid/rpki/gui/cacheview/misc.py b/rpkid/rpki/gui/cacheview/misc.py
new file mode 100644
index 00000000..393055e2
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/misc.py
@@ -0,0 +1,33 @@
+"""
+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 rpki.resource_set import resource_range_ipv4, resource_range_ipv6
+from rpki.exceptions import BadIPResource
+
+def parse_ipaddr(s):
+ # resource_set functions only accept str
+ if isinstance(s, unicode):
+ s = s.encode()
+ s = s.strip()
+ r = resource_range_ipv4.parse_str(s)
+ try:
+ r = resource_range_ipv4.parse_str(s)
+ return 4, r
+ except BadIPResource:
+ r = resource_range_ipv6.parse_str(s)
+ return 6, r
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py
new file mode 100644
index 00000000..76314760
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/models.py
@@ -0,0 +1,177 @@
+"""
+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 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
+
+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()
+ max = models.IPAddressField()
+
+ class Meta:
+ ordering = ('family', 'min', 'max')
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.address_view', [str(self.pk)])
+
+ def __unicode__(self):
+ if self.min == self.max:
+ return u'%s' % self.min
+
+ try:
+ r = resource_range_ipv4.from_strings(self.min, self.max)
+ except ValueError:
+ 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')
+
+ def __unicode__(self):
+ if self.min == self.max:
+ return u'AS%d' % self.min
+ else:
+ return u'%s-%s' % (self.min, self.max)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.as_view', [str(self.pk)])
+
+class SignedObject(models.Model):
+ """
+ Abstract class to hold common metadata for all signed objects.
+ 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)
+ timestamp = models.DateTimeField()
+ ok = models.BooleanField()
+ status = models.CharField(max_length=255)
+
+ mtime = models.PositiveIntegerField(default=0)
+
+ # validity period from EE cert which signed object
+ not_before = models.DateTimeField()
+ not_after = models.DateTimeField()
+
+ class Meta:
+ abstract = True
+
+ def mtime_as_datetime(self):
+ """
+ convert the local timestamp to UTC and convert to a datetime object
+ """
+ return datetime.utcfromtimestamp(self.mtime + time.timezone)
+
+class Cert(SignedObject):
+ """
+ Object representing a resource certificate.
+ """
+ # SubjectName
+ name = models.CharField(max_length=255)
+
+ # value from the SKI extension
+ keyid = models.CharField(max_length=50, db_index=True)
+
+ 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)
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.cert_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ return u'%s' % self.name
+
+class ROAPrefix(models.Model):
+ family = models.PositiveIntegerField()
+ prefix = models.IPAddressField()
+ bits = models.PositiveIntegerField()
+ max_length = models.PositiveIntegerField()
+
+ class Meta:
+ ordering = ['family', 'prefix', 'bits', '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)
+
+class ROA(SignedObject):
+ asid = models.PositiveIntegerField()
+ prefixes = models.ManyToManyField(ROAPrefix, 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']
+
+ 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 Ghostbuster(SignedObject):
+ 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')
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('rpki.gui.cacheview.views.ghostbuster_detail', [str(self.pk)])
+
+ def __unicode__(self):
+ if self.full_name:
+ return self.full_name
+ if self.organization:
+ return self.organization
+ if self.email_address:
+ return self.email_address
+ return self.telephone
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html
new file mode 100644
index 00000000..069f4270
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/address_detail.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+<p>
+Range: {{ object.min }} - {{ object.max }}
+</p>
+
+<p>Covered by the following resource certs:</p>
+
+<ul>
+{% for cert in object.certs.all %}
+<li><a href="{{ cert.get_absolute_url }}">{{ cert.subject }}</a></li>
+{% endfor %}
+</ul>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html b/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html
new file mode 100644
index 00000000..1947401c
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/cacheview_base.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block sidebar %}
+<ul class='compact'>
+ <li><a href="/cacheview/search">resource search</a>
+ <li><a href="/cacheview/query">roa search</a>
+ <li><a href="/rpki/">rpki dashboard</a>
+</ul>
+{% endblock %}
+
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html
new file mode 100644
index 00000000..80921ac0
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/cert_detail.html
@@ -0,0 +1,84 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}
+Resource Certificate Detail
+{% endblock %}
+
+{% block detail %}
+
+<p>
+<table>
+ <tr><td>Subject</td><td>{{ object.name }}</td></tr>
+ <tr><td>SKI</td><td>{{ object.keyid }}</td></tr>
+</table>
+
+<h2>RFC3779 Resources</h2>
+
+<ul>
+
+<li>
+
+<h3>AS</h3>
+{% if object.asns.all %}
+<ul>
+{% for asn in object.asns.all %}
+<li>{{ asn }}
+{% endfor %}
+</ul>
+{% else %}
+<p>none</p>
+{% endif %}
+
+<li>
+<h2>IP Ranges</h2>
+
+{% if object.addresses.all %}
+<ul>
+{% for rng in object.addresses.all %}
+<li>{{ rng }}
+{% endfor %}
+</ul>
+{% else %}
+<p>none</p>
+{% endif %}
+</ul><!--resources-->
+
+<h2>Issued Objects</h2>
+<ul>
+
+ <li>
+<h3>Ghostbusters</h3>
+{% if object.ghostbusters.all %}
+<ul>
+{% for g in object.ghostbusters.all %}
+<li><a href="{{ g.get_absolute_url }}">{{ g }}</a>
+{% endfor %}
+</ul>
+{% endif %}
+
+ <li>
+<h3>ROAs</h3>
+{% if object.roas.all %}
+<table>
+ <tr><th>Prefix</th><th>AS</th></tr>
+ {% for roa in object.roas.all %}
+ {% for pfx in roa.prefixes.all %}
+ <tr><td>{{ pfx }}</td><td>{{ roa.asid }}</td></tr>
+ {% endfor %}
+ {% endfor %}
+</table>
+{% endif %}
+
+<li>
+<h3>Children</h3>
+{% if object.children.all %}
+<ul>
+ {% for child in object.children.all %}
+ <li><a href="{{ child.get_absolute_url }}">{{ child.name }}</a>
+ {% endfor %}
+</ul>
+
+{% endif %}
+</ul><!--issued objects-->
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html
new file mode 100644
index 00000000..d179e7c2
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/ghostbuster_detail.html
@@ -0,0 +1,13 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}Ghostbuster Detail{% endblock %}
+
+{% block detail %}
+<p>
+<table>
+ <tr><td>Full Name</td><td>{{ object.full_name }}</td></tr>
+ <tr><td>Organization</td><td>{{ object.organization }}</td></tr>
+ <tr><td>Email</td><td>{{ object.email_address }}</td></tr>
+ <tr><td>Telephone</td><td>{{ object.telephone }}</td></tr>
+</table>
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html b/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html
new file mode 100644
index 00000000..3bc7e259
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/query_result.html
@@ -0,0 +1,21 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+
+<h1>{% block title %}Query Results{% endblock %}</h1>
+
+<table>
+ <tr><th>Prefix</th><th>AS</th><th>Valid</th><th>Until</th></tr>
+ {% for object in object_list %}
+ <tr>
+ <td>{{ object.0 }}</td>
+ <td>{{ object.1.asid }}</td>
+ <td><a href="{{ object.1.get_absolute_url }}">{{ object.1.ok }}</a></td>
+ <td>{{ object.1.not_after }}</td>
+ </tr>
+ {% endfor %}
+</table>
+
+<p><a href="{% url rpki.gui.cacheview.views.query_view %}">new query</a></p>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html
new file mode 100644
index 00000000..01c9f7bf
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/resourcecert_detail.html
@@ -0,0 +1 @@
+{% extends "cacheview/signedobject_detail.html" %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html
new file mode 100644
index 00000000..0ae91dbb
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/roa_detail.html
@@ -0,0 +1,22 @@
+{% extends "cacheview/signedobject_detail.html" %}
+
+{% block title %}
+<h1>ROA Detail</h1>
+{% endblock %}
+
+{% block detail %}
+
+<p>
+<table>
+ <tr><td>AS</td><td>{{ object.asid }}</td></tr>
+</table>
+
+<h2>Prefixes</h2>
+
+<ul>
+{% for pfx in object.prefixes.all %}
+<li>{{ pfx }}
+{% endfor %}
+</ul>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html b/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html
new file mode 100644
index 00000000..1141615d
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/search_form.html
@@ -0,0 +1,17 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block title %}
+{{ search_type }} Search
+{% endblock %}
+
+{% block content %}
+
+<h1>{{search_type}} Search</h1>
+
+<form method='post' action='{{ request.url }}'>
+ {% csrf_token %}
+ {{ form.as_p }}
+ <input type='submit' name='Search'>
+</form>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html b/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html
new file mode 100644
index 00000000..3756047f
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/search_result.html
@@ -0,0 +1,31 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+
+<h1>Search Results</h1>
+
+<h2>Matching Resource Certificates</h2>
+{% if certs %}
+<ul>
+{% for cert in certs %}
+<li><a href="{{ cert.get_absolute_url }}">{{ cert }}</a>
+{% endfor %}
+</ul>
+{% else %}
+<p>none</p>
+{% endif %}
+
+<h2>Matching ROAs</h2>
+{% if roas %}
+<ul>
+{% for roa in roas %}
+<li><a href="{{ roa.get_absolute_url }}">{{ roa }}</a>
+{% endfor %}
+</ul>
+{% else %}
+<p>none</p>
+{% endif %}
+
+<p><a href="{% url rpki.gui.cacheview.views.search_view %}">new search</a>
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html b/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html
new file mode 100644
index 00000000..aa2ec444
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/templates/cacheview/signedobject_detail.html
@@ -0,0 +1,25 @@
+{% extends "cacheview/cacheview_base.html" %}
+
+{% block content %}
+<h1>{% block title %}Signed Object Detail{% endblock %}</h1>
+
+<h2>Metadata</h2>
+
+<table>
+ <tr><td>URI</td><td>{{ object.uri }}</td></tr>
+ <tr><td>Last Modified</td><td>{{ object.mtime_as_datetime|date:"DATETIME_FORMAT" }}</td></tr>
+ <tr><td>Timestamp</td><td>{{ object.timestamp }}</td></tr>
+ <tr><td>Status</td><td>{{ object.status }}</td></tr>
+ <tr><td>Valid</td><td>{{ object.ok }}</td></tr>
+</table>
+
+<h2>Auth Info</h2>
+
+<table>
+ <tr><td>Validity</td><td>{{ object.not_before }} - {{ object.not_after }}</td></tr>
+ <tr><td>Issuer</td><td><a href='{{object.issuer.get_absolute_url}}'>{{ object.issuer.name }}</a></td></tr>
+</table>
+
+{% block detail %}{% endblock %}
+
+{% endblock %}
diff --git a/rpkid/rpki/gui/cacheview/tests.py b/rpkid/rpki/gui/cacheview/tests.py
new file mode 100644
index 00000000..2247054b
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/tests.py
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/rpkid/rpki/gui/cacheview/urls.py b/rpkid/rpki/gui/cacheview/urls.py
new file mode 100644
index 00000000..bbc14b35
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/urls.py
@@ -0,0 +1,28 @@
+# $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.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ (r'^query$', 'rpki.gui.cacheview.views.query_view'),
+ (r'^search$', 'rpki.gui.cacheview.views.search_view'),
+ (r'^cert/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.cert_detail'),
+ (r'^roa/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.roa_detail'),
+ (r'^gbr/(?P<pk>[^/]+)$', 'rpki.gui.cacheview.views.ghostbuster_detail'),
+)
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/cacheview/views.py b/rpkid/rpki/gui/cacheview/views.py
new file mode 100644
index 00000000..733dc5b1
--- /dev/null
+++ b/rpkid/rpki/gui/cacheview/views.py
@@ -0,0 +1,132 @@
+"""
+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.views.generic import list_detail
+from django.shortcuts import get_object_or_404, redirect
+
+from rpki.gui.cacheview import models, forms, misc
+from rpki.gui.app.views import render
+from rpki.resource_set import resource_range_as
+from rpki.ipaddrs import v4addr, v6addr
+
+# Create your views here.
+
+def address_detail(request, pk):
+ return list_detail.object_detail(request, models.AddressRange.objects.all(), pk)
+
+def as_detail(request, pk):
+ return list_detail.object_detail(request, models.ASRange.objects.all(), pk)
+
+def roa_detail(request, pk):
+ return list_detail.object_detail(request, models.ROA.objects.all(), pk)
+
+def cert_detail(request, pk):
+ return list_detail.object_detail(request, models.Cert.objects.all(), pk)
+
+def ghostbuster_detail(request, pk):
+ return list_detail.object_detail(request, models.Ghostbuster.objects.all(), pk)
+
+def search_view(request):
+ if request.method == 'POST':
+ form = forms.SearchForm(request.POST, request.FILES)
+ if form.is_valid():
+ certs = None
+ roas = None
+
+ addr = form.cleaned_data.get('addr')
+ asn = form.cleaned_data.get('asn')
+
+ if addr:
+ family, r = misc.parse_ipaddr(addr)
+ certs = models.Cert.objects.filter(addresses__family=family, addresses__min__gte=r.min, addresses__max__lte=r.max).distinct()
+ roas = models.ROA.objects.filter(prefixes__family=family, prefixes__prefix=str(r.min)).distinct()
+ elif asn:
+ r = resource_range_as.parse_str(asn)
+ certs = models.Cert.objects.filter(asns__min__gte=r.min, asns__max__lte=r.max)
+ roas = models.ROA.objects.filter(asid__gte=r.min, asid__lte=r.max)
+
+ return render('cacheview/search_result.html', { 'certs': certs, 'roas': roas }, request)
+ else:
+ form = forms.SearchForm()
+
+ return render('cacheview/search_form.html', { 'form': form, 'search_type': 'Resource' }, request)
+
+def cmp_prefix(x,y):
+ r = cmp(x[0].family, y[0].family)
+ if r == 0:
+ r = cmp(x[2], y[2]) # integer address
+ if r == 0:
+ r = cmp(x[0].bits, y[0].bits)
+ if r == 0:
+ r = cmp(x[0].max_length, y[0].max_length)
+ if r == 0:
+ r = cmp(x[1].asid, y[1].asid)
+ return r
+
+#def cmp_prefix(x,y):
+# for attr in ('family', 'prefix', 'bits', 'max_length'):
+# r = cmp(getattr(x[0], attr), getattr(y[0], attr))
+# if r:
+# return r
+# return cmp(x[1].asid, y[1].asid)
+
+def query_view(request):
+ """
+ Allow the user to search for an AS or prefix, and show all published ROA
+ information.
+ """
+
+ if request.method == 'POST':
+ form = forms.SearchForm(request.POST, request.FILES)
+ if form.is_valid():
+ certs = None
+ roas = None
+
+ addr = form.cleaned_data.get('addr')
+ asn = form.cleaned_data.get('asn')
+
+ if addr:
+ family, r = misc.parse_ipaddr(addr)
+ prefixes = models.ROAPrefix.objects.filter(family=family, prefix=str(r.min))
+
+ prefix_list = []
+ for pfx in prefixes:
+ for roa in pfx.roas.all():
+ prefix_list.append((pfx, roa))
+ elif asn:
+ r = resource_range_as.parse_str(asn)
+ roas = models.ROA.objects.filter(asid__gte=r.min, asid__lte=r.max)
+
+ # display the results sorted by prefix
+ prefix_list = []
+ for roa in roas:
+ for pfx in roa.prefixes.all():
+ if pfx.family == 4:
+ addr = v4addr(pfx.prefix.encode())
+ elif pfx.family == 6:
+ addr = v6addr(pfx.prefix.encode())
+
+ prefix_list.append((pfx, roa, addr))
+ prefix_list.sort(cmp=cmp_prefix)
+
+ return render('cacheview/query_result.html',
+ { 'object_list': prefix_list }, request)
+ else:
+ form = forms.SearchForm()
+
+ return render('cacheview/search_form.html', { 'form':form, 'search_type': 'ROA ' }, request)
+
+# vim:sw=4 ts=8 expandtab
diff --git a/rpkid/rpki/gui/settings.py.in b/rpkid/rpki/gui/settings.py.in
index 04d6aea8..898f7d31 100644
--- a/rpkid/rpki/gui/settings.py.in
+++ b/rpkid/rpki/gui/settings.py.in
@@ -58,7 +58,8 @@ INSTALLED_APPS = (
'django.contrib.admindocs',
'django.contrib.contenttypes',
'django.contrib.sessions',
- 'rpki.gui.app'
+ 'rpki.gui.app',
+ 'rpki.gui.cacheview'
)
TEMPLATE_CONTEXT_PROCESSORS = (
diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py
index 919a812e..70ea4056 100644
--- a/rpkid/rpki/gui/urls.py
+++ b/rpkid/rpki/gui/urls.py
@@ -32,6 +32,7 @@ urlpatterns = patterns('',
(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',
diff --git a/rpkid/setup.py b/rpkid/setup.py
index 679c7e7f..75d6069c 100644
--- a/rpkid/setup.py
+++ b/rpkid/setup.py
@@ -63,7 +63,8 @@ setup(name = "rpkitoolkit",
description = "RPKI Toolkit",
license = "BSD",
url = "http://www.rpki.net/",
- packages = ["rpki", "rpki.POW", "rpki.gui", "rpki.gui.app" ],
+ packages = ["rpki", "rpki.POW", "rpki.gui", "rpki.gui.app", "rpki.gui.cacheview" ],
ext_modules = [pow],
- package_data = { 'rpki.gui.app' : ['templates/*.html', 'templates/*/*.html'] },
+ package_data = { 'rpki.gui.app' : ['templates/*.html', 'templates/*/*.html'],
+ 'rpki.gui.cacheview' : [ 'templates/*/*.html' ] },
data_files = data_files)