diff options
author | Michael Elkins <melkins@tislabs.com> | 2013-02-23 00:19:54 +0000 |
---|---|---|
committer | Michael Elkins <melkins@tislabs.com> | 2013-02-23 00:19:54 +0000 |
commit | b033927cf90652a52ce2d71d95a4572527602d8f (patch) | |
tree | 4aa1a98ee8ae4777c649b12bfffc0fa4e38bdf28 | |
parent | 2ccd83627db9c376ad285c0bcea697cbb21b0e09 (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.py | 24 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/dashboard.html | 12 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/roarequest_confirm_multi_form.html | 64 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/app/roarequest_multi_form.html | 27 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 2 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 205 |
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. |