diff options
Diffstat (limited to 'rpkid/rpki/gui/app')
-rw-r--r-- | rpkid/rpki/gui/app/AllocationTree.py | 148 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/__init__.py | 0 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/admin.py | 54 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/asnset.py | 40 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 147 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/glue.py | 139 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/misc.py | 47 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/models.py | 207 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 50 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 557 |
10 files changed, 1389 insertions, 0 deletions
diff --git a/rpkid/rpki/gui/app/AllocationTree.py b/rpkid/rpki/gui/app/AllocationTree.py new file mode 100644 index 00000000..73277234 --- /dev/null +++ b/rpkid/rpki/gui/app/AllocationTree.py @@ -0,0 +1,148 @@ +# $Id$ +""" +Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +from rpki.gui.app import misc, models +from rpki import resource_set + +class AllocationTree(object): + '''Virtual class representing a tree of unallocated resource ranges. + Keeps track of which subsets of a resource range have been + allocated.''' + + def __init__(self, resource): + self.resource = resource + self.range = resource.as_resource_range() + self.need_calc = True + + def calculate(self): + if self.need_calc: + self.children = [] + self.alloc = self.__class__.set_type() + self.unalloc = self.__class__.set_type() + + if self.is_allocated(): + self.alloc.append(self.range) + else: + for child in self.resource.children.all(): + c = self.__class__(child) + if c.unallocated(): + self.children.append(c) + self.alloc = self.alloc.union(c.alloc) + total = self.__class__.set_type() + total.append(self.range) + self.unalloc = total.difference(self.alloc) + self.need_calc=False + + def unallocated(self): + self.calculate() + return self.unalloc + + def as_ul(self): + '''Returns a string of the tree as an unordered HTML list.''' + s = [] + s.append('<a href="%s">%s</a>' % (self.resource.get_absolute_url(), self.resource)) + + # when the unallocated range is a subset of the current range, + # display the missing ranges + u = self.unallocated() + if len(u) != 1 or self.range != u[0]: + s.append(' (missing: ') + s.append(', '.join(str(x) for x in u)) + s.append(')') + + # quick access links + if self.resource.parent: + s.append(' | <a href="%s/delete">delete</a>' % (self.resource.get_absolute_url(),)) + s.append(' | <a href="%s/allocate">give</a>' % (self.resource.get_absolute_url(),)) + if self.range.min != self.range.max: + s.append(' | <a href="%s/split">split</a>' % (self.resource.get_absolute_url(),)) + # add type-specific actions + a = self.supported_actions() + if a: + s.extend(a) + + if self.children: + s.append('\n<ul>\n') + for c in self.children: + s.append('<li>' + c.as_ul()) + s.append('\n</ul>') + + return ''.join(s) + + def supported_actions(self): + '''Virtual method allowing subclasses to add actions to the HTML list.''' + return None + + @classmethod + def from_resource_range(cls, resource): + if isinstance(resource, resource_set.resource_range_as): + return AllocationTreeAS(resource) + if isinstance(resource, resoute_set.resource_range_ip): + return AllocationTreeIP(resource) + raise ValueError, 'Unsupported resource range type' + +class AllocationTreeAS(AllocationTree): + set_type = resource_set.resource_set_as + + def __init__(self, *args, **kwargs): + AllocationTree.__init__(self, *args, **kwargs) + self.conf = misc.top_parent(self.resource).from_cert.all()[0].parent.conf + + def is_allocated(self): + '''Returns true if this AS has been allocated to a child or + used in a ROA request.''' + # FIXME: detect use in ROA requests + + if self.resource.allocated: + return True + + # for individual ASNs + if self.range.min == self.range.max: + # is this ASN used in any roa? + if self.conf.roas.filter(asn=self.range.min): + return True + + return False + +class AllocationTreeIP(AllocationTree): + '''virtual class representing a tree of IP address ranges.''' + + @classmethod + def from_prefix(cls, prefix): + r = prefix.as_resource_range() + if isinstance(r, resource_set.resource_range_ipv4): + return AllocationTreeIPv4(prefix) + elif isinstance(r, resource_set.resource_range_ipv6): + return AllocationTreeIPv6(prefix) + raise ValueError, 'Unsupported IP range type' + + def supported_actions(self): + '''add a link to issue a roa for this IP range''' + return[' | <a href="%s/roa">roa</a>' % (self.resource.get_absolute_url(),)] + + def is_allocated(self): + '''Return True if this IP range is allocated to a child or used + in a ROA request.''' + return self.resource.allocated or self.resource.roa_requests.count() + +class AllocationTreeIPv4(AllocationTreeIP): + set_type = resource_set.resource_set_ipv4 + +class AllocationTreeIPv6(AllocationTreeIP): + set_type = resource_set.resource_set_ipv6 + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/__init__.py b/rpkid/rpki/gui/app/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/app/__init__.py diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py new file mode 100644 index 00000000..f4134214 --- /dev/null +++ b/rpkid/rpki/gui/app/admin.py @@ -0,0 +1,54 @@ +""" +$Id$ + +Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +from django import forms +from django.contrib import admin +from rpki.gui.app import models + +class ConfAdmin( admin.ModelAdmin ): + pass + +class ChildAdmin( admin.ModelAdmin ): + pass + +class AddressRangeAdmin( admin.ModelAdmin ): + pass + +class AsnAdmin( admin.ModelAdmin ): + pass + +class ParentAdmin( admin.ModelAdmin ): + pass + +class RoaAdmin( admin.ModelAdmin ): + pass + +class ResourceCertAdmin(admin.ModelAdmin): + pass + +class RoaRequestAdmin(admin.ModelAdmin): + pass + +admin.site.register(models.Conf, ConfAdmin) +admin.site.register(models.Child, ChildAdmin) +admin.site.register(models.AddressRange, AddressRangeAdmin) +admin.site.register(models.Asn, AsnAdmin) +admin.site.register(models.Parent, ParentAdmin) +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/asnset.py b/rpkid/rpki/gui/app/asnset.py new file mode 100644 index 00000000..6a2d562c --- /dev/null +++ b/rpkid/rpki/gui/app/asnset.py @@ -0,0 +1,40 @@ +# $Id$ +""" +Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +class asnset(object): + """A set-like object for containing sets of ASN values.""" + v = set() + + def __init__(self, init=None): + """ + May be initialized from a comma separated list of positive integers. + """ + if init: + self.v = set(int(x) for x in init.split(',') if x.strip() != '') + if any([x for x in self.v if x < 0]): + raise ValueError, "Can't contain negative values." + + def __str__(self): + return ','.join(str(x) for x in sorted(self.v)) + + def __iter__(self): + return iter(self.v) + + def add(self, n): + assert isinstance(n, int) + assert n > 0 + self.v.add(n) diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py new file mode 100644 index 00000000..f7f51936 --- /dev/null +++ b/rpkid/rpki/gui/app/forms.py @@ -0,0 +1,147 @@ +# $Id$ +""" +Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +from django import forms + +import rpki.ipaddrs + +from rpki.gui.app import models, misc +from rpki.gui.app.asnset import asnset + +class AddConfForm(forms.Form): + handle = forms.CharField(required=True, + help_text='your handle for your rpki instance') + run_rpkid = forms.BooleanField(required=False, initial=True, + label='Run rpkid?', + help_text='do you want to run your own instance of rpkid?') + rpkid_server_host = forms.CharField(initial='rpkid.example.org', + label='rpkid hostname', + help_text='publicly visible hostname for your rpkid instance') + rpkid_server_port = forms.IntegerField(initial=4404, + label='rpkid port') + run_pubd = forms.BooleanField(required=False, initial=False, + label='Run pubd?', + help_text='do you want to run your own instance of pubd?') + pubd_server_host = forms.CharField(initial='pubd.example.org', + label='pubd hostname', + help_text='publicly visible hostname for your pubd instance') + pubd_server_port = forms.IntegerField(initial=4402, label='pubd port') + pubd_contact_info = forms.CharField(initial='repo-man@rpki.example.org', + label='Pubd contact', + help_text='email address for the operator of your pubd instance') + +class ImportForm(forms.Form): + '''Form used for uploading parent/child identity xml files''' + handle = forms.CharField() + xml = forms.FileField() + +def PrefixSplitForm(parent, *args, **kwargs): + class _wrapper(forms.Form): + prefix = forms.CharField(max_length=200, help_text='CIDR or range') + + def clean(self): + p = self.cleaned_data.get('prefix') + try: + r = misc.parse_resource_range(p) + except ValueError, err: + print err + raise forms.ValidationError, 'invalid prefix or range' + # we get AssertionError is the range is misordered (hi before lo) + except AssertionError, err: + print err + raise forms.ValidationError, 'invalid prefix or range' + pr = parent.as_resource_range() + if r.min < pr.min or r.max > pr.max: + raise forms.ValidationError, \ + 'range is outside parent range' + if r.min == pr.min and r.max == pr.max: + raise forms.ValidationError, \ + 'range is equal to parent' + if parent.allocated: + raise forms.ValidationError, 'prefix is assigned to child' + for p in parent.children.all(): + c = p.as_resource_range() + if c.min <= r.min <= c.max or c.min <= r.max <= c.max: + raise forms.ValidationError, \ + 'overlap with another child prefix: %s' % (c,) + + return self.cleaned_data + return _wrapper(*args, **kwargs) + +def PrefixAllocateForm(iv, child_set, *args, **kwargs): + class _wrapper(forms.Form): + child = forms.ModelChoiceField(initial=iv, queryset=child_set, + required=False) + return _wrapper(*args, **kwargs) + +def PrefixRoaForm(prefix, *args, **kwargs): + prefix_range = prefix.as_resource_range() + + class _wrapper(forms.Form): + asns = forms.CharField(max_length=200, required=False, + help_text='Comma-separated list of ASNs') + max_length = forms.IntegerField(required=False, + min_value=prefix_range.prefixlen(), + max_value=prefix_range.datum_type.bits) + + def clean_max_length(self): + v = self.cleaned_data.get('max_length') + if not v: + v = prefix_range.prefixlen() + return v + + def clean_asns(self): + try: + v = asnset(self.cleaned_data.get('asns')) + return ','.join(str(x) for x in sorted(v)) + except ValueError: + raise forms.ValidationError, \ + 'Must be a list of integers separated by commas.' + return self.cleaned_data['asns'] + + def clean(self): + if not prefix.is_prefix(): + raise forms.ValidationError, \ + '%s can not be represented as a prefix.' % (prefix,) + if prefix.allocated: + raise forms.ValidationError, \ + 'Prefix is allocated to a child.' + return self.cleaned_data + + return _wrapper(*args, **kwargs) + +def PrefixDeleteForm(prefix, *args, **kwargs): + class _wrapped(forms.Form): + delete = forms.BooleanField(label='Yes, I want to delete this prefix:') + + def clean(self): + v = self.cleaned_data.get('delete') + if v: + if not prefix.parent: + raise forms.ValidationError, \ + 'Can not delete prefix received from parent' + if prefix.allocated: + raise forms.ValidationError, 'Prefix is allocated to child' + if prefix.roa_requests.all(): + raise forms.ValidationError, 'Prefix is used in your ROAs' + if prefix.children.all(): + raise forms.ValidationError, 'Prefix has been subdivided' + return self.cleaned_data + + return _wrapped(*args, **kwargs) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py new file mode 100644 index 00000000..0204ff9e --- /dev/null +++ b/rpkid/rpki/gui/app/glue.py @@ -0,0 +1,139 @@ +# $Id$ +""" +Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +# $Id$ + +from __future__ import with_statement + +import os +import os.path +import csv +import stat +import sys + +from django.db.models import F + +import rpki +import rpki.config + +from rpki.gui import settings +from rpki.gui.app import models + +def conf(handle): + return settings.CONFDIR + '/' + handle + +#def form_to_conf(data): +# """Write out a rpki.conf based on the given form data.""" +# handle = data['handle'] +# confdir = settings.MYRPKI_DATA_DIR + '/' + handle +# if os.path.exists(confdir): +# raise RuntimeError, '%s: directory already exists!' % (confdir, ) +# os.makedirs(confdir) +# template = open(settings.MYRPKI_DATA_DIR + '/examples/rpki.conf', 'r').read() +# # stuff the appropriate output directory into the dict +# data['MYRPKI_DATA_DIR'] = confdir +# with open(confdir + '/rpki.conf', 'w') as conf: +# print >>conf, template % data +# invoke_rpki(handle, ['initialize']) + +def invoke_rpki(handle, args): + """Invoke the myrpki cli for the specified configuration.""" + myrpki_dir = confdir(handle) + config = myrpki_dir + '/rpki.conf' + # default rpki.conf uses relative paths, so chdir() to the repo first + cmd = 'cd %s && %s %s' % (settings.MYRPKI, myrpki_dir, ' '.join(['--config=' + config] + args)) + print >>sys.stderr, 'invoking', cmd + os.system(cmd) + +def read_file_from_handle(handle, fname): + """read a filename relative to the directory for the given resource handle. returns + a tuple of (content, mtime)""" + with open(conf(handle) + '/' + fname, 'r') as fp: + data = fp.read() + mtime = os.fstat(fp.fileno())[stat.ST_MTIME] + return data, mtime + +#def read_identity(handle): +# fname = settings.MYRPKI_DATA_DIR + '/' + handle + '/entitydb/identity.xml' +# with open(fname, 'r') as fp: +# data = fp.read() +# return data +read_identity = lambda h: read_file_from_handle(h, 'entitydb/identity.xml')[0] + +def read_child_response(handle, child): + fname = '%s/entitydb/children/%s.xml' % (conf(handle), child) + with open(fname, 'r') as fp: + data = fp.read() + return data + +# FIXME - remove this once rpki.myrpki.csv_writer is an object with a +# .file field +def csv_writer(f): + return csv.writer(f, dialect = csv.get_dialect("excel-tab")) + +def output_asns(path, handle): + '''Write out csv file containing asns delegated to my children.''' + qs = models.Asn.objects.filter(lo=F('hi'), allocated__in=handle.children.all()) + with open(path, 'w') as f: + w = csv_writer(f) + w.writerows([asn.allocated.handle, asn.lo] for asn in qs) + +def output_prefixes(path, handle): + '''Write out csv file containing prefixes delegated to my children.''' + qs = models.AddressRange.objects.filter(allocated__in=handle.children.all()) + with open(path, 'w') as f: + w = csv_writer(f) + w.writerows([p.allocated.handle, p.as_resource_range()] for p in qs) + +def output_roas(path, handle): + '''Write out csv file containing my roas.''' + qs = models.RoaRequest.objects.filter(roa__in=handle.roas.all()) + with open(path, 'w') as f: + w = csv_writer(f) + w.writerows([req.as_roa_prefix(), req.roa.asn, + '%s-group-%d' % (handle.handle, req.roa.pk)] for req in qs) + +def configure_daemons(handle): + args = ['configure_daemons'] + for hosted in handle.hosting.all(): + args.append(conf(hosted.handle) + '/myrpki.xml') + invoke_rpki(handle.handle, args) + +def configure_resources(handle): + '''Write out the csv files and invoke the myrpki.py command line tool.''' + # chdir to the repo dir since the default rpki.conf uses relative + # pathnames.. + os.chdir(conf(handle.handle)) + cfg = rpki.config.parser('rpki.conf', 'myrpki') + output_asns(cfg.get('asn_csv'), handle) + output_prefixes(cfg.get('prefix_csv'), handle) + output_roas(cfg.get('roa_csv'), handle) + run_rpkidemo = cfg.getboolean('run_rpkidemo', False) + if not run_rpkidemo: + run_rpkid = cfg.getboolean('run_rpkid') + if run_rpkid: + configure_daemons(handle) + else: + invoke_rpki(handle.handle, ['configure_resources']) + + # send the myrpki.xml to the rpkid hosting me + configure_daemons(handle.host) + + # process the response + invoke_rpki(handle.handle, ['configure_resources']) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/misc.py b/rpkid/rpki/gui/app/misc.py new file mode 100644 index 00000000..5d3cba93 --- /dev/null +++ b/rpkid/rpki/gui/app/misc.py @@ -0,0 +1,47 @@ +# $Id$ +""" +Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import rpki.resource_set +import rpki.ipaddrs + +def str_to_range(lo, hi): + """Convert IP address strings to resource_range_ip.""" + x = rpki.ipaddrs.parse(lo) + y = rpki.ipaddrs.parse(hi) + assert type(x) == type(y) + if isinstance(x, rpki.ipaddrs.v4addr): + return rpki.resource_set.resource_range_ipv4(x, y) + else: + return rpki.resource_set.resource_range_ipv6(x, y) + +def parse_resource_range(s): + '''Parse an IPv4/6 resource range.''' + # resource_set functions only accept str + if isinstance(s, unicode): + s = s.encode() + try: + return rpki.resource_set.resource_range_ipv4.parse_str(s) + except ValueError: + return rpki.resource_set.resource_range_ipv6.parse_str(s) + +def top_parent(prefix): + '''Returns the topmost resource from which the specified argument derives''' + while prefix.parent: + prefix = prefix.parent + return prefix + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py new file mode 100644 index 00000000..0efcd5ba --- /dev/null +++ b/rpkid/rpki/gui/app/models.py @@ -0,0 +1,207 @@ +# $Id$ +""" +Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import socket + +from django.db import models +from django.contrib.auth.models import User + +from rpki.gui.app.misc import str_to_range + +import rpki.resource_set +import rpki.exceptions + +class HandleField(models.CharField): + def __init__(self, **kwargs): + models.CharField.__init__(self, max_length=255, **kwargs) + +class IPAddressField(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> + in the rpkid schema.''' + handle = HandleField(unique=True, db_index=True) + owner = models.ManyToManyField(User) + + # NULL if self-hosted, otherwise the conf that is hosting us + host = models.ForeignKey('Conf', related_name='hosting', null=True) + + def __unicode__(self): + return self.handle + +class AddressRange(models.Model): + '''An address range/prefix.''' + lo = IPAddressField(blank=False) + hi = IPAddressField(blank=False) + # parent address range + parent = models.ForeignKey('AddressRange', related_name='children', + blank=True, null=True) + # child to which this resource is delegated + allocated = models.ForeignKey('Child', related_name='address_range', + blank=True, null=True) + + def __unicode__(self): + if self.lo == self.hi: + return u"%s" % (self.lo,) + + try: + # pretty print cidr + return unicode(self.as_resource_range()) + except socket.error, err: + print err + # work around for bug when hi/lo get reversed + except AssertionError, err: + print err + return u'%s - %s' % (self.lo, self.hi) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.address_view', [str(self.pk)]) + + def as_resource_range(self): + '''Convert to rpki.resource_set.resource_range_ip.''' + return str_to_range(self.lo, self.hi) + + def is_prefix(self): + '''Returns True if this address range can be represented as a + prefix.''' + try: + self.as_resource_range().prefixlen() + except rpki.exceptions.MustBePrefix, err: + print err + return False + return True + +class RoaRequest(models.Model): + roa = models.ForeignKey('Roa', related_name='from_roa_request') + max_length = models.IntegerField() + prefix = models.ForeignKey('AddressRange', related_name='roa_requests') + + def __unicode__(self): + return u'roa request for asn %d on %s-%d' % (self.roa.asn, self.prefix, + self.max_length) + + def as_roa_prefix(self): + '''Convert to a rpki.resouce_set.roa_prefix subclass.''' + r = self.prefix.as_resource_range() + if isinstance(r, rpki.resource_set.resource_set_ipv4): + return rpki.resource_set.roa_prefix_ipv4(r.min, r.prefixlen(), + self.max_length) + else: + return rpki.resource_set.roa_prefix_ipv6(r.min, r.prefixlen(), + self.max_length) + +class Asn(models.Model): + '''An ASN or range thereof.''' + lo = models.IntegerField(blank=False) + hi = models.IntegerField(blank=False) + # parent asn range + parent = models.ForeignKey('Asn', related_name='children', + blank=True, null=True) + # child to which this resource is delegated + allocated = models.ForeignKey('Child', related_name='asn', + blank=True, null=True) + + def __unicode__(self): + if self.lo == self.hi: + return u"ASN %d" % (self.lo,) + else: + return u"ASNs %d - %d" % (self.lo, self.hi) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.asn_view', [str(self.pk)]) + + def as_resource_range(self): + # we force conversion to long() here because resource_range_as() wants + # the type of both arguments to be identical, and models.IntegerField + # will be a long when the value is large + return rpki.resource_set.resource_range_as(long(self.lo), long(self.hi)) + +class Child(models.Model): + conf = models.ForeignKey(Conf, related_name='children') + handle = HandleField() # parent's name for child + + def __unicode__(self): + return u"%s's child %s" % (self.conf, self.handle) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.child_view', [self.handle]) + + class Meta: + verbose_name_plural = "children" + # children of a specific configuration should be unique + unique_together = ('conf', 'handle') + +class Parent(models.Model): + conf = models.ForeignKey(Conf, related_name='parents') + handle = HandleField() # my name for this parent + + def __unicode__(self): + return u"%s's parent %s" % (self.conf, self.handle) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.parent_view', [self.handle]) + + class Meta: + # parents of a specific configuration should be unique + unique_together = ('conf', 'handle') + +class ResourceCert(models.Model): + parent = models.ForeignKey(Parent, related_name='resources') + + # resources granted from my parent + asn = models.ManyToManyField(Asn, related_name='from_cert', blank=True, + null=True) + address_range = models.ManyToManyField(AddressRange, + related_name='from_cert', blank=True, null=True) + + # unique id for this resource certificate + # FIXME: URLField(verify_exists=False) doesn't seem to work - the admin + # editor won't accept a rsync:// scheme as valid + uri = models.CharField(max_length=200) + + # certificate validity period + not_before = models.DateTimeField() + not_after = models.DateTimeField() + + def __unicode__(self): + return u"%s's resource cert from parent %s" % (self.parent.conf.handle, + self.parent.handle) + +class Roa(models.Model): + '''Maps an ASN to the set of prefixes it can originate routes for. + This differs from a real ROA in that prefixes from multiple + parents/resource certs can be selected. The glue module contains + code to split the ROAs into groups by common resource certs.''' + + conf = models.ForeignKey(Conf, related_name='roas') + asn = models.IntegerField() + active = models.BooleanField() + + # the resource cert from which all prefixes for this roa are derived + cert = models.ForeignKey(ResourceCert, related_name='roas') + + def __unicode__(self): + return u"%s's ROA for %d" % (self.conf, self.asn) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py new file mode 100644 index 00000000..24b311e5 --- /dev/null +++ b/rpkid/rpki/gui/app/urls.py @@ -0,0 +1,50 @@ +# $Id$ +""" +Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +from django.conf.urls.defaults import * +from django.views.generic.list_detail import object_list +from rpki.gui.app import views + +urlpatterns = patterns('', + (r'^$', views.dashboard), +# (r'^conf/add$', views.conf_add), + (r'^conf/export$', views.conf_export), + (r'^conf/list$', views.conf_list), + (r'^conf/select$', views.conf_select), +# (r'^import/parent$', views.parent_import), +# (r'^import/child$', views.child_import), + (r'^parent/(?P<parent_handle>[^/]+)$', views.parent_view), + (r'^child/(?P<child_handle>[^/]+)$', views.child_view), +# (r'^parent/(?P<parent_handle>[^/]+)/address$', views.parent_address), +# (r'^parent/(?P<parent_handle>[^/]+)/asn$', views.parent_asn), + (r'^address/(?P<pk>\d+)$', views.address_view), + (r'^address/(?P<pk>\d+)/split$', views.prefix_split_view), + (r'^address/(?P<pk>\d+)/allocate$', views.prefix_allocate_view), + (r'^address/(?P<pk>\d+)/roa$', views.prefix_roa_view), + (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'^roa/(?P<pk>\d+)/delete$', views.roa_request_delete_view), + (r'^demo/down/asns/(?P<self_handle>[^/]+)$', views.download_asns), + (r'^demo/down/prefixes/(?P<self_handle>[^/]+)$', views.download_prefixes), + (r'^demo/down/roas/(?P<self_handle>[^/]+)$', views.download_roas), + (r'^upload-parent-request/(?P<self_handle>[^/]+)$', views.upload_parent_request), + (r'^upload-repository-request/(?P<self_handle>[^/]+)$', views.upload_repository_request), + (r'^upload-myrpki-xml/(?P<self_handle>[^/]+)$', views.upload_myrpki_xml), +) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py new file mode 100644 index 00000000..6929ab36 --- /dev/null +++ b/rpkid/rpki/gui/app/views.py @@ -0,0 +1,557 @@ +# $Id$ +""" +Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +""" + +import email.utils +import os +import os.path +import tempfile +import sys + +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, render_to_response +from django.utils.http import urlquote +from django.template import RequestContext +from django.db import IntegrityError +from django import http +from django.views.generic.list_detail import object_list +from django.views.decorators.csrf import csrf_exempt +from django.conf import settings +from django.core.urlresolvers import reverse + +from rpki.gui.app import models, forms, glue, misc, AllocationTree +from rpki.gui.app.asnset import asnset + +debug = False + +# For each type of object, we have a detail view, a create view and +# an update view. We heavily leverage the generic views, only +# adding our own idea of authorization. + +def handle_required(f): + @login_required + def wrapped_fn(request, *args, **kwargs): + if 'handle' not in request.session: + if request.user.is_superuser: + conf = models.Conf.objects.all() + else: + conf = models.Conf.objects.filter(owner=request.user) + if conf.count() == 1: + handle = conf[0] + elif conf.count() == 0: + return render('myrpki/conf_empty.html', {}, request) + #return http.HttpResponseRedirect('/myrpki/conf/add') + else: + # Should reverse the view for this instead of hardcoding + # the URL. + return http.HttpResponseRedirect( + reverse(conf_list) + '?next=' + urlquote(request.get_full_path())) + request.session[ 'handle' ] = handle + return f(request, *args, **kwargs) + return wrapped_fn + +def render(template, context, request): + return render_to_response(template, context, + context_instance=RequestContext(request)) + +@handle_required +def dashboard(request): + '''The user's dashboard.''' + handle = request.session[ 'handle' ] + # ... pick out data for the dashboard and return it + # my parents + # the resources that my parents have given me + # the resources that I have accepted from my parents + # my children + # the resources that I have given my children + # my roas + + # get list of ASNs used in my ROAs + roa_asns = [r.asn for r in handle.roas.all()] + asns=[] + for a in models.Asn.objects.filter(from_cert__parent__in=handle.parents.all()): + f = AllocationTree.AllocationTreeAS(a) + if f.unallocated(): + asns.append(f) + + prefixes = [] + for p in models.AddressRange.objects.filter(from_cert__parent__in=handle.parents.all()): + f = AllocationTree.AllocationTreeIP.from_prefix(p) + if f.unallocated(): + prefixes.append(f) + + asns.sort(key=lambda x: x.range.min) + prefixes.sort(key=lambda x: x.range.min) + + return render('myrpki/dashboard.html', { 'conf': handle, 'asns': asns, + 'ars': prefixes }, request) + +#@login_required +#def conf_add(request): +# '''Allow the user to create a new configuration.''' +# errors = [] +# if request.method == 'POST': +# form = forms.AddConfForm(request.POST) +# if form.is_valid(): +# try: +# handle = form.cleaned_data['handle'] +# # ensure this user is in the group for this handle +# grps = request.user.groups.filter(name=handle) +# if len(grps) == 0: +# errors.append( +# 'You are not in the proper group for that handle.') +# else: +# conf = models.Conf.objects.create( +# handle=form.cleaned_data['handle'], owner=grps[0]) +# conf.save() +# glue.form_to_conf(form.cleaned_data) +# return http.HttpResponseRedirect('/myrpki/') +# # data model will ensure the handle is unique +# except IntegrityError, e: +# print e +# errors.append('That handle already exists.') +# else: +# errors.append("The form wasn't valid.") +# else: +# form = forms.AddConfForm() +# return render_to_response('myrpki/add_conf.html', +# { 'form': form, 'errors': errors }) + +@login_required +def conf_list(request): + """Allow the user to select a handle.""" + if request.user.is_superuser: + queryset = models.Conf.objects.all() + else: + queryset = models.Conf.objects.filter(owner=request.user) + return object_list(request, queryset, + template_name='myrpki/conf_list.html', template_object_name='conf', extra_context={ 'select_url' : reverse(conf_select) }) + +@login_required +def conf_select(request): + '''Change the handle for the current session.''' + if not 'handle' in request.GET: + return http.HttpResponseRedirect('/myrpki/conf/select') + handle = request.GET['handle'] + next_url = request.GET.get('next', reverse(dashboard)) + if next_url == '': + next_url = reverse(dashboard) + + if request.user.is_superuser: + conf = models.Conf.objects.filter(handle=handle) + else: + # since the handle is passed in as a parameter, need to verify that + # the user is actually in the group + conf = models.Conf.objects.filter(handle=handle, + owner=request.user) + if conf: + request.session['handle'] = conf[0] + return http.HttpResponseRedirect(next_url) + + return http.HttpResponseRedirect(reverse(conf_list) + '?next=' + next_url) + +def serve_xml(content, basename): + resp = http.HttpResponse(content , mimetype='application/xml') + resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename, ) + return resp + +@handle_required +def conf_export(request): + """Return the identity.xml for the current handle.""" + handle = request.session['handle'] + return serve_xml(glue.read_identity(handle.handle), 'identity') + +@handle_required +def parent_import(request): + handle = request.session['handle'].handle + errs = [] + if request.method == 'POST': + form = forms.ImportForm(request.POST, request.FILES) + if form.is_valid(): + input_file = tempfile.NamedTemporaryFile(delete=False) + try: + parent_handle = form.cleaned_data['handle'] + parent = models.Parent( + conf=request.session['handle'], handle=parent_handle) + parent.save() + + input_file.write(request.FILES['xml'].read()) + input_file.close() + + args = ['configure_parent', '--parent_handle=' + parent_handle, + input_file.name] + glue.invoke_rpki(handle, args) + + return http.HttpResponseRedirect('/myrpki/') + except IntegrityError, e: + print e + errs.append('A parent with that handle already exists.') + finally: + os.remove(input_file.name) + else: + print 'invalid form' + errs.append('The form was invalid.') + else: + form = forms.ImportForm() + return render('myrpki/xml_import.html', { 'form': form, + 'kind': 'parent', 'post_url': '/myrpki/import/parent', + 'errors': errs }, request) + +@handle_required +def parent_view(request, parent_handle): + """Detail view for a particular parent.""" + handle = request.session['handle'] + parent = get_object_or_404(handle.parents, handle__exact=parent_handle) + return render('myrpki/parent_view.html', { 'parent': parent }, request) + +@handle_required +def child_import(request): + handle = request.session['handle'].handle + if request.method == 'POST': + form = forms.ImportForm(request.POST, request.FILES) + if form.is_valid(): + input_file = tempfile.NamedTemporaryFile(delete=False) + try: + child_handle = form.cleaned_data['handle'] + child = models.Child( + conf=request.session['handle'], handle=child_handle, + validity=form.cleaned_data['validity']) + child.save() + + input_file.write(request.FILES['xml'].read()) + input_file.close() + args = ['configure_child', '--child_handle=' + child_handle, + input_file.name] + glue.invoke_rpki(handle, args) + + # send response back to user + return serve_xml(glue.read_child_response(handle, + child_handle), child_handle) + finally: + os.remove(input_file.name) + else: + form = forms.ImportForm() + return render('myrpki/xml_import.html', + { 'form': form, 'kind': 'child', + 'post_url': '/myrpki/import/child'}, request) + +def get_parents_or_404(handle, obj): + '''Return the Parent object(s) that the given address range derives + from, or raise a 404 error.''' + cert_set = misc.top_parent(obj).from_cert.filter(parent__in=handle.parents.all()) + if cert_set.count() == 0: + raise http.Http404, 'Object is not delegated from any parent' + return [c.parent for c in cert_set] + +@handle_required +def asn_view(request, pk): + '''view/subdivide an asn range.''' + handle = request.session['handle'] + obj = get_object_or_404(models.Asn.objects, pk=pk) + # ensure this resource range belongs to a parent of the current conf + parent_set = get_parents_or_404(handle, obj) + roas = handle.roas.filter(asn=obj.lo) # roas which contain this asn + unallocated = AllocationTree.AllocationTreeAS(obj).unallocated() + + return render('myrpki/asn_view.html', + { 'asn': obj, 'parent': parent_set, 'roas': roas, + 'unallocated' : unallocated }, request) + +@handle_required +def child_view(request, child_handle): + '''Detail view of child for the currently selected handle.''' + handle = request.session['handle'] + child = get_object_or_404(handle.children, handle__exact=child_handle) + + return render('myrpki/child_view.html', { 'child': child }, request) + +class PrefixView(object): + '''Extensible view for address ranges/prefixes. This view can be + subclassed to add form handling for editing the prefix.''' + + def __init__(self, request, pk, form_class=None): + self.handle = request.session['handle'] + self.obj = get_object_or_404(models.AddressRange.objects, pk=pk) + # ensure this resource range belongs to a parent of the current conf + self.parent_set = get_parents_or_404(self.handle, self.obj) + self.form = None + self.form_class = form_class + self.request = request + + def __call__(self, *args, **kwargs): + if self.request.method == 'POST': + resp = self.handle_post() + else: + resp = self.handle_get() + + # allow get/post handlers to return a custom response + if resp: + return resp + + u = AllocationTree.AllocationTreeIP.from_prefix(self.obj).unallocated() + + return render('myrpki/prefix_view.html', + { 'addr': self.obj, 'parent': self.parent_set, 'unallocated': u, 'form': self.form }, + self.request) + + def handle_get(self): + '''Virtual method for extending GET handling. Default action is + to call the form class constructor with the prefix object.''' + if self.form_class: + self.form = self.form_class(self.obj) + + def form_valid(self): + '''Virtual method for handling a valid form. Called by the default + implementation of handle_post().''' + pass + + def handle_post(self): + '''Virtual method for extending POST handling. Default implementation + creates a form object using the form_class in the constructor and passing + the prefix object. If the form's is_valid() method is True, it then + invokes this class's form_valid() method.''' + resp = None + if self.form_class: + self.form = self.form_class(self.obj, self.request.POST) + if self.form.is_valid(): + resp = self.form_valid() + return resp + +@handle_required +def address_view(request, pk): + return PrefixView(request, pk)() + +class PrefixSplitView(PrefixView): + '''Class for handling the prefix split form.''' + def form_valid(self): + r = misc.parse_resource_range(self.form.cleaned_data['prefix']) + obj = models.AddressRange(lo=str(r.min), hi=str(r.max), parent=self.obj) + obj.save() + return http.HttpResponseRedirect(obj.get_absolute_url()) + +@handle_required +def prefix_split_view(request, pk): + return PrefixSplitView(request, pk, form_class=forms.PrefixSplitForm)() + +class PrefixAllocateView(PrefixView): + '''Class to handle the allocation to child form.''' + def handle_get(self): + self.form = forms.PrefixAllocateForm( + self.obj.allocated.pk if self.obj.allocated else None, + self.handle.children.all()) + + def handle_post(self): + self.form = forms.PrefixAllocateForm(None, self.handle.children.all(), self.request.POST) + if self.form.is_valid(): + self.obj.allocated = self.form.cleaned_data['child'] + self.obj.save() + glue.configure_resources(self.handle) + return http.HttpResponseRedirect(self.obj.get_absolute_url()) + +@handle_required +def prefix_allocate_view(request, pk): + return PrefixAllocateView(request, pk)() + +def add_roa_requests(handle, prefix, asns, max_length): + for asid in asns: + if debug: + print 'searching for a roa for AS %d containing %s-%d' % (asid, prefix, max_length) + req_set = prefix.roa_requests.filter(roa__asn=asid, max_length=max_length) + if not req_set: + if debug: + print 'no roa for AS %d containing %s-%d' % (asid, prefix, max_length) + + # find ROAs for prefixes derived from the same resource cert + # as this prefix + certs = misc.top_parent(prefix).from_cert.all() + roa_set = handle.roas.filter(asn=asid, cert__in=certs) + + # FIXME: currently only creates a ROA/request for the first + # resource cert, not all of them + if roa_set: + roa = roa_set[0] + else: + if debug: + print 'creating new roa for AS %d containg %s-%d' % (asid, prefix, max_length) + # no roa is present for this ASN, create a new one + roa = models.Roa.objects.create(asn=asid, conf=handle, + active=False, cert=certs[0]) + roa.save() + + req = models.RoaRequest.objects.create(prefix=prefix, roa=roa, + max_length=max_length) + req.save() + +class PrefixRoaView(PrefixView): + '''Class for handling the ROA creation form.''' + def form_valid(self): + asns = asnset(self.form.cleaned_data['asns']) + add_roa_requests(self.handle, self.obj, asns, self.form.cleaned_data['max_length']) + glue.configure_resources(self.handle) + return http.HttpResponseRedirect(self.obj.get_absolute_url()) + +@handle_required +def prefix_roa_view(request, pk): + return PrefixRoaView(request, pk, form_class=forms.PrefixRoaForm)() + +class PrefixDeleteView(PrefixView): + def form_valid(self): + if self.form.cleaned_data['delete']: + self.obj.delete() + return http.HttpResponseRedirect('/myrpki/') + +@handle_required +def prefix_delete_view(request, pk): + return PrefixDeleteView(request, pk, form_class=forms.PrefixDeleteForm)() + +@handle_required +def roa_request_delete_view(request, pk): + '''Remove a roa request from a particular prefix.''' + handle = request.session['handle'] + obj = get_object_or_404(models.RoaRequest.objects, pk=pk) + prefix = obj.prefix + # ensure this resource range belongs to a parent of the current conf + parent_set = get_parents_or_404(handle, prefix) + + roa = obj.roa + obj.delete() + if not roa.from_roa_request.all(): + roa.delete() + glue.configure_resources(handle) + + return http.HttpResponseRedirect(prefix.get_absolute_url()) + +@handle_required +def asn_allocate_view(request, pk): + handle = request.session['handle'] + obj = get_object_or_404(models.Asn.objects, pk=pk) + # ensure this resource range belongs to a parent of the current conf + parent_set = get_parents_or_404(handle, obj) + + if request.method == 'POST': + form = forms.PrefixAllocateForm(None, handle.children.all(), request.POST) + if form.is_valid(): + obj.allocated = form.cleaned_data['child'] + obj.save() + glue.configure_resources(handle) + return http.HttpResponseRedirect(obj.get_absolute_url()) + else: + form = forms.PrefixAllocateForm(obj.allocated.pk if obj.allocated else None, + handle.children.all()) + + return render('myrpki/asn_view.html', { 'form': form, + 'asn': obj, 'form': form, 'parent': parent_set }, request) + +# this is similar to handle_required, except that the handle is given in URL +def handle_or_404(request, handle): + "ensure the requested handle is available to this user" + if request.user.is_superuser: + conf_set = models.Conf.objects.filter(handle=handle) + else: + conf_set = models.Conf.objects.filter(owner=request.user, handle=handle) + if not conf_set: + raise http.Http404, 'resource handle not found' + return conf_set[0] + +def serve_file(handle, fname, content_type): + content, mtime = glue.read_file_from_handle(handle, fname) + resp = http.HttpResponse(content , mimetype=content_type) + resp['Content-Disposition'] = 'attachment; filename=%s' % (os.path.basename(fname), ) + resp['Last-Modified'] = email.utils.formatdate(mtime, usegmt=True) + return resp + +@login_required +def download_csv(request, self_handle, fname): + conf = handle_or_404(request, self_handle) + return serve_file(conf.handle, fname + '.csv', 'text/csv') + +def download_asns(request, self_handle): + return download_csv(request, self_handle, 'asns') + +def download_roas(request, self_handle): + return download_csv(request, self_handle, 'roas') + +def download_prefixes(request, self_handle): + return download_csv(request, self_handle, 'prefixes') + +def get_parent_handle(conf): + "determine who my parent is. for now just assume its hardcoded into the django db" + parent_set = models.Parent.objects.filter(conf=conf) + if parent_set: + return parent_set[0].handle + else: + raise http.Http404, 'you have no parents' + +@csrf_exempt +@login_required +def upload_parent_request(request, self_handle): + conf = handle_or_404(request, self_handle) + parent_handle = get_parent_handle(conf) + + if request.method == 'POST': + input_file = tempfile.NamedTemporaryFile(delete=False) + input_file.write(request.raw_post_data) + input_file.close() + + args = ['configure_child', input_file.name ] + glue.invoke_rpki(parent_handle, args) + + os.remove(input_file.name) + + return serve_file(parent_handle, 'entitydb/children/%s.xml' % self_handle, 'application/xml') + +@csrf_exempt +@login_required +def upload_repository_request(request, self_handle): + conf = handle_or_404(request, self_handle) + parent_handle = get_parent_handle(conf) + + if request.method == 'POST': + input_file = tempfile.NamedTemporaryFile(delete=False) + input_file.write(request.raw_post_data) + input_file.close() + + args = ['configure_publication_client', input_file.name ] + glue.invoke_rpki(parent_handle, args) + + os.remove(input_file.name) + + # FIXME: this assumes that the parent is running pubd. the actual filename + # will be different if the parent is not running pubd. see + # rpki.myrpki.do_configure_publication_client() + return serve_file(parent_handle, 'entitydb/pubclients/%s.%s.xml' % (parent_handle, self_handle), 'application/xml') + +@csrf_exempt +@login_required +def upload_myrpki_xml(request, self_handle): + "handles POST of the myrpki.xml file for a given resource handle." + conf = handle_or_404(request, self_handle) + + if request.method == 'POST': + try: + fname = '%s/%s/myrpki.xml' % (settings.MYRPKI_DATA_DIR, self_handle,) + print >>sys.stderr, 'writing ', fname + myrpki_xml = open(fname, 'w') + myrpki_xml.write(request.raw_post_data) + myrpki_xml.close() + + glue.configure_daemons(conf.host) + except: + print >>sys.stderr, ''.join(sys.exc_info()) + + return serve_file(self_handle, 'myrpki.xml', 'application/xml') + +# vim:sw=4 ts=8 expandtab |