aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Elkins <melkins@tislabs.com>2013-02-23 00:19:54 +0000
committerMichael Elkins <melkins@tislabs.com>2013-02-23 00:19:54 +0000
commitb033927cf90652a52ce2d71d95a4572527602d8f (patch)
tree4aa1a98ee8ae4777c649b12bfffc0fa4e38bdf28
parent2ccd83627db9c376ad285c0bcea697cbb21b0e09 (diff)
add new roa creation form allowing multiple roas to be entered
add links for creating roas for IP ranges by automatically splitting the range into prefixes closes #399 closes #420 svn path=/trunk/; revision=5055
-rw-r--r--rpkid/rpki/gui/app/forms.py24
-rw-r--r--rpkid/rpki/gui/app/templates/app/dashboard.html12
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html64
-rw-r--r--rpkid/rpki/gui/app/templates/app/roarequest_multi_form.html27
-rw-r--r--rpkid/rpki/gui/app/urls.py2
-rw-r--r--rpkid/rpki/gui/app/views.py205
6 files changed, 293 insertions, 41 deletions
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py
index 355f9d2c..1d354521 100644
--- a/rpkid/rpki/gui/app/forms.py
+++ b/rpkid/rpki/gui/app/forms.py
@@ -159,11 +159,24 @@ class ROARequest(forms.Form):
Handles both IPv4 and IPv6."""
prefix = forms.CharField(
- widget=forms.TextInput(attrs={'autofocus': 'true', 'size': '50'})
+ widget=forms.TextInput(attrs={
+ 'autofocus': 'true', 'placeholder': 'Prefix',
+ 'class': 'span4'
+ })
+ )
+ max_prefixlen = forms.CharField(
+ required=False,
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'Max len',
+ 'class': 'span1'
+ })
+ )
+ asn = forms.IntegerField(
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'ASN',
+ 'class': 'span1'
+ })
)
- max_prefixlen = forms.CharField(required=False,
- label='Max Prefix Length')
- asn = forms.IntegerField(label='AS')
confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False)
def __init__(self, *args, **kwargs):
@@ -173,8 +186,11 @@ class ROARequest(forms.Form):
"""
conf = kwargs.pop('conf', None)
+ kwargs['auto_id'] = False
super(ROARequest, self).__init__(*args, **kwargs)
self.conf = conf
+ self.inline = True
+ self.use_table = False
def _as_resource_range(self):
"""Convert the prefix in the form to a
diff --git a/rpkid/rpki/gui/app/templates/app/dashboard.html b/rpkid/rpki/gui/app/templates/app/dashboard.html
index 0af4bae6..3349c3cd 100644
--- a/rpkid/rpki/gui/app/templates/app/dashboard.html
+++ b/rpkid/rpki/gui/app/templates/app/dashboard.html
@@ -81,10 +81,7 @@
<tr>
<td>{{ addr }}</td>
<td>
- {# if addr can be represented as a prefix, add a button for issuing a roa #}
- {% if addr.is_prefix %}
- <a class="btn btn-mini" title="Create ROA using this prefix" href="{% url rpki.gui.app.views.roa_create %}?prefix={{ addr }}">ROA</a>
- {% endif %}
+ <a class="btn btn-mini" title="Create ROA using this prefix" href="{% url rpki.gui.app.views.roa_create_multi %}?roa={{ addr }}"><i class="icon-plus-sign"></i> ROA</a>
</td>
</tr>
{% endfor %} <!-- addrs -->
@@ -99,10 +96,7 @@
<tr>
<td>{{ addr }}</td>
<td>
- {# if addr can be represented as a prefix, add a button for issuing a roa #}
- {% if addr.is_prefix %}
- <a class="btn btn-mini" title='create roa using this prefix' href="{% url rpki.gui.app.views.roa_create %}?prefix={{ addr }}">roa</a>
- {% endif %}
+ <a class="btn btn-mini" title='create roa using this prefix' href="{% url rpki.gui.app.views.roa_create_multi %}?roa={{ addr }}"><i class="icon-plus-sign"></i> ROA</a>
</td>
</tr>
{% endfor %} <!-- addrs -->
@@ -132,7 +126,7 @@
</tr>
{% endfor %}
</table>
-<a class="btn" href="{% url rpki.gui.app.views.roa_create %}"><i class="icon-plus-sign"></i> Create</a>
+<a class="btn" href="{% url rpki.gui.app.views.roa_create_multi %}"><i class="icon-plus-sign"></i> Create</a>
</div>
<div class="span6">
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
new file mode 100644
index 00000000..cd0ed3c2
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html
@@ -0,0 +1,64 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Confirm ROA Requests</h1>
+</div>
+
+<div class='row-fluid'>
+ <div class='span6'>
+ <div class='alert alert-block-message alert-warning'>
+ <p><strong>Please confirm</strong> that you would like to create the following ROA(s).
+ The accompanying table indicates how the validation status may change as a result.
+ </div>
+
+ <table class='table table-condensed table-striped'>
+ <tr>
+ <th>Prefix</th>
+ <th>Max Length</th>
+ <th>AS</th>
+ </tr>
+ {% for roa in roas %}
+ <tr>
+ <td>{{ roa.prefix }}</td>
+ <td>{{ roa.max_prefixlen }}</td>
+ <td>{{ roa.asn }}</td>
+ </tr>
+ {% endfor %}
+ </table>
+
+ <form method='POST' action='{% url rpki.gui.app.views.roa_create_multi_confirm %}'>
+ {% csrf_token %}
+ {{ formset.management_form }}
+ {% for form in formset %}
+ {% include "app/bootstrap_form.html" %}
+ {% endfor %}
+
+ <div class='form-actions'>
+ <input class='btn btn-primary' type='submit' value='Create'/>
+ <a class='btn' href='{% url rpki.gui.app.views.dashboard %}'>Cancel</a>
+ </div>
+ </form>
+ </div>
+
+ <div class='span6'>
+ <h2>Matched Routes</h2>
+
+ <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></td>
+ </tr>
+ {% endfor %}
+ </table>
+ </div>
+
+</div>
+{% endblock content %}
diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_multi_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_multi_form.html
new file mode 100644
index 00000000..91151036
--- /dev/null
+++ b/rpkid/rpki/gui/app/templates/app/roarequest_multi_form.html
@@ -0,0 +1,27 @@
+{% extends "app/app_base.html" %}
+
+{% block content %}
+<div class='page-title'>
+ <h1>Create ROA Requests</h1>
+</div>
+
+<form method='POST' action='{{ request.get_full_path }}'>
+ {% csrf_token %}
+ {{ formset.management_form }}
+ {% for form in formset %}
+ <div class="controls controls-row">
+ {{ form.prefix }}
+ {{ form.max_prefixlen }}
+ {{ form.asn }}
+ <label class="checkbox inline span1">{{ form.DELETE }} Delete</label>
+ {% if form.errors %}<span class="help-inline">{{ form.errors }}</span>{% endif %}
+ {% if form.non_field_errors %}<span class="help-inline">{{ form.non_field_errors }}</span>{% endif %}
+ </div>
+ {% endfor %}
+
+ <div class="form-actions">
+ <input class="btn" type="submit" value="Preview">
+ <a class="btn" href="{% url rpki.gui.app.views.dashboard %}">Cancel</a>
+ </div>
+</form>
+{% endblock %}
diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py
index c5b85c39..8f11c5be 100644
--- a/rpkid/rpki/gui/app/urls.py
+++ b/rpkid/rpki/gui/app/urls.py
@@ -49,7 +49,9 @@ urlpatterns = patterns(
(r'^repo/(?P<pk>\d+)/delete$', views.repository_delete),
(r'^roa/(?P<pk>\d+)/$', views.roa_detail),
(r'^roa/create$', views.roa_create),
+ (r'^roa/create_multi$', views.roa_create_multi),
(r'^roa/confirm$', views.roa_create_confirm),
+ (r'^roa/confirm_multi$', views.roa_create_multi_confirm),
(r'^roa/(?P<pk>\d+)/delete$', views.roa_delete),
(r'^route/$', views.route_view),
(r'^route/(?P<pk>\d+)/$', views.route_detail),
diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py
index de4ea488..3e1cdbe2 100644
--- a/rpkid/rpki/gui/app/views.py
+++ b/rpkid/rpki/gui/app/views.py
@@ -26,13 +26,14 @@ import os.path
from tempfile import NamedTemporaryFile
from django.contrib.auth.decorators import login_required
-from django.shortcuts import get_object_or_404, render
+from django.shortcuts import get_object_or_404, render, redirect
from django.utils.http import urlquote
from django import http
from django.core.urlresolvers import reverse
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
from rpki.irdb import Zookeeper, ChildASN, ChildNet
from rpki.gui.app import models, forms, glue, range_list
@@ -447,6 +448,39 @@ def roa_detail(request, pk):
})
+def get_covered_routes(rng, max_prefixlen, asn):
+ """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)
+
+ return routes
+
+
@handle_required
def roa_create(request):
"""Present the user with a form to create a ROA.
@@ -464,33 +498,34 @@ 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)
+# # 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)
form = forms.ROARequestConfirm(initial={'asn': asn,
@@ -513,6 +548,90 @@ def roa_create(request):
return render(request, 'app/roarequest_form.html', {'form': form})
+class ROARequestFormSet(BaseFormSet):
+ """There is no way to pass arbitrary keyword arguments to the form
+ constructor, so we have to override BaseFormSet to allow it.
+
+ """
+ def __init__(self, *args, **kwargs):
+ self.conf = kwargs.pop('conf')
+ super(ROARequestFormSet, self).__init__(*args, **kwargs)
+
+ def _construct_forms(self):
+ self.forms = []
+ for i in xrange(self.total_form_count()):
+ self.forms.append(self._construct_form(i, conf=self.conf))
+
+
+def split_with_default(s):
+ xs = s.split(',')
+ if len(xs) == 1:
+ return xs[0], None
+ return xs
+
+
+@handle_required
+def roa_create_multi(request):
+ """version of roa_create that uses a formset to allow entry of multiple
+ roas on a single page.
+
+ ROAs can be specified in the GET query string, as such:
+
+ ?roa=prefix,asn
+
+ Mulitple ROAs may be specified:
+
+ ?roa=prefix,asn+roa=prefix2,asn2
+
+ If an IP range is specified, it will be automatically split into multiple
+ prefixes:
+
+ ?roa=1.1.1.1-2.2.2.2,42
+
+ The ASN may optionally be omitted.
+
+ """
+
+ conf = request.session['handle']
+ if request.method == 'GET':
+ init = []
+ for x in request.GET.getlist('roa'):
+ rng, asn = split_with_default(x)
+ rng = resource_range_ip.parse_str(rng)
+ if rng.can_be_prefix:
+ init.append({'asn': asn, 'prefix': str(rng)})
+ else:
+ v = []
+ rng.chop_into_prefixes(v)
+ init.extend([{'asn': asn, 'prefix': str(p)} for p in v])
+ formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
+ can_delete=True)(initial=init, conf=conf)
+ elif request.method == 'POST':
+ formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
+ extra=0, can_delete=True)(request.POST, request.FILES, conf=conf)
+ if formset.is_valid():
+ routes = []
+ v = []
+ # as of Django 1.4.5 we still can't use formset.cleaned_data
+ # because deleted forms are not excluded, which causes an
+ # AttributeError to be raised.
+ for form in formset:
+ if hasattr(form, 'cleaned_data') and form.cleaned_data: # exclude empty forms
+ 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'))
+ routes.extend(get_covered_routes(rng, max_prefixlen, asn))
+ v.append({'prefix': str(rng), 'max_prefixlen': max_prefixlen,
+ 'asn': asn})
+ # if there were no rows, skip the confirmation step
+ if v:
+ formset = formset_factory(forms.ROARequestConfirm, extra=0)(initial=v)
+ return render(request, 'app/roarequest_confirm_multi_form.html',
+ {'routes': routes, 'formset': formset, 'roas': v})
+ return render(request, 'app/roarequest_multi_form.html',
+ {'formset': formset})
+
+
@handle_required
def roa_create_confirm(request):
"""This function is called when the user confirms the creation of a ROA
@@ -543,6 +662,36 @@ def roa_create_confirm(request):
@handle_required
+def roa_create_multi_confirm(request):
+ """This function is called when the user confirms the creation of a ROA
+ request. It is responsible for updating the IRDB.
+
+ """
+ conf = request.session['handle']
+ log = request.META['wsgi.errors']
+ if request.method == 'POST':
+ formset = formset_factory(forms.ROARequestConfirm, extra=0)(request.POST, request.FILES)
+ if formset.is_valid():
+ for cleaned_data in formset.cleaned_data:
+ asn = cleaned_data.get('asn')
+ prefix = cleaned_data.get('prefix')
+ rng = resource_range_ip.parse_str(prefix)
+ max_prefixlen = cleaned_data.get('max_prefixlen')
+ # Always create ROA requests with a single prefix.
+ # https://trac.rpki.net/ticket/32
+ roa = models.ROARequest.objects.create(issuer=conf, asn=asn)
+ v = 'IPv%d' % rng.version
+ roa.prefixes.create(version=v, prefix=str(rng.min),
+ prefixlen=rng.prefixlen(),
+ max_prefixlen=max_prefixlen)
+ Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now()
+ return redirect(dashboard)
+ # What should happen when the submission form isn't valid? For now
+ # just fall through and redirect back to the ROA creation form
+ return http.HttpResponseRedirect(reverse(roa_create_multi))
+
+
+@handle_required
def roa_delete(request, pk):
"""Handles deletion of a single ROARequest object.