diff options
Diffstat (limited to 'portal-gui')
-rw-r--r-- | portal-gui/Makefile.in | 1 | ||||
-rw-r--r-- | portal-gui/rpkigui/myrpki/AllocationTree.py | 148 | ||||
-rw-r--r-- | portal-gui/rpkigui/myrpki/misc.py | 6 | ||||
-rw-r--r-- | portal-gui/rpkigui/myrpki/models.py | 3 | ||||
-rw-r--r-- | portal-gui/rpkigui/myrpki/views.py | 256 | ||||
-rw-r--r-- | portal-gui/rpkigui/templates/myrpki/asn_view.html | 9 | ||||
-rw-r--r-- | portal-gui/rpkigui/templates/myrpki/dashboard.html | 49 | ||||
-rw-r--r-- | portal-gui/rpkigui/templates/myrpki/prefix_view.html | 9 |
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 }}"> |