models.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. # -*- coding: utf-8 -*-
  2. from django.db import models
  3. from django.core.exceptions import ValidationError
  4. from django.core.validators import MaxValueValidator
  5. from django.db.models import Q
  6. from netfields import CidrAddressField, NetManager
  7. from netaddr import IPNetwork, IPSet
  8. def validate_subnet(cidr):
  9. """Checks that a CIDR object is indeed a subnet, i.e. the host bits are
  10. all set to zero."""
  11. if not isinstance(cidr, IPNetwork):
  12. raise ValidationError("Internal error, expected IPNetwork object")
  13. if cidr.ip != cidr.network:
  14. raise ValidationError("{} is not a proper subnet, you probably mean {}".format(cidr, cidr.cidr))
  15. class IPPool(models.Model):
  16. """Pool of IP addresses (either v4 or v6)."""
  17. name = models.CharField(max_length=255, blank=False, null=False,
  18. help_text='Name of the IP pool')
  19. default_subnetsize = models.PositiveSmallIntegerField(blank=False,
  20. verbose_name='default subnet size',
  21. help_text='Default subnet size to allocate to subscribers in this pool',
  22. validators=[MaxValueValidator(64)])
  23. inet = CidrAddressField(validators=[validate_subnet],
  24. help_text="Address space to use")
  25. objects = NetManager()
  26. def clean(self):
  27. if self.inet:
  28. max_subnetsize = 64 if self.inet.version == 6 else 32
  29. if not self.inet.prefixlen <= self.default_subnetsize <= max_subnetsize:
  30. raise ValidationError('Invalid default subnet size')
  31. # Check that related subnet are in the pool (useful when
  32. # modifying an existing pool that already has subnets
  33. # allocated in it)
  34. incorrect = [str(subnet) for subnet in self.ipsubnet_set.all()
  35. if not subnet.inet in self.inet]
  36. if incorrect:
  37. err = 'Some subnets allocated in this pool are outside the pool: {}'.format(incorrect)
  38. raise ValidationError(err)
  39. def __unicode__(self):
  40. return self.name
  41. class IPSubnet(models.Model):
  42. # TODO: find some way to signal to Subscriptions objects when a subnet
  43. # gets modified (so that the subscription can update the LDAP backend
  44. # accordingly)
  45. # Actually, a better idea would be to build a custom relation and update
  46. # LDAP in the relation itself.
  47. inet = CidrAddressField(blank=True, validators=[validate_subnet],
  48. verbose_name="subnet",
  49. help_text="Leave empty for automatic allocation")
  50. objects = NetManager()
  51. ip_pool = models.ForeignKey(IPPool, verbose_name="IP pool")
  52. offer_subscription = models.ForeignKey('offers.OfferSubscription',
  53. related_name='ip_subnet',
  54. verbose_name="subscription")
  55. delegate_reverse_dns = models.BooleanField(default=False)
  56. name_server = models.ManyToManyField('reverse_dns.NameServer',
  57. blank=True,
  58. help_text="Nameserver to use for the delegation of reverse DNS")
  59. def allocate(self):
  60. """Automatically allocate a free subnet"""
  61. pool = IPSet([self.ip_pool.inet])
  62. used = IPSet((s.inet for s in self.ip_pool.ipsubnet_set.all()))
  63. free = pool.difference(used)
  64. # Generator for efficiency (we don't build the whole list)
  65. available = (p for p in free.iter_cidrs() if p.prefixlen <= self.ip_pool.default_subnetsize)
  66. # TODO: for IPv4, get rid of the network and broadcast
  67. # addresses? Not really needed nowadays, and we usually don't
  68. # have a real subnet in practice (i.e. Ethernet segment), but
  69. # many /32.
  70. try:
  71. first_free = available.next()
  72. except StopIteration:
  73. raise ValidationError('Unable to allocate an IP subnet in the specified pool: not enough space left.')
  74. self.inet = first_free.subnet(self.ip_pool.default_subnetsize, 1).next()
  75. def validate_inclusion(self):
  76. """Check that we are included in the IP pool"""
  77. if not self.inet in self.ip_pool.inet:
  78. raise ValidationError('Subnet must be included in the IP pool.')
  79. # Check that we don't conflict with existing subnets.
  80. conflicting = self.ip_pool.ipsubnet_set.filter(Q(inet__net_contained_or_equal=self.inet) |
  81. Q(inet__net_contains_or_equals=self.inet)).exclude(id=self.id)
  82. if conflicting:
  83. raise ValidationError('Subnet must not intersect with existing subnets.\nIntersected subnets: {}.'.format(conflicting))
  84. def validate_reverse_dns(self):
  85. """Check that reverse DNS entries, if any, are included in the subnet"""
  86. incorrect = [str(rev.ip) for rev in self.reversednsentry_set.all() if not rev.ip in self.inet]
  87. if incorrect:
  88. raise ValidationError('Some reverse DNS entries are not in the subnet: {}.'.format(incorrect))
  89. def clean(self):
  90. if not self.inet:
  91. self.allocate()
  92. else:
  93. self.validate_inclusion()
  94. self.validate_reverse_dns()
  95. def __unicode__(self):
  96. return str(self.inet)