aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rpkid/rpki/gui/app/forms.py4
-rw-r--r--rpkid/rpki/gui/app/models.py48
-rw-r--r--rpkid/rpki/gui/app/templates/app/dashboard.html2
-rw-r--r--rpkid/rpki/gui/app/templates/app/roa_detail.html7
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_confirm_delete.html3
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html3
-rw-r--r--rpkid/rpki/gui/app/templates/app/route_detail.html53
-rw-r--r--rpkid/rpki/gui/app/templates/app/routes_view.html35
-rw-r--r--rpkid/rpki/gui/app/templates/base.html8
-rw-r--r--rpkid/rpki/gui/app/templatetags/app_extras.py12
-rw-r--r--rpkid/rpki/gui/app/templatetags/bootstrap_pager.py2
-rw-r--r--rpkid/rpki/gui/app/urls.py2
-rw-r--r--rpkid/rpki/gui/app/views.py254
-rw-r--r--rpkid/rpki/gui/cacheview/models.py10
-rw-r--r--rpkid/rpki/gui/models.py1
-rw-r--r--rpkid/rpki/gui/routeview/models.py33
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>&laquo;</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