# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator from django.db.models import Q from netfields import CidrAddressField, NetManager from netaddr import IPNetwork, IPSet def validate_subnet(cidr): """Checks that a CIDR object is indeed a subnet, i.e. the host bits are all set to zero.""" if not isinstance(cidr, IPNetwork): raise ValidationError("Erreur, objet IPNetwork attendu.") if cidr.ip != cidr.network: raise ValidationError("{} n'est pas un sous-réseau valide, voulez-vous dire {} ?".format(cidr, cidr.cidr)) class IPPool(models.Model): """Pool of IP addresses (either v4 or v6).""" name = models.CharField(max_length=255, blank=False, null=False, verbose_name='nom', help_text="Nom du pool d'IP") default_subnetsize = models.PositiveSmallIntegerField(blank=False, verbose_name='taille de sous-réseau par défaut', help_text='Taille par défaut du sous-réseau à allouer aux abonnés dans ce pool', validators=[MaxValueValidator(64)]) inet = CidrAddressField(validators=[validate_subnet], verbose_name='réseau', help_text="Bloc d'adresses IP du pool") objects = NetManager() def clean(self): if self.inet: max_subnetsize = 64 if self.inet.version == 6 else 32 if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize: raise ValidationError('Taille de sous-réseau invalide') # Check that related subnet are in the pool (useful when # modifying an existing pool that already has subnets # allocated in it) incorrect = [str(subnet) for subnet in self.ipsubnet_set.all() if not subnet.inet in self.inet] if incorrect: err = "Des sous-réseaux se retrouveraient en-dehors du bloc d'IP: {}".format(incorrect) raise ValidationError(err) def __unicode__(self): return self.name class Meta: verbose_name = "pool d'IP" verbose_name_plural = "pools d'IP" class IPSubnet(models.Model): # TODO: find some way to signal to Subscriptions objects when a subnet # gets modified (so that the subscription can update the LDAP backend # accordingly) # Actually, a better idea would be to build a custom relation and update # LDAP in the relation itself. inet = CidrAddressField(blank=True, validators=[validate_subnet], verbose_name="sous-réseau", help_text="Laisser vide pour allouer automatiquement") objects = NetManager() ip_pool = models.ForeignKey(IPPool, verbose_name="pool d'IP") configuration = models.ForeignKey('configuration.Configuration', related_name='ip_subnet', verbose_name='configuration') delegate_reverse_dns = models.BooleanField(default=False, verbose_name='déléguer le reverse DNS', help_text='Déléguer la résolution DNS inverse de ce sous-réseau à un ou plusieurs serveurs de noms') name_server = models.ManyToManyField('reverse_dns.NameServer', blank=True, verbose_name='serveur de noms', help_text="Serveur de noms à qui déléguer la résolution DNS inverse") def allocate(self): """Automatically allocate a free subnet""" pool = IPSet([self.ip_pool.inet]) used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all())) free = pool.difference(used) # Generator for efficiency (we don't build the whole list) available = (p for p in free.iter_cidrs() if p.prefixlen <= self.ip_pool.default_subnetsize) # TODO: for IPv4, get rid of the network and broadcast # addresses? Not really needed nowadays, and we usually don't # have a real subnet in practice (i.e. Ethernet segment), but # many /32. try: first_free = available.next() except StopIteration: raise ValidationError("Impossible d'allouer un sous-réseau : bloc d'IP rempli.") self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next() def validate_inclusion(self): """Check that we are included in the IP pool""" if not self.inet in self.ip_pool.inet: raise ValidationError("Le sous-réseau doit être inclus dans le bloc d'IP.") # Check that we don't conflict with existing subnets. conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) | Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id) if conflicting: raise ValidationError("Le sous-réseau est en conflit avec des sous-réseaux existants: {}.".format(conflicting)) def validate_reverse_dns(self): """Check that reverse DNS entries, if any, are included in the subnet""" incorrect = [str(rev.ip) for rev in self.reversednsentry_set.all() if not rev.ip in self.inet] if incorrect: raise ValidationError("Des entrées DNS inverse ne sont pas dans le sous-réseau: {}.".format(incorrect)) def clean(self): if not self.inet: self.allocate() else: self.validate_inclusion() self.validate_reverse_dns() def __unicode__(self): return str(self.inet) class Meta: verbose_name = "sous-réseau IP" verbose_name_plural = "sous-réseaux IP"