aboutsummaryrefslogtreecommitdiff
path: root/portal-gui
diff options
context:
space:
mode:
Diffstat (limited to 'portal-gui')
-rw-r--r--portal-gui/Makefile.in1
-rw-r--r--portal-gui/rpkigui/myrpki/AllocationTree.py148
-rw-r--r--portal-gui/rpkigui/myrpki/misc.py6
-rw-r--r--portal-gui/rpkigui/myrpki/models.py3
-rw-r--r--portal-gui/rpkigui/myrpki/views.py256
-rw-r--r--portal-gui/rpkigui/templates/myrpki/asn_view.html9
-rw-r--r--portal-gui/rpkigui/templates/myrpki/dashboard.html49
-rw-r--r--portal-gui/rpkigui/templates/myrpki/prefix_view.html9
8 files changed, 326 insertions, 155 deletions
diff --git a/portal-gui/Makefile.in b/portal-gui/Makefile.in
index 311b6c5c..22e817fb 100644
--- a/portal-gui/Makefile.in
+++ b/portal-gui/Makefile.in
@@ -49,6 +49,7 @@ INSTALL_FILES=\
rpkigui/manage.py \
rpkigui/settings.py \
rpkigui/urls.py \
+ rpkigui/myrpki/AllocationTree.py \
rpkigui/myrpki/__init__.py \
rpkigui/myrpki/admin.py \
rpkigui/myrpki/asnset.py \
diff --git a/portal-gui/rpkigui/myrpki/AllocationTree.py b/portal-gui/rpkigui/myrpki/AllocationTree.py
new file mode 100644
index 00000000..13936797
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/AllocationTree.py
@@ -0,0 +1,148 @@
+# $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.
+"""
+
+from rpkigui.myrpki 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'''
+ return[' | <a href="%s/roa">roa</a>' % (self.resource.get_absolute_url(),)]
+
+ 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/portal-gui/rpkigui/myrpki/misc.py b/portal-gui/rpkigui/myrpki/misc.py
index 16954d87..5d3cba93 100644
--- a/portal-gui/rpkigui/myrpki/misc.py
+++ b/portal-gui/rpkigui/myrpki/misc.py
@@ -38,4 +38,10 @@ def parse_resource_range(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/portal-gui/rpkigui/myrpki/models.py b/portal-gui/rpkigui/myrpki/models.py
index 1cfb1788..fc8d4a6d 100644
--- a/portal-gui/rpkigui/myrpki/models.py
+++ b/portal-gui/rpkigui/myrpki/models.py
@@ -127,6 +127,9 @@ class Asn(models.Model):
def get_absolute_url(self):
return u'/myrpki/asn/%d' % (self.pk,)
+ def as_resource_range(self):
+ return rpki.resource_set.resource_range_as(self.lo, self.hi)
+
class Child(models.Model):
conf = models.ForeignKey(Conf, related_name='children')
handle = HandleField() # parent's name for child
diff --git a/portal-gui/rpkigui/myrpki/views.py b/portal-gui/rpkigui/myrpki/views.py
index be9741a5..fa9f27e3 100644
--- a/portal-gui/rpkigui/myrpki/views.py
+++ b/portal-gui/rpkigui/myrpki/views.py
@@ -26,9 +26,11 @@ from django.db import IntegrityError
from django import http
from django.views.generic.list_detail import object_list
-from rpkigui.myrpki import models, forms, glue, misc
+from rpkigui.myrpki import models, forms, glue, misc, AllocationTree
from rpkigui.myrpki.asnset import asnset
+debug = False
+
# 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.
@@ -60,22 +62,6 @@ def render(template, context, request):
return render_to_response(template, context,
context_instance=RequestContext(request))
-def unallocated_resources(handle, roa_asns, roa_prefixes, asns, prefixes):
- child_asns = []
- for a in asns:
- child_asns.extend(o for o in a.children.filter(allocated__isnull=True).exclude(lo__in=roa_asns) if o.hi == o.lo)
-
- child_prefixes = []
- for p in 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,
- child_asns, child_prefixes)
- return asns + x, prefixes + y
- else:
- return asns, prefixes
-
@handle_required
def dashboard(request):
'''The user's dashboard.'''
@@ -90,21 +76,20 @@ 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.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,
- roa_requests__isnull=True))
- asns, prefixes = unallocated_resources(handle, roa_asns, roa_addrs, asns,
- prefixes)
+ 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.sort(key=lambda x: x.as_resource_range().min)
+ 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('myrpki/dashboard.html', { 'conf': handle, 'asns': asns,
'ars': prefixes }, request)
@@ -261,22 +246,12 @@ def child_import(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 = top_parent(obj).from_cert.filter(parent__in=handle.parents.all())
+ 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 address_view(request, pk):
- handle = request.session['handle']
- obj = get_object_or_404(models.AddressRange.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, obj)
-
- return render('myrpki/prefix_view.html',
- { 'addr': obj, 'parent': parent_set }, request)
-
-@handle_required
def asn_view(request, pk):
'''view/subdivide an asn range.'''
handle = request.session['handle']
@@ -284,9 +259,11 @@ def asn_view(request, 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('myrpki/asn_view.html',
- { 'asn': obj, 'parent': parent_set, 'roas': roas }, request)
+ { 'asn': obj, 'parent': parent_set, 'roas': roas,
+ 'unallocated' : unallocated }, request)
@handle_required
def child_view(request, child_handle):
@@ -296,122 +273,143 @@ def child_view(request, child_handle):
return render('myrpki/child_view.html', { 'child': child }, request)
+class PrefixView(object):
+ '''Extensible view for address ranges/prefixes. This view can be
+ subclassed to add form handling for editing the prefix.'''
+
+ 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 = None
+ 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('myrpki/prefix_view.html',
+ { 'addr': self.obj, 'parent': self.parent_set, 'unallocated': u, 'form': self.form },
+ 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 prefix_split_view(request, pk):
- handle = request.session['handle']
- prefix = get_object_or_404(models.AddressRange.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, prefix)
+def address_view(request, pk):
+ return PrefixView(request, pk)()
- if request.method == 'POST':
- form = forms.PrefixSplitForm(prefix, request.POST)
- if form.is_valid():
- r = misc.parse_resource_range(form.cleaned_data['prefix'])
- obj = models.AddressRange(lo=str(r.min), hi=str(r.max),
- parent=prefix)
- #obj = models.AddressRange(lo=form.cleaned_data['lo'],
- # hi=form.cleaned_data['hi'], parent=prefix)
- obj.save()
- return http.HttpResponseRedirect(obj.get_absolute_url())
- else:
- form = forms.PrefixSplitForm(prefix)
+class PrefixSplitView(PrefixView):
+ '''Class for handling the prefix split form.'''
+ 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())
- return render('myrpki/prefix_view.html', { 'form': form,
- 'addr': prefix, 'form': form, 'parent': parent_set }, request)
+@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.'''
+ 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.handle)
+ return http.HttpResponseRedirect(self.obj.get_absolute_url())
@handle_required
def prefix_allocate_view(request, pk):
- handle = request.session['handle']
- prefix = get_object_or_404(models.AddressRange.objects, pk=pk)
- # ensure this resource range belongs to a parent of the current conf
- parent_set = get_parents_or_404(handle, prefix)
-
- if request.method == 'POST':
- form = forms.PrefixAllocateForm(None, handle.children.all(), request.POST)
- if form.is_valid():
- prefix.allocated = form.cleaned_data['child']
- prefix.save()
- glue.configure_resources(handle)
- return http.HttpResponseRedirect(prefix.get_absolute_url())
- else:
- form = forms.PrefixAllocateForm(
- prefix.allocated.pk if prefix.allocated else None,
- handle.children.all())
-
- return render('myrpki/prefix_view.html', { 'form': form,
- 'addr': prefix, 'form': form, 'parent': parent_set }, request)
-
-def top_parent(prefix):
- '''Returns the topmost resource from which the specified argument derives'''
- while prefix.parent:
- prefix = prefix.parent
- return prefix
+ return PrefixAllocateView(request, pk)()
def find_roa(handle, prefix, asid):
'''Find a roa with prefixes from the same resource cert.'''
roa_set = handle.roas.filter(asn=asid)
- for c in top_parent(prefix).from_cert.all():
+ for c in misc.top_parent(prefix).from_cert.all():
for r in roa_set:
for req in r.from_roa_request.all():
- if c in top_parent(req.prefix).from_cert.all():
+ if c in misc.top_parent(req.prefix).from_cert.all():
return r
return None
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 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)
roa = find_roa(handle, prefix, asid)
if not roa:
+ 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)
+ 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 = models.RoaRequest.objects.create(prefix=prefix, roa=roa, max_length=max_length)
req.save()
+class PrefixRoaView(PrefixView):
+ '''Class for handling the ROA creation form.'''
+ 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.handle)
+ return http.HttpResponseRedirect(self.obj.get_absolute_url())
+
@handle_required
def prefix_roa_view(request, pk):
- handle = request.session['handle']
- obj = get_object_or_404(models.AddressRange.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.PrefixRoaForm(obj, request.POST)
- if form.is_valid():
- 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(obj)
-
- return render('myrpki/prefix_view.html', { 'form': form,
- 'addr': obj, 'form': form, 'parent': parent_set }, request)
-
+ return PrefixRoaView(request, pk, form_class=forms.PrefixRoaForm)()
+
+class PrefixDeleteView(PrefixView):
+ def form_valid(self):
+ if self.form.cleaned_data['delete']:
+ self.obj.delete()
+ return http.HttpResponseRedirect('/myrpki/')
+
@handle_required
def prefix_delete_view(request, pk):
- handle = request.session['handle']
- obj = get_object_or_404(models.AddressRange.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.PrefixDeleteForm(obj, request.POST)
- if form.is_valid():
- if form.cleaned_data['delete']:
- obj.delete()
- return http.HttpResponseRedirect('/myrpki/')
- else:
- form = forms.PrefixDeleteForm(obj)
-
- return render('myrpki/prefix_view.html', { 'form': form,
- 'addr': obj, 'form': form, 'parent': parent_set }, request)
+ return PrefixDeleteView(request, pk, form_class=forms.PrefixDeleteForm)()
@handle_required
def roa_request_delete_view(request, pk):
diff --git a/portal-gui/rpkigui/templates/myrpki/asn_view.html b/portal-gui/rpkigui/templates/myrpki/asn_view.html
index c7720a47..3ab2fe25 100644
--- a/portal-gui/rpkigui/templates/myrpki/asn_view.html
+++ b/portal-gui/rpkigui/templates/myrpki/asn_view.html
@@ -64,6 +64,15 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e
</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 }}">
diff --git a/portal-gui/rpkigui/templates/myrpki/dashboard.html b/portal-gui/rpkigui/templates/myrpki/dashboard.html
index 49f03a7d..ea925653 100644
--- a/portal-gui/rpkigui/templates/myrpki/dashboard.html
+++ b/portal-gui/rpkigui/templates/myrpki/dashboard.html
@@ -99,33 +99,30 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e
</div><!-- roas -->
<div style="border: outset">
-<h1 style="text-align: center">Unallocated Resources</h1>
-{% if asns or ars %}
-<table>
- {% for asn in asns %}
- <tr>
- <td style='text-align: left'><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></td>
- <td><a href="{{ asn.get_absolute_url }}/allocate">give</a></td>
- </tr>
- {% endfor %}
- {% for addr in ars %}
- <tr>
- <td style='text-align: left'><a href="{{ addr.get_absolute_url }}">{{ addr }}</a></td>
- <td>
- <a href="{{ addr.get_absolute_url }}/allocate">give</a>
- | <a href="{{ addr.get_absolute_url }}/split">split</a>
- {% if addr.is_prefix %}
- | <a href="{{ addr.get_absolute_url }}/roa">roa</a>
- {% endif %}
- </td>
- </tr>
- {% endfor %}
-</table>
-{% else %}
-<p>-- none --
-{% endif %}
+ <h1 style="text-align: center">Unallocated Resources</h1>
+ {% if asns or ars %}
+
+ {% if asns %}
+ <ul>
+ {% for asn in asns %}
+ <li>{{ asn.as_ul|safe }}
+ {% endfor %} <!-- ASNs -->
+ </ul>
+ {% endif %}
-</ul>
+ {% if ars %}
+ <ul>
+ {% for addr in ars %}
+ <li>{{ addr.as_ul|safe }}
+ {% endfor %} <!-- addrs -->
+ </ul>
+ {% endif %}
+
+ {% else %}
+ <p>-- none --
+ {% endif %}
+
+ </ul>
</div>
</span>
{% endblock %}
diff --git a/portal-gui/rpkigui/templates/myrpki/prefix_view.html b/portal-gui/rpkigui/templates/myrpki/prefix_view.html
index bd38ee5f..d26cbdff 100644
--- a/portal-gui/rpkigui/templates/myrpki/prefix_view.html
+++ b/portal-gui/rpkigui/templates/myrpki/prefix_view.html
@@ -61,6 +61,15 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e
</table>
{% endif %} <!-- roa requests -->
+{% 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 }}">