diff options
author | Michael Elkins <melkins@tislabs.com> | 2011-03-24 21:20:02 +0000 |
---|---|---|
committer | Michael Elkins <melkins@tislabs.com> | 2011-03-24 21:20:02 +0000 |
commit | f8438285d83f460d6fdde9ad084bc8cf8a48673d (patch) | |
tree | ad3567305a1c9300e604438f5c63063a9b656528 /rpkid | |
parent | b9db5dadd233bb3faeae65eb0e6010cd73297f19 (diff) |
add support to the portal gui for generating ghostbuster requests
svn path=/rpkid/rpki/gui/app/admin.py; revision=3738
Diffstat (limited to 'rpkid')
-rw-r--r-- | rpkid/rpki/gui/app/admin.py | 10 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 56 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/glue.py | 35 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/models.py | 38 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/rpkigui/dashboard.html | 108 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html | 14 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html | 44 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html | 17 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html | 23 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 5 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 70 |
11 files changed, 380 insertions, 40 deletions
diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py index 8d2b7824..8b7cd24a 100644 --- a/rpkid/rpki/gui/app/admin.py +++ b/rpkid/rpki/gui/app/admin.py @@ -46,11 +46,15 @@ class ResourceCertAdmin(admin.ModelAdmin): class RoaRequestAdmin(admin.ModelAdmin): pass -admin.site.register(models.Conf, ConfAdmin) -admin.site.register(models.Child, ChildAdmin) +class GhostbusterAdmin(admin.ModelAdmin): + pass + admin.site.register(models.AddressRange, AddressRangeAdmin) +admin.site.register(models.Child, ChildAdmin) +admin.site.register(models.Conf, ConfAdmin) admin.site.register(models.Asn, AsnAdmin) +admin.site.register(models.Ghostbuster, GhostbusterAdmin) admin.site.register(models.Parent, ParentAdmin) +admin.site.register(models.ResourceCert, ResourceCertAdmin) admin.site.register(models.Roa, RoaAdmin) admin.site.register(models.RoaRequest, RoaRequestAdmin) -admin.site.register(models.ResourceCert, ResourceCertAdmin) diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index f7f51936..86c3d6d8 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -144,4 +144,60 @@ def PrefixDeleteForm(prefix, *args, **kwargs): return _wrapped(*args, **kwargs) +def GhostbusterForm(parent_qs, conf=None): + """ + Generate a ModelForm with the subset of parents for the current + resource handle. + + The 'conf' argument is required when creating a new object, in + order to specify the value of the 'conf' field in the new + Ghostbuster object. + """ + class wrapped(forms.ModelForm): + # override parent + parent = forms.ModelMultipleChoiceField(queryset=parent_qs, required=False, + help_text='use this record for a specific parent, or leave blank for all parents') + # override full_name. it is required in the db schema, but we allow the + # user to skip it and default from family+given name + full_name = forms.CharField(max_length=40, required=False, + help_text='automatically generated from family and given names if left blank') + + class Meta: + model = models.Ghostbuster + exclude = [ 'conf' ] + + def clean(self): + family_name = self.cleaned_data.get('family_name') + given_name = self.cleaned_data.get('given_name') + if not all([family_name, given_name]): + raise forms.ValidationError, 'Family and Given names must be specified' + + email = self.cleaned_data.get('email_address') + postal = self.cleaned_data.get('postal_address') + telephone = self.cleaned_data.get('telephone') + if not any([email, postal, telephone]): + raise forms.ValidationError, 'One of telephone, email or postal address must be specified' + + # if the full name is not specified, default to given+family + fn = self.cleaned_data.get('full_name') + if not fn: + self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name) + + return self.cleaned_data + + def save(self, *args, **kwargs): + if conf: + # the generic create_object view doesn't allow us to set + # the conf field, so wrap the save() method and set it + # here + kwargs['commit'] = False + obj = super(wrapped, self).save(*args, **kwargs) + obj.conf = conf + obj.save() + return obj + else: + return super(wrapped, self).save(*args, **kwargs) + + return wrapped + # vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py index a26910d8..70ec255e 100644 --- a/rpkid/rpki/gui/app/glue.py +++ b/rpkid/rpki/gui/app/glue.py @@ -95,6 +95,29 @@ def build_rpkid_caller(cfg, verbose=False): url = rpkid_base + "left-right", debug = verbose)) +def ghostbuster_to_vcard(gbr): + """ + Convert a Ghostbuster object into a vCard object. + """ + import vobject + + vcard = vobject.vCard() + vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, given=gbr.given_name) + # mapping from vCard type to Ghostbuster model field + # the ORG type is a sequence of organization unit names, so + # transform the org name into a tuple before stuffing into the + # vCard object + attrs = [ ('FN', 'full_name', None), + ('ADR', 'postal_address', None), + ('TEL', 'telephone', None), + ('ORG', 'organization', lambda x: (x,)), + ('EMAIL', 'email_address', None) ] + for vtype, field, transform in attrs: + v = getattr(gbr, field) + if v: + vcard.add(vtype).value = transform(v) if transform else v + return vcard.serialize() + def configure_resources(log, handle): """ This function should be called when resources for this resource @@ -142,8 +165,18 @@ def configure_resources(log, handle): valid_until = rpki.sundial.datetime.fromdatetime(child.valid_until) children.append((child.handle, asns, v4, v6, valid_until)) + ghostbusters = [] + for gbr in handle.ghostbusters.all(): + vcard = ghostbuster_to_vcard(gbr) + parent_set = gbr.parent.all() + if parent_set: + for p in parent_set: + ghostbusters.append((p, vcard)) + else: + ghostbusters.append((None, vcard)) + irdb = IRDB(cfg) - irdb.update(handle, roa_requests, children) + irdb.update(handle, roa_requests, children, ghostbusters) irdb.close() # for hosted handles, get the config for the rpkid host diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index 3b161a59..0a5b12ad 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -33,6 +33,10 @@ class IPAddressField(models.CharField): def __init__( self, **kwargs ): models.CharField.__init__(self, max_length=40, **kwargs) +class TelephoneField(models.CharField): + def __init__( self, **kwargs ): + models.CharField.__init__(self, max_length=40, **kwargs) + class Conf(models.Model): '''This is the center of the universe, also known as a place to have a handle on a resource-holding entity. It's the <self> @@ -222,4 +226,38 @@ class Roa(models.Model): def get_absolute_url(self): return ('rpki.gui.app.views.roa_view', [str(self.pk)]) +class Ghostbuster(models.Model): + """ + Stores the information require to fill out a vCard entry to populate + a ghostbusters record. + """ + full_name = models.CharField(max_length=40) + + # components of the vCard N type + family_name = models.CharField(max_length=20) + given_name = models.CharField(max_length=20) + additional_name = models.CharField(max_length=20, blank=True, null=True) + honorific_prefix = models.CharField(max_length=10, blank=True, null=True) + honorific_suffix = models.CharField(max_length=10, blank=True, null=True) + + email_address = models.EmailField(blank=True, null=True) + postal_address = models.CharField(blank=True, null=True, max_length=255) + organization = models.CharField(blank=True, null=True, max_length=255) + telephone = TelephoneField(blank=True, null=True) + + conf = models.ForeignKey(Conf, related_name='ghostbusters') + # parent can be null when using the same record for all parents + parent = models.ManyToManyField(Parent, related_name='ghostbusters', + blank=True, null=True, help_text='use this record for a specific parent, or leave blank for all parents') + + def __unicode__(self): + return u"%s's GBR: %s" % (self.conf, self.full_name) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.ghostbuster_view', [str(self.pk)]) + + class Meta: + ordering = [ 'family_name', 'given_name' ] + # vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html index dea03c31..b3e9fab2 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html +++ b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html @@ -4,16 +4,34 @@ table { border-collapse: collapse } th { border: solid 1px; padding: 1em } td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } +h2 { text-align:center; background-color:#dddddd } +{% endblock %} + +{% block sidebar %} +<ul class='compact'> + <li><a href="#parents">parents</a></li> + <li><a href="#children">children</a></li> + <li><a href="#roas">roas</a></li> + <li><a href="#ghostbusters">ghostbusters</a></li> + <li><a href="#unallocated">unallocated</a></li> +</ul> + +<ul class='compact'> + <li><a href="{% url rpki.gui.app.views.conf_export %}">export identity</a></li> + <li><a href="{% url rpki.gui.app.views.conf_list %}">select identity</a></li> +</ul> {% endblock %} {% block content %} -<p>Handle: {{ request.session.handle }} -| <a href="{% url rpki.gui.app.views.conf_export %}">export identity</a> -| <a href="{% url rpki.gui.app.views.conf_list %}">select</a> -<div style="border: inset"> -<h1 style="text-align: center">Parents</h1> +<p id='breadcrumb'>{{ request.session.handle }} > Dashboard</p> + +<h1>Dashboard</h1> + +<div class='separator'> +<a name='parents'><h2>Parents</h2></a> +{% if request.session.handle.parents.all %} <ul> {% for parent in request.session.handle.parents.all %} <li><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a> @@ -42,15 +60,15 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e {% endfor %} </ul> +{% else %} +<p style='font-style:italic'>none</p> +{% endif %} -<!-- -<a href="/myrpki/import/parent">[add]</a> ---> -</div> +</div><!--parents--> + +<div class='separator'> + <a name='children'><h2>Children</h2></a> -<span> -<div style="border: outset"> -<h1 style="text-align: center">Children</h1> {% if request.session.handle.children.all %} <ul> {% for child in request.session.handle.children.all %} @@ -73,7 +91,7 @@ td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1e <a href="/myrpki/import/child">[add]</a> --> {% else %} -<p>-- none -- +<p style='font-style:italic'>none</p> {% endif %} <p> @@ -82,32 +100,52 @@ Export (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.h </div> -<div style="border: outset"> <!-- ROAs --> -<h1 style="text-align: center">My ROA [request]s</h1> +<div class='separator'> <!-- ROAs --> + <a name='roas'><h2>ROA Requests</h2></a> -<table> -<tr> <th>Prefix</th> <th>ASN</th> </tr> + {% if request.session.handle.roas.all %} + <table> + <tr> <th>Prefix</th> <th>ASN</th> </tr> -{% for roa in request.session.handle.roas.all %} -<tr> - <td style='text-align: left'> + {% for roa in request.session.handle.roas.all %} + <tr> + <td style='text-align: left'> <ul style='list-style-position: outside'> -{% for req in roa.from_roa_request.all %} - <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a> -{% endfor %} + {% for req in roa.from_roa_request.all %} + <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a> + {% endfor %} </ul> - </td> - <td>{{ roa.asn }}</td> -</tr> -{% endfor %} -</table> - -<p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a> - + </td> + <td>{{ roa.asn }}</td> + </tr> + {% endfor %} + </table> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + + <p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a> </div><!-- roas --> -<div style="border: outset"> - <h1 style="text-align: center">Unallocated Resources</h1> +<div class='separator'><!-- ghostbusters --> +<a name='ghostbusters'><h2>Ghostbuster Requests</h2></a> + {% if request.session.handle.ghostbusters.all %} + <ul> + {% for gbr in request.session.handle.ghostbusters.all %} + <li><a href="{{ gbr.get_absolute_url }}">{{ gbr.full_name }}</a> | + <a href="{{ gbr.get_absolute_url }}/edit">edit</a> | + <a href="{{ gbr.get_absolute_url }}/delete">delete</a> + </li> + {% endfor %} + {% else %} +<p style='font-style:italic'>none</p> + {% endif %} +</ul> +<p><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></p> +</div> + +<div class='separator'> +<a name='unallocated'><h2>Unallocated Resources</h2></a> {% if asns or ars %} {% if asns %} @@ -127,10 +165,10 @@ Export (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.h {% endif %} {% else %} - <p>-- none -- +<p style='font-style:italic'>none</p> {% endif %} </ul> </div> -</span> + {% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html new file mode 100644 index 00000000..6abd315a --- /dev/null +++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html @@ -0,0 +1,14 @@ +{% extends "rpkigui/ghostbuster_detail.html" %} + +{% block extra %} + +<p> +Please confirm that you really want to delete this object by clicking Submit. +</p> + +<form method=POST action='{{ request.get_full_path }}'> + {% csrf_token %} + <input type='submit'> +</form> + +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html new file mode 100644 index 00000000..cb03ec4e --- /dev/null +++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block css %} +td { padding-right: 1em } +td.label { font-weight:bold } +{% endblock %} + +{% block sidebar %} +<ul class='compact'> + <li><a href='{{ object.get_absolute_url }}/edit'>edit</a></li> + <li><a href='{{ object.get_absolute_url }}/delete'>delete</a></li> +</ul> +{% endblock %} + +{% block content %} +<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> > {{ object.full_name }}</p> + +<h1>Ghostbuster View</h1> + +<table> + <tr><td class='label'>Full Name</td><td>{{ object.full_name }}</td></tr> + + {% if object.honorific_prefix %} + <tr><td class='label'>Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr> + {% endif %} + + {% if object.organization %} + <tr><td class='label'>Organization</td><td>{{ object.organization }}</td></tr> + {% endif %} + + {% if object.telephone %} + <tr><td class='label'>Telephone</td><td>{{ object.telephone }}</td></tr> + {% endif %} + + {% if object.email_address %} + <tr><td class='label'>Email</td><td>{{ object.email_address }}</td></tr> + {% endif %} + + {% if object.postal_address %} + <tr><td class='label'>Postal Address</td><td>{{ object.postal_address }}</td></tr> + {% endif %} +</table> +{% block extra %}{% endblock %} +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html new file mode 100644 index 00000000..8a1d32ff --- /dev/null +++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} + +<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> > Edit</p> + +<h1>Edit Ghostbuster Request</h1> + +<form action='{{ request.get_full_path }}' method='POST'> + {% csrf_token %} + <table> +{{ form.as_table }} + +</table> + <input type='submit' /> +</form> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html new file mode 100644 index 00000000..6890782d --- /dev/null +++ b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} + +{% block sidebar %} +<ul class='compact'> + <li><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></li> +</ul> +{% endblock %} + +{% block content %} +<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > Ghostbusters</p> + +<h1>Ghostbuster Requests</h1> + +{% if object_list %} +<ul> + {% for obj in object_list %} + <li><a href="{{ obj.get_absolute_url }}">{{ obj.full_name }}</a> | <a href="{{obj.get_absolute_url}}/edit">edit</a> | <a href="{{obj.get_absolute_url}}/delete">delete</a></li> + {% endfor %} +</ul> +{% else %} +<p style='font-style:italic'>none</p> +{% endif %} +{% endblock %} diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index f71dc9d3..f2020d0d 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -33,6 +33,11 @@ urlpatterns = patterns('', (r'^address/(?P<pk>\d+)/delete$', views.prefix_delete_view), (r'^asn/(?P<pk>\d+)$', views.asn_view), (r'^asn/(?P<pk>\d+)/allocate$', views.asn_allocate_view), + (r'^gbr/$', views.ghostbusters_list), + (r'^gbr/create$', views.ghostbuster_create), + (r'^gbr/(?P<pk>\d+)$', views.ghostbuster_view), + (r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit), + (r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete), (r'^roa/(?P<pk>\d+)$', views.roa_view), (r'^roareq/(?P<pk>\d+)$', views.roa_request_view), (r'^roareq/(?P<pk>\d+)/delete$', views.roa_request_delete_view), diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index ad5f87a6..936a9108 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -25,7 +25,8 @@ from django.shortcuts import get_object_or_404, render_to_response from django.utils.http import urlquote from django.template import RequestContext from django import http -from django.views.generic.list_detail import object_list +from django.views.generic.list_detail import object_list, object_detail +from django.views.generic.create_update import delete_object, update_object, create_object from django.core.urlresolvers import reverse from rpki.gui.app import models, forms, glue, misc, AllocationTree, settings @@ -531,4 +532,71 @@ def roa_view(request, pk): """not yet implemented""" return +@handle_required +def ghostbusters_list(request): + """ + Display a list of all ghostbuster requests for the current Conf. + """ + conf = request.session['handle'] + + return object_list(request, queryset=conf.ghostbusters.all(), template_name='rpkigui/ghostbuster_list.html') + +@handle_required +def ghostbuster_view(request, pk): + """ + Display an individual ghostbuster request. + """ + conf = request.session['handle'] + + return object_detail(request, queryset=conf.ghostbusters.all(), object_id=pk, template_name='rpkigui/ghostbuster_detail.html') + +@handle_required +def ghostbuster_delete(request, pk): + conf = request.session['handle'] + + # verify that the object is owned by this conf + obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + + # modeled loosely on the generic delete_object() view. + if request.method == 'POST': + obj.delete() + glue.configure_resources(request.META['wsgi.errors'], conf) + return http.HttpResponseRedirect(reverse(ghostbusters_list)) + else: + return render('rpkigui/ghostbuster_confirm_delete.html', { 'object': obj }, request) + +def _ghostbuster_edit(request, obj=None): + """ + Common code for create/edit. + """ + conf = request.session['handle'] + form_class = forms.GhostbusterForm(conf.parents.all()) + if request.method == 'POST': + form = form_class(request.POST, request.FILES, instance=obj) + if form.is_valid(): + # use commit=False for the creation case, otherwise form.save() + # will fail due to schema constraint violation because conf is + # NULL + obj = form.save(commit=False) + obj.conf = conf + obj.save() + glue.configure_resources(request.META['wsgi.errors'], conf) + return http.HttpResponseRedirect(obj.get_absolute_url()) + else: + form = form_class(instance=obj) + return render('rpkigui/ghostbuster_form.html', { 'form': form }, request) + +@handle_required +def ghostbuster_edit(request, pk): + conf = request.session['handle'] + + # verify that the object is owned by this conf + obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + + return _ghostbuster_edit(request, obj) + +@handle_required +def ghostbuster_create(request): + return _ghostbuster_edit(request) + # vim:sw=4 ts=8 expandtab |