models.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. from netaddr import IPNetwork, cidr_merge
  2. from django.core.exceptions import ValidationError
  3. from django.core.urlresolvers import reverse
  4. from django.core.validators import MaxValueValidator, MinValueValidator
  5. from django.db import models
  6. from dcim.models import Interface
  7. from .fields import IPNetworkField, IPAddressField
  8. AF_CHOICES = (
  9. (4, 'IPv4'),
  10. (6, 'IPv6'),
  11. )
  12. PREFIX_STATUS_CHOICES = (
  13. (0, 'Container'),
  14. (1, 'Active'),
  15. (2, 'Reserved'),
  16. (3, 'Deprecated')
  17. )
  18. VLAN_STATUS_CHOICES = (
  19. (1, 'Active'),
  20. (2, 'Reserved'),
  21. (3, 'Deprecated')
  22. )
  23. STATUS_CHOICE_CLASSES = {
  24. 0: 'default',
  25. 1: 'primary',
  26. 2: 'info',
  27. 3: 'danger',
  28. }
  29. class VRF(models.Model):
  30. """
  31. A discrete layer three forwarding domain (e.g. a routing table)
  32. """
  33. name = models.CharField(max_length=50)
  34. rd = models.CharField(max_length=21, unique=True, verbose_name='Route distinguisher')
  35. description = models.CharField(max_length=100, blank=True)
  36. class Meta:
  37. ordering = ['name']
  38. verbose_name = 'VRF'
  39. verbose_name_plural = 'VRFs'
  40. def __unicode__(self):
  41. return self.name
  42. def get_absolute_url(self):
  43. return reverse('ipam:vrf', args=[self.pk])
  44. class RIR(models.Model):
  45. """
  46. A regional Internet registry (e.g. ARIN) or governing standard (e.g. RFC 1918)
  47. """
  48. name = models.CharField(max_length=50, unique=True)
  49. slug = models.SlugField(unique=True)
  50. class Meta:
  51. ordering = ['name']
  52. verbose_name = 'RIR'
  53. verbose_name_plural = 'RIRs'
  54. def __unicode__(self):
  55. return self.name
  56. def get_absolute_url(self):
  57. return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
  58. class Aggregate(models.Model):
  59. """
  60. A top-level IPv4 or IPv6 prefix
  61. """
  62. family = models.PositiveSmallIntegerField(choices=AF_CHOICES)
  63. prefix = IPNetworkField()
  64. rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
  65. date_added = models.DateField(blank=True, null=True)
  66. description = models.CharField(max_length=100, blank=True)
  67. class Meta:
  68. ordering = ['family', 'prefix']
  69. def __unicode__(self):
  70. return str(self.prefix)
  71. def get_absolute_url(self):
  72. return reverse('ipam:aggregate', args=[self.pk])
  73. def clean(self):
  74. if self.prefix:
  75. # Clear host bits from prefix
  76. self.prefix = self.prefix.cidr
  77. # Ensure that the aggregate being added is not covered by an existing aggregate
  78. covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
  79. if self.pk:
  80. covering_aggregates = covering_aggregates.exclude(pk=self.pk)
  81. if covering_aggregates:
  82. raise ValidationError("{} is already covered by an existing aggregate ({})"
  83. .format(self.prefix, covering_aggregates[0]))
  84. def save(self, *args, **kwargs):
  85. if self.prefix:
  86. # Infer address family from IPNetwork object
  87. self.family = self.prefix.version
  88. super(Aggregate, self).save(*args, **kwargs)
  89. def get_utilization(self):
  90. """
  91. Determine the utilization rate of the aggregate prefix and return it as a percentage.
  92. """
  93. child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
  94. # Remove overlapping prefixes from list of children
  95. networks = cidr_merge([c.prefix for c in child_prefixes])
  96. children_size = float(0)
  97. for p in networks:
  98. children_size += p.size
  99. return int(children_size / self.prefix.size * 100)
  100. class Role(models.Model):
  101. """
  102. The role of an address resource (e.g. customer, infrastructure, mgmt, etc.)
  103. """
  104. name = models.CharField(max_length=50, unique=True)
  105. slug = models.SlugField(unique=True)
  106. weight = models.PositiveSmallIntegerField(default=1000)
  107. class Meta:
  108. ordering = ['weight', 'name']
  109. def __unicode__(self):
  110. return self.name
  111. @property
  112. def count_prefixes(self):
  113. return self.prefixes.count()
  114. @property
  115. def count_vlans(self):
  116. return self.vlans.count()
  117. class PrefixQuerySet(models.QuerySet):
  118. def annotate_depth(self, limit=None):
  119. """
  120. Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
  121. to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
  122. performance issues at scale.
  123. Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
  124. modifications.
  125. """
  126. queryset = self
  127. stack = []
  128. for p in queryset:
  129. try:
  130. prev_p = stack[-1]
  131. except IndexError:
  132. prev_p = None
  133. if prev_p is not None:
  134. while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
  135. stack.pop()
  136. try:
  137. prev_p = stack[-1]
  138. except IndexError:
  139. prev_p = None
  140. break
  141. if prev_p is not None:
  142. prev_p.has_children = True
  143. stack.append(p)
  144. p.depth = len(stack) - 1
  145. if limit is None:
  146. return queryset
  147. return filter(lambda p: p.depth <= limit, queryset)
  148. class Prefix(models.Model):
  149. """
  150. An IPv4 or IPv6 prefix, including mask length
  151. """
  152. family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
  153. prefix = IPNetworkField()
  154. site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
  155. vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
  156. verbose_name='VRF')
  157. vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
  158. verbose_name='VLAN')
  159. status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
  160. role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
  161. description = models.CharField(max_length=100, blank=True)
  162. objects = PrefixQuerySet.as_manager()
  163. class Meta:
  164. ordering = ['family', 'prefix']
  165. verbose_name_plural = 'prefixes'
  166. def __unicode__(self):
  167. return str(self.prefix)
  168. def get_absolute_url(self):
  169. return reverse('ipam:prefix', args=[self.pk])
  170. def save(self, *args, **kwargs):
  171. if self.prefix:
  172. # Clear host bits from prefix
  173. self.prefix = self.prefix.cidr
  174. # Infer address family from IPNetwork object
  175. self.family = self.prefix.version
  176. super(Prefix, self).save(*args, **kwargs)
  177. @property
  178. def new_subnet(self):
  179. if self.family == 4:
  180. if self.prefix.prefixlen <= 30:
  181. return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
  182. return None
  183. if self.family == 6:
  184. if self.prefix.prefixlen <= 126:
  185. return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
  186. return None
  187. def get_status_class(self):
  188. return STATUS_CHOICE_CLASSES[self.status]
  189. class IPAddress(models.Model):
  190. """
  191. An IPv4 or IPv6 address
  192. """
  193. family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
  194. address = IPAddressField()
  195. vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
  196. verbose_name='VRF')
  197. interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
  198. null=True)
  199. nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
  200. null=True, verbose_name='NAT IP (inside)')
  201. description = models.CharField(max_length=100, blank=True)
  202. class Meta:
  203. ordering = ['family', 'address']
  204. verbose_name = 'IP address'
  205. verbose_name_plural = 'IP addresses'
  206. def __unicode__(self):
  207. return str(self.address)
  208. def get_absolute_url(self):
  209. return reverse('ipam:ipaddress', args=[self.pk])
  210. def save(self, *args, **kwargs):
  211. if self.address:
  212. # Infer address family from IPAddress object
  213. self.family = self.address.version
  214. super(IPAddress, self).save(*args, **kwargs)
  215. @property
  216. def device(self):
  217. if self.interface:
  218. return self.interface.device
  219. return None
  220. class VLAN(models.Model):
  221. """
  222. A VLAN within a site
  223. """
  224. site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
  225. vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
  226. MinValueValidator(1),
  227. MaxValueValidator(4094)
  228. ])
  229. name = models.CharField(max_length=30)
  230. status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
  231. role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
  232. class Meta:
  233. ordering = ['site', 'vid']
  234. verbose_name = 'VLAN'
  235. verbose_name_plural = 'VLANs'
  236. def __unicode__(self):
  237. return "{0} ({1})".format(self.vid, self.name)
  238. def get_absolute_url(self):
  239. return reverse('ipam:vlan', args=[self.pk])
  240. def get_status_class(self):
  241. return STATUS_CHOICE_CLASSES[self.status]