diff options
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 4 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/models.py | 48 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/dashboard.html | 2 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/roa_detail.html | 7 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html | 3 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html | 3 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/route_detail.html | 53 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/routes_view.html | 35 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/base.html | 8 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templatetags/app_extras.py | 12 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templatetags/bootstrap_pager.py | 2 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 2 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 254 | ||||
-rw-r--r-- | rpkid/rpki/gui/cacheview/models.py | 10 | ||||
-rw-r--r-- | rpkid/rpki/gui/models.py | 1 | ||||
-rw-r--r-- | rpkid/rpki/gui/routeview/models.py | 33 |
16 files changed, 289 insertions, 188 deletions
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index e138b572..e74230b1 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -104,6 +104,10 @@ class ImportClientForm(forms.Form): xml = forms.FileField(label='XML file') +class ImportCSVForm(forms.Form): + csv = forms.FileField(label='CSV file') + + class UserCreateForm(forms.Form): username = forms.CharField(max_length=30) email = forms.CharField(max_length=30, diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index a2142b4f..64dcc656 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -118,6 +118,34 @@ class Conf(rpki.irdb.models.ResourceHolderCA): def roas(self): return ROARequest.objects.filter(issuer=self) + @property + def routes(self): + """Return all IPv4 routes covered by RPKI certs issued to this resource + holder. + + """ + # build a Q filter to select all RouteOrigin objects covered by + # prefixes in the resource holder's certificates + q = models.Q() + for p in ResourceRangeAddressV4.objects.filter(cert__conf=self): + q |= models.Q(prefix_min__gte=p.prefix_min, + prefix_max__lte=p.prefix_max) + return RouteOrigin.objects.filter(q) + + @property + def routes_v6(self): + """Return all IPv6 routes covered by RPKI certs issued to this resource + holder. + + """ + # build a Q filter to select all RouteOrigin objects covered by + # prefixes in the resource holder's certificates + q = models.Q() + for p in ResourceRangeAddressV6.objects.filter(cert__conf=self): + q |= models.Q(prefix_min__gte=p.prefix_min, + prefix_max__lte=p.prefix_max) + return RouteOriginV6.objects.filter(q) + class Meta: proxy = True @@ -187,6 +215,26 @@ class ROARequest(rpki.irdb.models.ROARequest): def get_absolute_url(self): return ('rpki.gui.app.views.roa_detail', [str(self.pk)]) + @property + def routes(self): + "Return all IPv4 routes covered by this roa prefix." + # this assumes one prefix per ROA + rng = self.prefixes.filter(version=4)[0].as_resource_range() + return rpki.gui.routeview.models.RouteOrigin.objects.filter( + prefix_min__lte=rng.max, + prefix_max__gte=rng.min + ) + + @property + def routes_v6(self): + "Return all IPv6 routes covered by this roa prefix." + # this assumes one prefix per ROA + rng = self.prefixes.filter(version=6)[0].as_resource_range() + return rpki.gui.routeview.models.RouteOriginV6.objects.filter( + prefix_min__lte=rng.max, + prefix_max__gte=rng.min + ) + class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix): class Meta: diff --git a/rpkid/rpki/gui/app/templates/app/dashboard.html b/rpkid/rpki/gui/app/templates/app/dashboard.html index a60ba780..d03e0846 100644 --- a/rpkid/rpki/gui/app/templates/app/dashboard.html +++ b/rpkid/rpki/gui/app/templates/app/dashboard.html @@ -131,6 +131,8 @@ {% endfor %} </table> <a class="btn" href="{% url "rpki.gui.app.views.roa_create_multi" %}"><i class="icon-plus-sign"></i> Create</a> +<a class="btn" href="{% url "roa-import" %}" help="import a CSV file containing ROA requests"><i class="icon-upload"></i> Import</a> +<a class="btn" href="{% url "roa-export" %}" help="download a CSV file containing ROA requests"><i class="icon-download"></i> Export</a> </div> <div class="span6"> diff --git a/rpkid/rpki/gui/app/templates/app/roa_detail.html b/rpkid/rpki/gui/app/templates/app/roa_detail.html index 89a07fd8..f939846a 100644 --- a/rpkid/rpki/gui/app/templates/app/roa_detail.html +++ b/rpkid/rpki/gui/app/templates/app/roa_detail.html @@ -1,5 +1,6 @@ {% extends "app/app_base.html" %} {% load url from future %} +{% load app_extras %} {% block content %} <div class="page-header"> @@ -22,9 +23,9 @@ <h3>Covered Routes</h3> <p>This table lists currently announced routes which are covered by prefixes included in this ROA. <table class="table"> - <tr><th>Prefix</th><th>AS</th></tr> - {% for r in routes %} - <tr><td>{{ r.as_resource_range }}</td><td>{{ r.asn }}</td></tr> + <tr><th>Prefix</th><th>AS</th><th>Validity</th></tr> + {% for r in object.routes %} + <tr><td>{{ r.as_resource_range }}</td><td>{{ r.asn }}</td><td>{% validity_label r.status %}</tr> {% endfor %} </table> </div> diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html index 28de172b..7dc3ec2b 100644 --- a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html +++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html @@ -1,5 +1,6 @@ {% extends "app/app_base.html" %} {% load url from future %} +{% load app_extras %} {% block content %} <div class='page-header'> @@ -49,7 +50,7 @@ <tr> <td>{{ r.get_prefix_display }}</td> <td>{{ r.asn }}</td> - <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + <td>{% validity_label r.newstatus %}</td> </tr> {% endfor %} </table> diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html index 69b0542e..4a06a4aa 100644 --- a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html +++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html @@ -1,5 +1,6 @@ {% extends "app/app_base.html" %} {% load url from future %} +{% load app_extras %} {% block content %} <div class='page-title'> @@ -55,7 +56,7 @@ <tr> <td>{{ r.get_prefix_display }}</td> <td>{{ r.asn }}</td> - <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + <td>{% validity_label r.newstatus %}</td> </tr> {% endfor %} </table> diff --git a/rpkid/rpki/gui/app/templates/app/route_detail.html b/rpkid/rpki/gui/app/templates/app/route_detail.html index 286250a6..0477b4a6 100644 --- a/rpkid/rpki/gui/app/templates/app/route_detail.html +++ b/rpkid/rpki/gui/app/templates/app/route_detail.html @@ -1,4 +1,5 @@ {% extends "app/app_base.html" %} +{% load app_extras %} {# template for displaying the list of ROAs covering a specific route #} @@ -10,12 +11,16 @@ <div class="row-fluid"> <div class="span12 well"> <table class="table table-striped table-condensed"> - <tr><th>Prefix</th><th>AS</th></tr> - <tr> - <td>{{ object.as_resource_range }}</td> - <td>{{ object.asn }} - </td> - </tr> + <thead> + <tr><th>Prefix</th><th>AS</th><th>Validity</th></tr> + </thead> + <tbody> + <tr> + <td>{{ object.as_resource_range }}</td> + <td>{{ object.asn }}</td> + <td>{% validity_label object.status %}</td> + </tr> + </tbody> </table> </div> </div> @@ -25,22 +30,26 @@ <p>The table below lists all ROAs which cover the route described above. <table class="table table-striped table-condensed"> - <tr> - <th>Prefix</th> - <th>Max Length</th> - <th>ASN</th> - <th>Expires</th> - <th>URI</th> - </tr> - {% for pfx in roa_prefixes %} - <tr> - <td>{{ pfx.as_resource_range }}</td> - <td>{{ pfx.max_length }}</td> - <td>{{ pfx.roas.all.0.asid }}</td> - <td>{{ pfx.roas.all.0.not_after }}</td> - <td>{{ pfx.roas.all.0.repo.uri }}</td> - </tr> - {% endfor %} + <thead> + <tr> + <th>Prefix</th> + <th>Max Length</th> + <th>ASN</th> + <th>Expires</th> + <th>URI</th> + </tr> + </thead> + <tbody> + {% for pfx in object.roa_prefixes %} + <tr> + <td>{{ pfx.as_resource_range }}</td> + <td>{{ pfx.max_length }}</td> + <td>{{ pfx.roas.all.0.asid }}</td> + <td>{{ pfx.roas.all.0.not_after }}</td> + <td>{{ pfx.roas.all.0.repo.uri }}</td> + </tr> + {% endfor %} + </tbody> </table> </div> </div> diff --git a/rpkid/rpki/gui/app/templates/app/routes_view.html b/rpkid/rpki/gui/app/templates/app/routes_view.html index 190f9ed8..8ea6da2e 100644 --- a/rpkid/rpki/gui/app/templates/app/routes_view.html +++ b/rpkid/rpki/gui/app/templates/app/routes_view.html @@ -1,6 +1,7 @@ {% extends "app/app_base.html" %} {% load url from future %} {% load bootstrap_pager %} +{% load app_extras %} {% block sidebar_extra %} <p> @@ -23,21 +24,25 @@ rcynic cache updated<br> This view shows currently advertised routes for the prefixes listed in resource certs received from RPKI parents. <table class='table table-striped table-condensed'> - <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='{% url "rpki.gui.app.views.route_detail" r.pk %}' title='display ROAs covering this prefix'><i class="icon-info-sign"></i></a> - </td> - </tr> - {% endfor %} + <thead> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + </thead> + <tbody> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td> + {% validity_label r.status %} + <a href='{% url "rpki.gui.app.views.route_detail" r.pk %}' title='display ROAs covering this prefix'><i class="icon-info-sign"></i></a> + </td> + </tr> + {% endfor %} + </tbody> </table> {% bootstrap_pager request routes %} diff --git a/rpkid/rpki/gui/app/templates/base.html b/rpkid/rpki/gui/app/templates/base.html index aa0f6a58..45f1b791 100644 --- a/rpkid/rpki/gui/app/templates/base.html +++ b/rpkid/rpki/gui/app/templates/base.html @@ -41,6 +41,14 @@ </div> <div class="span10"> + {% if messages %} + {% for message in messages %} + {# this will break if there is more than one tag, but don't expect to use that feature #} + <div class='alert alert-{{ message.tags }}'> + {{ message }} + </div> + {% endfor %} + {% endif %} {% block content %}{% endblock %} </div> </div> diff --git a/rpkid/rpki/gui/app/templatetags/app_extras.py b/rpkid/rpki/gui/app/templatetags/app_extras.py index 25de4467..e7b90b38 100644 --- a/rpkid/rpki/gui/app/templatetags/app_extras.py +++ b/rpkid/rpki/gui/app/templatetags/app_extras.py @@ -2,12 +2,24 @@ 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.capitalize() + @register.simple_tag def verbose_name_plural(qs): "Return the verbose name for the model class." return qs.model._meta.verbose_name_plural.capitalize() + +css = { + 'valid': 'label-success', + 'invalid': 'label-important' +} + + +@register.simple_tag +def validity_label(validity): + return '<span class="label %s">%s</span>' % (css.get(validity, ''), validity) diff --git a/rpkid/rpki/gui/app/templatetags/bootstrap_pager.py b/rpkid/rpki/gui/app/templatetags/bootstrap_pager.py index ff141c9d..422c460c 100644 --- a/rpkid/rpki/gui/app/templatetags/bootstrap_pager.py +++ b/rpkid/rpki/gui/app/templatetags/bootstrap_pager.py @@ -11,6 +11,8 @@ class BootstrapPagerNode(template.Node): def render(self, context): request = self.request.resolve(context) pager_object = self.pager_object.resolve(context) + if pager_object.paginator.num_pages == 1: + return '' r = ['<div class="pagination"><ul>'] if pager_object.number == 1: r.append('<li class="disabled"><a>«</a></li>') diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index de6521ec..b5b3ab5c 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -57,6 +57,8 @@ urlpatterns = patterns( (r'^roa/create_multi$', views.roa_create_multi), (r'^roa/confirm$', views.roa_create_confirm), (r'^roa/confirm_multi$', views.roa_create_multi_confirm), + url(r'^roa/export$', views.roa_export, name='roa-export'), + url(r'^roa/import$', views.roa_import, name='roa-import'), (r'^roa/(?P<pk>\d+)/delete$', views.roa_delete), url(r'^roa/(?P<pk>\d+)/clone$', views.roa_clone, name="roa-clone"), (r'^route/$', views.route_view), diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index 78c3462a..b8a49903 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -34,15 +34,17 @@ from django.contrib.auth.models import User from django.views.generic import DetailView from django.core.paginator import Paginator from django.forms.formsets import formset_factory, BaseFormSet +import django.db.models +from django.contrib import messages -from rpki.irdb import Zookeeper, ChildASN, ChildNet +from rpki.irdb import Zookeeper, ChildASN, ChildNet, ROARequestPrefix from rpki.gui.app import models, forms, glue, range_list from rpki.resource_set import (resource_range_as, resource_range_ip, roa_prefix_ipv4) from rpki import sundial import rpki.exceptions -from rpki.gui.cacheview.models import ROAPrefixV4, ROA +from rpki.gui.cacheview.models import ROA from rpki.gui.routeview.models import RouteOrigin from rpki.gui.decorators import tls_required @@ -448,44 +450,46 @@ def child_delete(request, pk): def roa_detail(request, pk): conf = request.session['handle'] obj = get_object_or_404(conf.roas, pk=pk) - pfx = obj.prefixes.all()[0].as_resource_range() - routes = RouteOrigin.objects.filter(prefix_min__gte=pfx.min, - prefix_max__lte=pfx.max) - return render(request, 'app/roa_detail.html', { - 'object': obj, - 'routes': routes, - }) + return render(request, 'app/roa_detail.html', {'object': obj}) def get_covered_routes(rng, max_prefixlen, asn): - """find list of matching routes""" + """Returns a list of routeview.models.RouteOrigin objects which would + change validation status if a ROA were created with the parameters to this + function. + + A "newstatus" attribute is monkey-patched on the RouteOrigin objects which + can be used in the template. "status" remains the current validation + status of the object. + """ + + qs = RouteOrigin.objects.filter( + prefix_min__lte=rng.max, + prefix_max__gte=rng.min + ) routes = [] - match = roa_match(rng) - for route, roas in match: - validate_route(route, roas) + for route in qs: + status = route.status # 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 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 = 'label-success' - else: - route.status = 'invalid' - route.status_label = 'label-important' - elif route.status == 'invalid': + # if the AS matches, it is valid, otherwise invalid + if (route.asn != 0 and route.asn == asn and route.prefixlen <= max_prefixlen): + route.newstatus = 'valid' + else: + route.newstatus = 'invalid' + routes.append(route) + elif 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 = 'label-success' - - routes.append(route) + if route.asn != 0 and route.asn == asn and route.prefixlen <= max_prefixlen: + route.newstatus = 'valid' + routes.append(route) return routes @@ -507,33 +511,6 @@ def roa_create(request): 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 = 'label-success' -# else: -# route.status = 'invalid' -# route.status_label = '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 = 'label-success' -# -# routes.append(route) routes = get_covered_routes(rng, max_prefixlen, asn) prefix = str(rng) @@ -629,9 +606,13 @@ def roa_create_multi(request): asn = form.cleaned_data.get('asn') rng = resource_range_ip.parse_str(form.cleaned_data.get('prefix')) max_prefixlen = int(form.cleaned_data.get('max_prefixlen')) + # FIXME: This won't do the right thing in the event that a + # route is covered by multiple ROAs created in the form. + # You will see duplicate entries, each with a potentially + # different validation status. routes.extend(get_covered_routes(rng, max_prefixlen, asn)) v.append({'prefix': str(rng), 'max_prefixlen': max_prefixlen, - 'asn': asn}) + 'asn': asn}) # if there were no rows, skip the confirmation step if v: formset = formset_factory(forms.ROARequestConfirm, extra=0)(initial=v) @@ -714,23 +695,37 @@ def roa_delete(request, pk): if request.method == 'POST': roa.delete() Zookeeper(handle=conf.handle).run_rpkid_now() - return http.HttpResponseRedirect(reverse(dashboard)) + return redirect(reverse(dashboard)) ### Process GET ### - obj = roa.prefixes.all()[0] - roa_pfx = obj.as_roa_prefix() - match = roa_match(obj.as_resource_range()) - 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} + # note: assumes we only generate one prefix per ROA + roa_prefix = roa.prefixes.all()[0] + rng = roa_prefix.as_resource_range() - # exclude ROAs which seem to match this request and display the result routes = [] - for route, roas in match: - qs = roas.exclude(asid=roa.asn, **args) - validate_route(route, qs) + for route in roa.routes: + # select all roas which cover this route + # excluding the current roa + # note: we can't identify the exact ROA here, because we only know what + # was requested to rpkid + roas = route.roas.exclude( + asid=roa.asn, + prefixes__prefix_min=rng.min, + prefixes__prefix_max=rng.max, + prefixes__max_length=roa_prefix.max_prefixlen + ) + + # subselect exact match + if route.asn != 0 and roas.filter(asid=route.asn, + prefixes__max_length__gte=route.prefixlen).exists(): + route.newstatus = 'valid' + elif roas.exists(): + route.newstatus = 'invalid' + else: + route.newstatus = 'unknown' + # we may want to ignore routes for which there is no status change, + # but the user may want to see that nothing has changed explicitly routes.append(route) return render(request, 'app/roarequest_confirm_delete.html', @@ -745,6 +740,48 @@ def roa_clone(request, pk): reverse(roa_create_multi) + "?roa=" + str(roa.prefixes.all()[0].as_roa_prefix()) ) + +@handle_required +def roa_import(request): + """Import CSV containing ROA declarations.""" + if request.method == 'POST': + form = forms.ImportCSVForm(request.POST, request.FILES) + if form.is_valid(): + import tempfile + tmp = tempfile.NamedTemporaryFile(suffix='.csv', prefix='roas', delete=False) + tmp.write(request.FILES['csv'].read()) + tmp.close() + z = Zookeeper(handle=request.session['handle']) + z.load_roa_requests(tmp.name) + z.run_rpkid_now() + os.unlink(tmp.name) + messages.success(request, 'Successfully imported ROAs.') + return redirect(dashboard) + else: + form = forms.ImportCSVForm() + return render(request, 'app/app_form.html', { + 'form_title': 'Import ROAs from CSV', + 'form': form, + 'cancel_url': reverse(dashboard) + }) + + +@handle_required +def roa_export(request): + """Export CSV containing ROA declarations.""" + # FIXME: remove when Zookeeper can do this + import cStringIO + import csv + f = cStringIO.StringIO() + csv = csv.writer(f, delimiter=' ') + conf = request.session['handle'] + # each roa prefix gets a unique group so rpkid will issue separate roas + for group, roapfx in enumerate(ROARequestPrefix.objects.filter(roa_request__issuer=conf)): + csv.writerow([str(roapfx.as_roa_prefix()), roapfx.roa_request.asn, '%s-%d' % (conf.handle, group)]) + resp = http.HttpResponse(f.getvalue(), mimetype='application/csv') + resp['Content-Disposition'] = 'attachment; filename=roas.csv' + return resp + class GhostbusterDetailView(DetailView): def get_queryset(self): @@ -821,60 +858,6 @@ def refresh(request): return http.HttpResponseRedirect(reverse(dashboard)) -def roa_match(rng): - """Return a list of tuples of matching routes and roas.""" - if rng.min.version == 6: - route_manager = models.RouteOriginV6.objects - pfx = 'prefixes_v6' - else: - route_manager = models.RouteOrigin.objects - pfx = 'prefixes' - - rv = [] - # return a max of 50 routes - for obj in route_manager.filter(prefix_min__gte=rng.min, - prefix_max__lte=rng.max)[:50]: - # 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)) - - return rv - - -def validate_route(route, roas): - """Annotate the route object with its validation status. - - `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 = '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 = 'label-success' - route.status = 'valid' - # 4. otherwise the route is invalid - else: - route.status_label = 'label-important' - route.status = 'invalid' - - return route - - @handle_required def route_view(request): """ @@ -883,39 +866,20 @@ def route_view(request): """ conf = request.session['handle'] - log = request.META['wsgi.errors'] count = request.GET.get('count', 25) page = request.GET.get('page', 1) - routes = [] - for p in models.ResourceRangeAddressV4.objects.filter(cert__conf=conf): - 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__conf=conf): - r = p.as_resource_range() - print >>log, 'querying for routes matching %s' % r - routes.extend([validate_route(*x) for x in roa_match(r)]) - - paginator = Paginator(routes, count) - content = paginator.page(page) - + paginator = Paginator(conf.routes, count) + routes = paginator.page(page) ts = dict((attr['name'], attr['ts']) for attr in models.Timestamp.objects.values()) return render(request, 'app/routes_view.html', - {'routes': content, 'timestamp': ts}) + {'routes': routes, 'timestamp': ts}) def route_detail(request, pk): - """Show a list of ROAs that match a given route.""" - # FIXME only supports IPv4 routes + """Show a list of ROAs that match a given IPv4 route.""" route = get_object_or_404(models.RouteOrigin, pk=pk) - # select accepted ROAs which cover this route - # The rpki.net tool only generates a single prefix per ROA, but other tools - # may not, so we generate the list by roa prefix instead - qs = ROAPrefixV4.objects.filter(prefix_min__lte=route.prefix_min, - prefix_max__gte=route.prefix_max).select_related() - return render(request, 'app/route_detail.html', - {'object': route, 'roa_prefixes': qs}) + return render(request, 'app/route_detail.html', {'object': route}) @handle_required diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py index bef72079..9ba71f82 100644 --- a/rpkid/rpki/gui/cacheview/models.py +++ b/rpkid/rpki/gui/cacheview/models.py @@ -177,6 +177,13 @@ class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4): roa_cls = rpki.resource_set.roa_prefix_ipv4 + @property + def routes(self): + """return all routes covered by this roa prefix""" + return RouteOrigin.objects.filter( + prefix_min__lte=self.prefix_max, + prefix_max__gte=self.prefix_min) + class Meta: ordering = ('prefix_min',) @@ -227,3 +234,6 @@ class Ghostbuster(SignedObject): if self.email_address: return self.email_address return self.telephone + + +from rpki.gui.routeview.models import RouteOrigin diff --git a/rpkid/rpki/gui/models.py b/rpkid/rpki/gui/models.py index d6073c2f..90e5c487 100644 --- a/rpkid/rpki/gui/models.py +++ b/rpkid/rpki/gui/models.py @@ -84,6 +84,7 @@ class Prefix(models.Model): """ return self.range_cls(self.prefix_min, self.prefix_max) + @property def prefixlen(self): "Returns the prefix length for the prefix in this object." return self.as_resource_range().prefixlen() diff --git a/rpkid/rpki/gui/routeview/models.py b/rpkid/rpki/gui/routeview/models.py index 321fde5d..33de6fb2 100644 --- a/rpkid/rpki/gui/routeview/models.py +++ b/rpkid/rpki/gui/routeview/models.py @@ -13,7 +13,7 @@ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -__version__ = '$Id' +__version__ = '$Id$' from django.db.models import PositiveIntegerField import rpki.gui.models @@ -28,6 +28,33 @@ class RouteOrigin(rpki.gui.models.PrefixV4): return u"AS%d's route origin for %s" % (self.asn, self.get_prefix_display()) + @property + def roas(self): + "Return a queryset of ROAs which cover this route." + return cacheview.models.ROA.objects.filter( + prefixes__prefix_min__lte=self.prefix_min, + prefixes__prefix_max__gte=self.prefix_max + ) + + @property + def roa_prefixes(self): + "Return a queryset of ROA prefixes which cover this route." + return cacheview.models.ROAPrefixV4.objects.filter( + prefix_min__lte=self.prefix_min, + prefix_max__gte=self.prefix_max + ) + + @property + def status(self): + "Returns the validation status of this route origin object." + roas = self.roas + # subselect exact match + if self.asn != 0 and roas.filter(asid=self.asn, prefixes__max_length__gte=self.prefixlen).exists(): + return 'valid' + elif roas.exists(): + return 'invalid' + return 'unknown' + class Meta: # sort by increasing mask length (/16 before /24) ordering = ('prefix_min', '-prefix_max') @@ -44,3 +71,7 @@ class RouteOriginV6(rpki.gui.models.PrefixV6): class Meta: ordering = ('prefix_min', '-prefix_max') + + +# this goes at the end of the file to avoid problems with circular imports +from rpki.gui import cacheview |