aboutsummaryrefslogtreecommitdiff
path: root/rpkid
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid')
-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.