From 4cd9eb22f754c0e9ad905a055fcb94899fd7007f Mon Sep 17 00:00:00 2001 From: Benjamin Graillot <graillot@crans.org> Date: Sun, 21 Nov 2021 13:26:47 +0100 Subject: [PATCH] [prefix_delegation] Add prefix_delegation app --- prefix_delegation/__init__.py | 0 prefix_delegation/admin.py | 27 +++++ prefix_delegation/api/__init__.py | 0 prefix_delegation/api/serializers.py | 63 ++++++++++ prefix_delegation/api/urls.py | 7 ++ prefix_delegation/api/view.py | 12 ++ prefix_delegation/apps.py | 7 ++ prefix_delegation/migrations/__init__.py | 0 prefix_delegation/models.py | 146 +++++++++++++++++++++++ prefix_delegation/tests.py | 3 + prefix_delegation/views.py | 3 + 11 files changed, 268 insertions(+) create mode 100644 prefix_delegation/__init__.py create mode 100644 prefix_delegation/admin.py create mode 100644 prefix_delegation/api/__init__.py create mode 100644 prefix_delegation/api/serializers.py create mode 100644 prefix_delegation/api/urls.py create mode 100644 prefix_delegation/api/view.py create mode 100644 prefix_delegation/apps.py create mode 100644 prefix_delegation/migrations/__init__.py create mode 100644 prefix_delegation/models.py create mode 100644 prefix_delegation/tests.py create mode 100644 prefix_delegation/views.py diff --git a/prefix_delegation/__init__.py b/prefix_delegation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prefix_delegation/admin.py b/prefix_delegation/admin.py new file mode 100644 index 0000000..adf1d70 --- /dev/null +++ b/prefix_delegation/admin.py @@ -0,0 +1,27 @@ +from django.contrib import admin + +from .models import Prefix, DelegatedPrefix, Nameserver, DsRecord, DelegatedReverseDns + + +class DelegatedPrefixInline(admin.TabularInline): + extra = 0 + model = DelegatedPrefix + autocomplete_fields = ('owner',) + +@admin.register(Prefix) +class PrefixAdmin(admin.ModelAdmin): + list_display = ('name', 'prefix', 'length', 'delegated_length') + inlines = (DelegatedPrefixInline,) + +@admin.register(Nameserver) +class NameserverAdmin(admin.ModelAdmin): + list_display = ('name',) + +class DsRecordInline(admin.TabularInline): + extra = 0 + model = DsRecord + +@admin.register(DelegatedReverseDns) +class DelegatedReverseDnsAdmin(admin.ModelAdmin): + list_display = ('delegated_prefix',) + inlines = (DsRecordInline,) diff --git a/prefix_delegation/api/__init__.py b/prefix_delegation/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prefix_delegation/api/serializers.py b/prefix_delegation/api/serializers.py new file mode 100644 index 0000000..6ab5f97 --- /dev/null +++ b/prefix_delegation/api/serializers.py @@ -0,0 +1,63 @@ +from rest_framework import serializers + +from ..models import Nameserver, DsRecord, DelegatedReverseDns, \ + DelegatedPrefix, Prefix + + +class NameserverSerializer(serializers.ModelSerializer): + """Serialize `Nameserver` objects. + """ + + class Meta: + model = Nameserver + fields = ('name',) + + +class DsRecordSerializer(serializers.ModelSerializer): + """Serialize `DsRecord` objects. + """ + + class Meta: + model = DsRecord + fields = ('key_tag', 'algorithm', 'digest_type', 'digest') + + +class DelegatedReverseDnsSerializer(serializers.ModelSerializer): + """Serialize `DelegatedReverseDns` objects. + """ + nameservers = NameserverSerializer( + many=True, + read_only=True, + ) + + ds_records = DsRecordSerializer( + many=True, + read_only=True, + ) + + class Meta: + model = DelegatedReverseDns + fields = ('nameservers', 'ds_records') + + +class DelegatedPrefixSerializer(serializers.ModelSerializer): + """Serialize `DelegatedPrefix` objects. + """ + delegated_reverse_dns = DelegatedReverseDnsSerializer(read_only=True) + + class Meta: + model = DelegatedPrefix + fields = ('delegated_prefix', 'gateway', 'delegated_reverse_dns') + + +class PrefixSerializer(NamespacedHMSerializer): + """Serialize `Prefix` objects. + """ + delegated_prefixes = DelegatedPrefixSerializer( + many=True, + read_only=True, + ) + + class Meta: + model = Prefix + fields = ('prefix', 'length', 'delegated_length', 'delegated_prefixes') diff --git a/prefix_delegation/api/urls.py b/prefix_delegation/api/urls.py new file mode 100644 index 0000000..3114630 --- /dev/null +++ b/prefix_delegation/api/urls.py @@ -0,0 +1,7 @@ +from rest_framework.routers import BaseRouter + +from . import views + + +def register_routes(router: BaseRouter) -> None: + router.register('prefix_delegation/prefix', views.PrefixViewSet) diff --git a/prefix_delegation/api/view.py b/prefix_delegation/api/view.py new file mode 100644 index 0000000..902549e --- /dev/null +++ b/prefix_delegation/api/view.py @@ -0,0 +1,12 @@ +from rest_framework import viewsets + +from .serializers import PrefixSerializer +from ..models import Prefix + + +class PrefixViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `Prefix` objects. + """ + + queryset = Prefix.objects.all() + serializer_class = PrefixSerializer diff --git a/prefix_delegation/apps.py b/prefix_delegation/apps.py new file mode 100644 index 0000000..21b5eff --- /dev/null +++ b/prefix_delegation/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class PrefixDelegationConfig(AppConfig): + name = 'prefix_delegation' + verbose_name = _("prefix delegation") diff --git a/prefix_delegation/migrations/__init__.py b/prefix_delegation/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prefix_delegation/models.py b/prefix_delegation/models.py new file mode 100644 index 0000000..e7a9c3f --- /dev/null +++ b/prefix_delegation/models.py @@ -0,0 +1,146 @@ +import ipaddress + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + + +class Prefix(models.Model): + + name = models.SlugField( + unique=True, + verbose_name=_("name"), + ) + prefix = models.GenericIPAddressField( + protocol='IPv6', + unique=True, + verbose_name=_("prefix"), + ) + length = models.PositiveSmallIntegerField( + verbose_name=_("length"), + ) + delegated_length = models.PositiveSmallIntegerField( + verbose_name=_("length of delegated prefixes"), + ) + + def network(self): + return ipaddress.IPv6Network(f'{self.prefix}/{self.length}') + + def clean(self): + try: + self.network() + except: + raise ValidationError(f"{self.prefix}/{self.length} is not a valid network.") + if self.length >= self.delegated_length: + raise ValidationError(f"length ({self.length}) must be lower than delegated_length ({self.delegated_length}).") + if self.delegated_length > 128: + raise ValidationError(f"delegated_length {self.delegated_length} must be lower than or equal to 128.") + + def __str__(self): + return str(self.network()) + + class Meta: + verbose_name = _("Prefix") + verbose_name_plural = _("Prefixes") + + +class DelegatedPrefix(models.Model): + + prefix = models.ForeignKey( + Prefix, + on_delete=models.PROTECT, + related_name='delegated_prefixes', + verbose_name=_("prefix"), + ) + delegated_prefix = models.GenericIPAddressField( + protocol='IPv6', + unique=True, + verbose_name=_("delegated prefix"), + ) + gateway = models.GenericIPAddressField( + protocol='IPv6', + verbose_name=_("gateway"), + ) + owner = models.ForeignKey( + settings.PREFIX_DELEGATION_OWNER, + on_delete=models.CASCADE, + verbose_name=_("owner"), + ) + + def network(self): + return ipaddress.IPv6Network(f'{self.delegated_prefix}/{self.prefix.delegated_length}') + + def clean(self): + try: + network = self.network() + except: + raise ValidationError(f"{self.delegated_prefix}/{self.prefix.delegated_length} is not a valid network.") + prefix_network = self.prefix.network() + if not network.subnet_of(prefix_network): + raise ValidationError(f"{network} must be a subnet of {prefix_network}.") + + def __str__(self): + return str(self.network()) + + class Meta: + verbose_name = "Delegated prefix" + verbose_name_plural = "Delegated prefixes" + + +class Nameserver(models.Model): + + name = models.CharField( + max_length=253, + unique=True, + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = "Name server" + verbose_name_plural = "Name servers" + + +class DelegatedReverseDns(models.Model): + + delegated_prefix = models.OneToOneField(DelegatedPrefix, related_name='delegated_reverse_dns') + nameservers = models.ManyToManyField(Nameserver) + + def owner(self): + return self.delegated_prefix.owner + + def clean(self): + if self.delegated_prefix.prefix.delegated_length % 4: + raise ValidationError(f"delegated_length must be a multiple of 4 in order to delegate a reverse DNS.") + + def __str__(self): + network = self.delegated_prefix.network() + zone = network.network_address.exploded.replace(':', '')[network.prefixlen//4-1::-1] + zone = '.'.join(list(zone)) + '.ip6.arpa' + return zone + + class Meta: + verbose_name = "Delegated reverse DNS" + verbose_name_plural = "Delegated reverse DNS" + + +class DsRecord(models.Model): + + delegated_reverse_dns = models.ForeignKey( + DelegatedReverseDns, + on_delete=models.CASCADE, + related_name='ds_records' + ) + key_tag = models.PositiveIntegerField() + algorithm = models.PositiveSmallIntegerField() + digest_type = models.PositiveSmallIntegerField() + digest = models.TextField() + + def __str__(self): + return f"{self.ket_tag} {self.algorithm} {self.digest_type} {self.digest}" + + class Meta: + verbose_name = "DS record" + verbose_name_plural = "DS records" diff --git a/prefix_delegation/tests.py b/prefix_delegation/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/prefix_delegation/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/prefix_delegation/views.py b/prefix_delegation/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/prefix_delegation/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. -- GitLab