aboutsummaryrefslogtreecommitdiff
path: root/rpkid/rpki/gui
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid/rpki/gui')
-rw-r--r--rpkid/rpki/gui/app/AllocationTree.py151
-rw-r--r--rpkid/rpki/gui/app/admin.py62
-rw-r--r--rpkid/rpki/gui/app/asnset.py40
-rw-r--r--rpkid/rpki/gui/app/forms.py493
-rw-r--r--rpkid/rpki/gui/app/glue.py506
-rw-r--r--rpkid/rpki/gui/app/misc.py47
-rw-r--r--rpkid/rpki/gui/app/models.py376
-rwxr-xr-xrpkid/rpki/gui/app/range_list.py244
-rw-r--r--rpkid/rpki/gui/app/settings.py.in14
-rw-r--r--rpkid/rpki/gui/app/templates/app/app_base.html31
-rw-r--r--rpkid/rpki/gui/app/templates/app/bootstrap_form.html33
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_add_resource_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_delete_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_detail.html53
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_form.html17
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_import_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/child_list.html7
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_detail.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_import_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/client_list.html1
-rw-r--r--rpkid/rpki/gui/app/templates/app/conf_empty.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/conf_list.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/conf_list.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/dashboard.html85
-rw-r--r--rpkid/rpki/gui/app/templates/app/destroy_handle_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/generic_result.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/generic_result.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbuster_form.html21
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html53
-rw-r--r--rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html13
-rw-r--r--rpkid/rpki/gui/app/templates/app/initialize_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_detail.html33
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_list.html36
-rw-r--r--rpkid/rpki/gui/app/templates/app/object_table.html41
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_detail.html62
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_import_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/parent_list.html5
-rw-r--r--rpkid/rpki/gui/app/templates/app/pubclient_list.html9
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_detail.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_import_form.html18
-rw-r--r--rpkid/rpki/gui/app/templates/app/repository_list.html7
-rw-r--r--rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html54
-rw-r--r--rpkid/rpki/gui/app/templates/app/roa_request_list.html14
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html58
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/route_roa_list.html19
-rw-r--r--rpkid/rpki/gui/app/templates/app/routes_view.html41
-rw-r--r--rpkid/rpki/gui/app/templates/app/update_bpki_form.html (renamed from rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html)0
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_confirm_delete.html20
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_create_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_edit_form.html16
-rw-r--r--rpkid/rpki/gui/app/templates/app/user_list.html29
-rw-r--r--rpkid/rpki/gui/app/templates/base.html72
-rw-r--r--rpkid/rpki/gui/app/templates/registration/login.html41
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/asn_view.html93
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_form.html20
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_view.html60
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/dashboard.html193
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html14
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html69
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html18
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html23
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html13
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/parent_form.html11
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/parent_view.html38
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html96
-rw-r--r--rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html24
-rw-r--r--rpkid/rpki/gui/app/templatetags/__init__.py0
-rw-r--r--rpkid/rpki/gui/app/templatetags/app_extras.py13
-rw-r--r--rpkid/rpki/gui/app/timestamp.py25
-rw-r--r--rpkid/rpki/gui/app/urls.py101
-rw-r--r--rpkid/rpki/gui/app/views.py1428
-rw-r--r--rpkid/rpki/gui/cacheview/admin.py59
-rw-r--r--rpkid/rpki/gui/cacheview/models.py203
-rw-r--r--rpkid/rpki/gui/models.py132
-rw-r--r--rpkid/rpki/gui/routeview/__init__.py0
-rw-r--r--rpkid/rpki/gui/routeview/models.py46
-rw-r--r--rpkid/rpki/gui/urls.py47
80 files changed, 2957 insertions, 2778 deletions
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> &gt; AS View &gt; {{ 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> &gt;
-<a href="{{ child.get_absolute_url }}">{{ child.handle }}</a> &gt; 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> &gt; {{ 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 }} &gt; 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> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> &gt; {{ 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> &gt; <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> &gt; 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> &gt; 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> &gt; Parent View &gt; {{ 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> &gt; Prefix View &gt; {{ 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> &gt; <a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a> &gt; 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'}),
)