aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--portal-gui/rpkigui/myrpki/admin.py4
-rw-r--r--portal-gui/rpkigui/myrpki/forms.py67
-rw-r--r--portal-gui/rpkigui/myrpki/glue.py10
-rw-r--r--portal-gui/rpkigui/myrpki/misc.py10
-rw-r--r--portal-gui/rpkigui/myrpki/models.py64
-rw-r--r--portal-gui/rpkigui/myrpki/urls.py1
-rw-r--r--portal-gui/rpkigui/myrpki/views.py69
-rw-r--r--portal-gui/rpkigui/settings.py6
-rw-r--r--portal-gui/rpkigui/templates/myrpki/dashboard.html4
-rw-r--r--portal-gui/rpkigui/templates/myrpki/prefix_view.html22
10 files changed, 196 insertions, 61 deletions
diff --git a/portal-gui/rpkigui/myrpki/admin.py b/portal-gui/rpkigui/myrpki/admin.py
index 3d843bb0..b3932770 100644
--- a/portal-gui/rpkigui/myrpki/admin.py
+++ b/portal-gui/rpkigui/myrpki/admin.py
@@ -23,10 +23,14 @@ class RoaAdmin( admin.ModelAdmin ):
class ResourceCertAdmin(admin.ModelAdmin):
pass
+class RoaRequestAdmin(admin.ModelAdmin):
+ pass
+
admin.site.register(models.Conf, ConfAdmin)
admin.site.register(models.Child, ChildAdmin)
admin.site.register(models.AddressRange, AddressRangeAdmin)
admin.site.register(models.Asn, AsnAdmin)
admin.site.register(models.Parent, ParentAdmin)
admin.site.register(models.Roa, RoaAdmin)
+admin.site.register(models.RoaRequest, RoaRequestAdmin)
admin.site.register(models.ResourceCert, ResourceCertAdmin)
diff --git a/portal-gui/rpkigui/myrpki/forms.py b/portal-gui/rpkigui/myrpki/forms.py
index 866be7dd..d0ddc462 100644
--- a/portal-gui/rpkigui/myrpki/forms.py
+++ b/portal-gui/rpkigui/myrpki/forms.py
@@ -3,6 +3,7 @@
from django import forms
import models
from rpkigui.myrpki.misc import str_to_addr
+from rpkigui.myrpki.asnset import asnset
def ConfCertForm(request):
class CertForm(forms.ModelForm):
@@ -62,7 +63,8 @@ def RangeForm(field_type, *args, **kwargs):
hi = self.cleaned_data.get('hi')
if lo > hi:
# should we just fix it?
- raise forms.ValidationError, 'Lower bound is higher than upper.'
+ raise forms.ValidationError, \
+ 'Lower bound is higher than upper.'
return self.cleaned_data
return wrapped(*args, **kwargs)
@@ -92,8 +94,8 @@ def get_pk(child):
def SubOrAssignForm(handle, addr, field_type, *args, **kwargs):
'''Closure to select child of the specified handle.'''
class Wrapper(forms.Form):
- '''Form for the address view to allow the user to subdivide or assign
- the block to a child.'''
+ '''Form for the address view to allow the user to subdivide or
+ assign the block to a child.'''
lo = field_type(required=False, label='Lower bound')
hi = field_type(required=False, label='Upper bound')
child = forms.ModelChoiceField(required=False, label='Assign to child',
@@ -107,7 +109,8 @@ def SubOrAssignForm(handle, addr, field_type, *args, **kwargs):
lo = None
if lo != None:
if lo < addr.lo or lo > addr.hi:
- raise forms.ValidationError, 'Value is out of range of parent.'
+ raise forms.ValidationError, \
+ 'Value is out of range of parent.'
# ensure there is no overlap with other children
for c in addr.children.all():
if lo >= c.lo and lo <= c.hi:
@@ -162,7 +165,8 @@ def SubOrAssignAsnForm(handle, asn, *args, **kwargs):
return SubOrAssignForm(handle, asn, forms.IntegerField, *args, **kwargs)
def RoaForm(handle, pk=None, initval=[], *args, **kwargs):
- vals = models.AddressRange.objects.filter(from_parent__in=handle.parents.all())
+ vals = models.AddressRange.objects.filter(
+ from_parent__in=handle.parents.all())
class Wrapped(forms.Form):
asn = AsnField(initial=pk)
@@ -186,9 +190,11 @@ def PrefixSplitForm(prefix, *args, **kwargs):
pfx_loaddr = str_to_addr(prefix.lo)
pfx_hiaddr = str_to_addr(prefix.hi)
if type(loaddr) != type(pfx_hiaddr):
- raise forms.ValidationError, 'Not the same IP address type as parent'
+ raise forms.ValidationError, \
+ 'Not the same IP address type as parent'
if loaddr < pfx_loaddr or loaddr > pfx_hiaddr:
- raise forms.ValidationError, 'Value out of range of parent prefix'
+ raise forms.ValidationError, \
+ 'Value out of range of parent prefix'
return lo
def clean_hi(self):
@@ -201,9 +207,11 @@ def PrefixSplitForm(prefix, *args, **kwargs):
pfx_loaddr = str_to_addr(prefix.lo)
pfx_hiaddr = str_to_addr(prefix.hi)
if type(hiaddr) != type(pfx_loaddr):
- raise forms.ValidationError, 'Not the same IP address type as parent'
+ raise forms.ValidationError, \
+ 'Not the same IP address type as parent'
if hiaddr < pfx_loaddr or hiaddr > pfx_hiaddr:
- raise forms.ValidationError, 'Value out of range of parent prefix'
+ raise forms.ValidationError, \
+ 'Value out of range of parent prefix'
return hi
def clean(self):
@@ -224,21 +232,35 @@ def PrefixSplitForm(prefix, *args, **kwargs):
def PrefixAllocateForm(iv, child_set, *args, **kwargs):
class _wrapper(forms.Form):
- child = forms.ModelChoiceField(initial=iv, queryset=child_set, required=False)
+ child = forms.ModelChoiceField(initial=iv, queryset=child_set,
+ required=False)
return _wrapper(*args, **kwargs)
-class PrefixRoaForm(forms.Form):
- asns = forms.CharField(max_length=200, required=False)
+def PrefixRoaForm(prefix, *args, **kwargs):
+ prefix_range = prefix.as_resource_range()
- def clean_asns(self):
- try:
- v = [int(d) for d in self.cleaned_data['asns'].split(',') if d.strip() != '']
- if any([x for x in v if x < 0]):
- raise forms.ValidationError, 'must be a positive integer'
- return ','.join(str(x) for x in sorted(v))
- except ValueError:
- raise forms.ValidationError, 'must be a list of integers separated by commas'
- return self.cleaned_data['asns']
+ class _wrapper(forms.Form):
+ asns = forms.CharField(max_length=200, required=False, help_text='Comma-separated list of ASNs')
+ max_length = forms.IntegerField(required=False,
+ min_value=prefix_range._prefixlen(),
+ max_value=prefix_range.datum_type.bits)
+
+ def clean_max_length(self):
+ v = self.cleaned_data.get('max_length')
+ if not v:
+ v = prefix_range._prefixlen()
+ return v
+
+ def clean_asns(self):
+ try:
+ v = asnset(self.cleaned_data.get('asns'))
+ return ','.join(str(x) for x in sorted(v))
+ except ValueError:
+ raise forms.ValidationError, \
+ 'Must be a list of integers separated by commas.'
+ return self.cleaned_data['asns']
+
+ return _wrapper(*args, **kwargs)
def PrefixDeleteForm(prefix, *args, **kwargs):
class _wrapped(forms.Form):
@@ -248,7 +270,8 @@ def PrefixDeleteForm(prefix, *args, **kwargs):
v = self.cleaned_data.get('delete')
if v:
if not prefix.parent:
- raise forms.ValidationError, 'Can not delete prefix received from parent'
+ raise forms.ValidationError, \
+ 'Can not delete prefix received from parent'
if prefix.allocated:
raise forms.ValidationError, 'Prefix is allocated to child'
if prefix.asns:
diff --git a/portal-gui/rpkigui/myrpki/glue.py b/portal-gui/rpkigui/myrpki/glue.py
index 1417131b..eececa7a 100644
--- a/portal-gui/rpkigui/myrpki/glue.py
+++ b/portal-gui/rpkigui/myrpki/glue.py
@@ -64,12 +64,10 @@ def output_prefixes(path, handle):
def output_roas(path, handle):
f = csv_writer(path)
- for r in handle.roas.all():
- for addr in r.prefix.all():
- f.writerow([resource_range_ipv4(v4addr(str(addr.lo)),
- v4addr(str(addr.hi))),
- r.asn,
- '%s-group-%d' % (handle.handle, r.pk)])
+ for roa in handle.roas.all():
+ for req in roa.from_roa_request.all():
+ f.writerow([req.as_roa_prefix(), roa.asn,
+ '%s-group-%d' % (handle.handle, roa.pk)])
def configure_resources(handle):
'''Write out the csv files and invoke the myrpki.py command line tool.'''
diff --git a/portal-gui/rpkigui/myrpki/misc.py b/portal-gui/rpkigui/myrpki/misc.py
index 4e0970a6..3b54107f 100644
--- a/portal-gui/rpkigui/myrpki/misc.py
+++ b/portal-gui/rpkigui/myrpki/misc.py
@@ -23,4 +23,14 @@ def str_to_range(lo, hi):
else:
return rpki.resource_set.resource_range_ipv6(x, y)
+#def str_to_roa(lo, hi):
+# """Convert IP address strings to a subclass of roa_prefix."""
+# x = str_to_addr(lo)
+# y = str_to_addr(hi)
+# assert type(x) == type(y)
+# if isinstance(x, rpki.ipaddrs.v4addr):
+# return rpki.resource_set.roa_prefix_ipv4(x, y)
+# else:
+# return rpki.resource_set.roa_prefix_ipv6(x, y)
+
# vim:sw=4 ts=8 expandtab
diff --git a/portal-gui/rpkigui/myrpki/models.py b/portal-gui/rpkigui/myrpki/models.py
index 9a6952ce..cf062b3c 100644
--- a/portal-gui/rpkigui/myrpki/models.py
+++ b/portal-gui/rpkigui/myrpki/models.py
@@ -4,8 +4,11 @@ import socket
from django.db import models
from django.contrib.auth.models import User
-from rpkigui.myrpki.misc import str_to_range
+
+from rpkigui.myrpki.misc import str_to_range, str_to_addr
+
import rpki.resource_set
+import rpki.exceptions
class HandleField(models.CharField):
def __init__(self, **kwargs):
@@ -15,10 +18,6 @@ class IPAddressField(models.CharField):
def __init__( self, **kwargs ):
models.CharField.__init__(self, max_length=40, **kwargs)
-class ASNListField(models.CharField):
- def __init__( self, **kwargs ):
- models.CharField.__init__(self, max_length=255, **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>
@@ -39,8 +38,6 @@ class AddressRange(models.Model):
# child to which this resource is delegated
allocated = models.ForeignKey('Child', related_name='address_range',
blank=True, null=True)
- # who can originate routes for this prefix
- asns = ASNListField(null=True, blank=True)
def __unicode__(self):
if self.lo == self.hi:
@@ -48,7 +45,7 @@ class AddressRange(models.Model):
try:
# pretty print cidr
- return unicode(str_to_range(self.lo, self.hi))
+ return unicode(self.as_resource_range())
except socket.error, err:
print err
# work around for bug when hi/lo get reversed
@@ -59,6 +56,43 @@ class AddressRange(models.Model):
def get_absolute_url(self):
return u'/myrpki/address/%d' % (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:
+ print err
+ return False
+ return True
+
+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')
+
+ 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_set_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)
+
+ def get_absolute_url(self):
+ return u'/myrpki/roa/%d' % (self.pk,)
+
class Asn(models.Model):
'''An ASN or range thereof.'''
lo = models.IntegerField(blank=False)
@@ -114,8 +148,8 @@ class ResourceCert(models.Model):
# 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)
+ address_range = models.ManyToManyField(AddressRange,
+ related_name='from_cert', blank=True, null=True)
# unique id for this resource certificate
# FIXME: URLField(verify_exists=False) doesn't seem to work - the admin
@@ -134,13 +168,13 @@ class ResourceCert(models.Model):
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.'''
+ '''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.'''
+
conf = models.ForeignKey(Conf, related_name='roas')
asn = models.IntegerField()
- prefix = models.ManyToManyField(AddressRange, related_name='from_roa')
active = models.BooleanField()
def __unicode__(self):
diff --git a/portal-gui/rpkigui/myrpki/urls.py b/portal-gui/rpkigui/myrpki/urls.py
index be6277d3..4ee97984 100644
--- a/portal-gui/rpkigui/myrpki/urls.py
+++ b/portal-gui/rpkigui/myrpki/urls.py
@@ -23,6 +23,7 @@ urlpatterns = patterns('',
(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'^roa/(?P<pk>\d+)/delete$', views.roa_request_delete_view)
# (r'^roa/$', views.roa_edit ),
# (r'^roa/(?P<pk>\d+)$', views.roa_edit ),
)
diff --git a/portal-gui/rpkigui/myrpki/views.py b/portal-gui/rpkigui/myrpki/views.py
index d78eec33..33502631 100644
--- a/portal-gui/rpkigui/myrpki/views.py
+++ b/portal-gui/rpkigui/myrpki/views.py
@@ -17,6 +17,7 @@ import forms
import glue
from asnset import asnset
from rpkigui.myrpki.misc import str_to_range
+from rpkigui.myrpki.asnset import asnset
# For each type of object, we have a detail view, a create view and
# an update view. We heavily leverage the generic views, only
@@ -87,7 +88,7 @@ def unallocated_resources(handle, roa_asns, roa_prefixes, asns, prefixes):
child_prefixes = []
for p in prefixes:
- child_prefixes.extend(o for o in p.children.filter(allocated__isnull=True).exclude(from_roa__in=roa_prefixes))
+ child_prefixes.extend(o for o in p.children.filter(allocated__isnull=True, roa_requests__isnull=True))
if child_asns or child_prefixes:
x, y = unallocated_resources(handle, roa_asns, roa_prefixes,
@@ -111,14 +112,14 @@ def dashboard(request):
# get list of ASNs used in my ROAs
roa_asns = [r.asn for r in handle.roas.all()]
# get list of address ranges included in ROAs
- roa_addrs = [p for r in handle.roas.all() for p in r.prefix.all()]
+ roa_addrs = [p.prefix for r in handle.roas.all() for p in r.from_roa_request.all()]
asns=[]
prefixes=[]
for p in handle.parents.all():
for c in p.resources.all():
asns.extend(c.asn.filter(allocated__isnull=True).exclude(lo__in=roa_asns))
- prefixes.extend(c.address_range.filter(allocated__isnull=True).exclude(from_roa__in=roa_addrs))
+ prefixes.extend(c.address_range.filter(allocated__isnull=True, roa_requests__isnull=True))
asns, prefixes = unallocated_resources(handle, roa_asns, roa_addrs, asns,
prefixes)
@@ -491,9 +492,18 @@ def prefix_allocate_view(request, pk):
return render('myrpki/prefix_view.html', { 'form': form,
'addr': prefix, 'form': form, 'parent': parent_set }, request)
+def parent_prefix(prefix):
+ '''Returns the top-most parent prefix for the given prefix.'''
+ while prefix.parent:
+ prefix = prefix.parent
+ return prefix
+
def common_cert(prefix, prefix_set):
'''Return true if prefix is derived from the same resource cert
as all the addresses in prefix_set.'''
+ while prefix.parent:
+ prefix = prefix.parent
+
# list of certs for the target prefix
certs = prefix.from_cert.all()
# all prefixes will have the same cert, so just check the first one
@@ -530,10 +540,34 @@ def update_roas(handle, prefix):
else:
# no roa is present for this ASN, create a new one
print 'creating new roa for asn %d with %s' % (asid, prefix)
- roa = models.Roa.objects.create(asn=asid, conf=handle, active=False)
+ roa = models.Roa.objects.create(asn=asid, conf=handle,
+ active=False)
roa.save()
roa.prefix.add(prefix)
+def add_roa_requests(handle, prefix, asns, max_length):
+ for asid in asns:
+ req_set = prefix.roa_requests.filter(roa__asn=asid,
+ max_length=max_length)
+ if not req_set:
+ # no req is present for this (ASN, prefix, max_length).
+
+ # find all roas with prefixes from the same resource cert
+ roa_set = handle.roas.filter(asn=asid,
+ from_roa_request__prefix__from_cert__in=prefix.from_cert.all())
+ if roa_set:
+ roa = roa_set[0]
+ else:
+ # no roa is present for this ASN, create a new one
+ print 'creating new roa for asn %d' % (asid,)
+ roa = models.Roa.objects.create(asn=asid, conf=handle,
+ active=False)
+ roa.save()
+
+ req = models.RoaRequest.objects.create(prefix=prefix, roa=roa,
+ max_length=max_length)
+ req.save()
+
@handle_required
def prefix_roa_view(request, pk):
handle = request.session['handle']
@@ -542,15 +576,15 @@ def prefix_roa_view(request, pk):
parent_set = get_parents_or_404(handle, obj)
if request.method == 'POST':
- form = forms.PrefixRoaForm(request.POST)
+ form = forms.PrefixRoaForm(obj, request.POST)
if form.is_valid():
- obj.asns = form.cleaned_data['asns']
- obj.save()
- update_roas(handle, obj)
+ asns = asnset(form.cleaned_data['asns'])
+ add_roa_requests(handle, obj, asns,
+ form.cleaned_data['max_length'])
glue.configure_resources(handle)
return http.HttpResponseRedirect(obj.get_absolute_url())
else:
- form = forms.PrefixRoaForm(initial={ 'asns': obj.asns })
+ form = forms.PrefixRoaForm(obj)
return render('myrpki/prefix_view.html', { 'form': form,
'addr': obj, 'form': form, 'parent': parent_set }, request)
@@ -575,6 +609,23 @@ def prefix_delete_view(request, pk):
'addr': obj, 'form': form, 'parent': parent_set }, request)
@handle_required
+def roa_request_delete_view(request, pk):
+ '''Remove a roa request from a particular prefix.'''
+ 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)
+
+ roa = obj.roa
+ obj.delete()
+ if not roa.from_roa_request.all():
+ print 'removing empty roa for asn %d' % (roa.asn,)
+ roa.delete()
+
+ return http.HttpResponseRedirect(prefix.get_absolute_url())
+
+@handle_required
def asn_allocate_view(request, pk):
handle = request.session['handle']
obj = get_object_or_404(models.Asn.objects, pk=pk)
diff --git a/portal-gui/rpkigui/settings.py b/portal-gui/rpkigui/settings.py
index afabc19d..43025cbd 100644
--- a/portal-gui/rpkigui/settings.py
+++ b/portal-gui/rpkigui/settings.py
@@ -10,7 +10,7 @@ ADMINS = (
MANAGERS = ADMINS
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
-DATABASE_NAME = '/home/melkins/myrpki/rpkiop' # Or path to database file if using sqlite3.
+DATABASE_NAME = '/home/me/myrpki/rpkiop' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
@@ -72,7 +72,7 @@ TEMPLATE_DIRS = (
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
#XXX
- '/home/melkins/subvert-rpki.hactrn.net/portal-gui/rpkigui/templates',
+ '/home/me/src/rpki/portal-gui/rpkigui/templates',
)
INSTALLED_APPS = (
@@ -99,7 +99,7 @@ TEMPLATE_CONTEXT_PROCESSORS = (
#
# Top of directory tree where myrpki.conf, etc are stored for each resource holder
-MYRPKI_DATA_DIR = '/home/melkins/myrpki'
+MYRPKI_DATA_DIR = '/home/me/myrpki'
# where to find the myrpki.py command line tool
MYRPKI_SRC_DIR = '/home/melkins/subvert-rpki.hactrn.net/rpkid'
diff --git a/portal-gui/rpkigui/templates/myrpki/dashboard.html b/portal-gui/rpkigui/templates/myrpki/dashboard.html
index 8abe7f82..07df5ad6 100644
--- a/portal-gui/rpkigui/templates/myrpki/dashboard.html
+++ b/portal-gui/rpkigui/templates/myrpki/dashboard.html
@@ -82,8 +82,8 @@
{% for roa in request.session.handle.roas.all %}
<tr><td>
-{% for address_range in roa.prefix.all %}
-<li><a href="{{ address_range.get_absolute_url }}">{{ address_range }}</a>
+{% for req in roa.from_roa_request.all %}
+<li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a>
{% endfor %}
</td>
<td>{{ roa.asn }}</td>
diff --git a/portal-gui/rpkigui/templates/myrpki/prefix_view.html b/portal-gui/rpkigui/templates/myrpki/prefix_view.html
index a41af7c9..74bf45d8 100644
--- a/portal-gui/rpkigui/templates/myrpki/prefix_view.html
+++ b/portal-gui/rpkigui/templates/myrpki/prefix_view.html
@@ -26,9 +26,6 @@
{% if addr.allocated %}
<tr><td>Allocated:</td><td><a href="{{addr.allocated.get_absolute_url}}">{{addr.allocated.handle}}</a></td></tr>
{% endif %}
-
- <tr><td>Allowed to orignate:</td><td>{{addr.asns}}</td></tr>
-
</table>
{% if addr.children.count %}
@@ -42,6 +39,21 @@
{% endif %}
+{% if addr.roa_requests %}
+<h2>ROA requests</h2>
+<table style='border: solid 1px; width:100%'>
+ <tr><th>ASN</th><th>Max Length</th></tr>
+
+ {% for r in addr.roa_requests.all %}
+ <tr><td style='text-align: center'>{{ r.roa.asn }}</td>
+ <td style='text-align: center'>{{ r.max_length }}</td>
+ <td><a href="{{ r.get_absolute_url }}/delete">delete</a></tr>
+ {% endfor %}
+
+</table>
+
+{% endif %} <!-- roa requests -->
+
{% if form %}
<h2>Edit</h2>
<form method="POST" action="{{ request.get_full_path }}">
@@ -53,7 +65,9 @@
<p>Action:
<a href="{{addr.get_absolute_url}}/split">split</a> |
<a href="{{addr.get_absolute_url}}/allocate">give to child</a> |
-<a href="{{addr.get_absolute_url}}/roa">edit allowed originators</a> |
+{% if addr.is_prefix %}
+<a href="{{addr.get_absolute_url}}/roa">roa</a> |
+{% endif %}
<a href="{{addr.get_absolute_url}}/delete">delete</a>
{% endblock %}