models.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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. def to_csv(self):
  45. return ','.join([
  46. self.name,
  47. self.rd,
  48. self.description,
  49. ])
  50. class RIR(models.Model):
  51. """
  52. A regional Internet registry (e.g. ARIN) or governing standard (e.g. RFC 1918)
  53. """
  54. name = models.CharField(max_length=50, unique=True)
  55. slug = models.SlugField(unique=True)
  56. class Meta:
  57. ordering = ['name']
  58. verbose_name = 'RIR'
  59. verbose_name_plural = 'RIRs'
  60. def __unicode__(self):
  61. return self.name
  62. def get_absolute_url(self):
  63. return "{}?rir={}".format(reverse('ipam:aggregate_list'), self.slug)
  64. class Aggregate(models.Model):
  65. """
  66. A top-level IPv4 or IPv6 prefix
  67. """
  68. family = models.PositiveSmallIntegerField(choices=AF_CHOICES)
  69. prefix = IPNetworkField()
  70. rir = models.ForeignKey('RIR', related_name='aggregates', on_delete=models.PROTECT, verbose_name='RIR')
  71. date_added = models.DateField(blank=True, null=True)
  72. description = models.CharField(max_length=100, blank=True)
  73. class Meta:
  74. ordering = ['family', 'prefix']
  75. def __unicode__(self):
  76. return str(self.prefix)
  77. def get_absolute_url(self):
  78. return reverse('ipam:aggregate', args=[self.pk])
  79. def clean(self):
  80. if self.prefix:
  81. # Clear host bits from prefix
  82. self.prefix = self.prefix.cidr
  83. # Ensure that the aggregate being added is not covered by an existing aggregate
  84. covering_aggregates = Aggregate.objects.filter(prefix__net_contains_or_equals=str(self.prefix))
  85. if self.pk:
  86. covering_aggregates = covering_aggregates.exclude(pk=self.pk)
  87. if covering_aggregates:
  88. raise ValidationError("{} is already covered by an existing aggregate ({})"
  89. .format(self.prefix, covering_aggregates[0]))
  90. def save(self, *args, **kwargs):
  91. if self.prefix:
  92. # Infer address family from IPNetwork object
  93. self.family = self.prefix.version
  94. super(Aggregate, self).save(*args, **kwargs)
  95. def to_csv(self):
  96. return ','.join([
  97. str(self.prefix),
  98. self.rir.name,
  99. self.date_added.isoformat() if self.date_added else '',
  100. self.description,
  101. ])
  102. def get_utilization(self):
  103. """
  104. Determine the utilization rate of the aggregate prefix and return it as a percentage.
  105. """
  106. child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(self.prefix))
  107. # Remove overlapping prefixes from list of children
  108. networks = cidr_merge([c.prefix for c in child_prefixes])
  109. children_size = float(0)
  110. for p in networks:
  111. children_size += p.size
  112. return int(children_size / self.prefix.size * 100)
  113. class Role(models.Model):
  114. """
  115. The role of an address resource (e.g. customer, infrastructure, mgmt, etc.)
  116. """
  117. name = models.CharField(max_length=50, unique=True)
  118. slug = models.SlugField(unique=True)
  119. weight = models.PositiveSmallIntegerField(default=1000)
  120. class Meta:
  121. ordering = ['weight', 'name']
  122. def __unicode__(self):
  123. return self.name
  124. @property
  125. def count_prefixes(self):
  126. return self.prefixes.count()
  127. @property
  128. def count_vlans(self):
  129. return self.vlans.count()
  130. class PrefixQuerySet(models.QuerySet):
  131. def annotate_depth(self, limit=None):
  132. """
  133. Iterate through a QuerySet of Prefixes and annotate the hierarchical level of each. While it would be preferable
  134. to do this using .extra() on the QuerySet to count the unique parents of each prefix, that approach introduces
  135. performance issues at scale.
  136. Because we're adding a non-field attribute to the model, annotation must be made *after* any QuerySet
  137. modifications.
  138. """
  139. queryset = self
  140. stack = []
  141. for p in queryset:
  142. try:
  143. prev_p = stack[-1]
  144. except IndexError:
  145. prev_p = None
  146. if prev_p is not None:
  147. while (p.prefix not in prev_p.prefix) or p.prefix == prev_p.prefix:
  148. stack.pop()
  149. try:
  150. prev_p = stack[-1]
  151. except IndexError:
  152. prev_p = None
  153. break
  154. if prev_p is not None:
  155. prev_p.has_children = True
  156. stack.append(p)
  157. p.depth = len(stack) - 1
  158. if limit is None:
  159. return queryset
  160. return filter(lambda p: p.depth <= limit, queryset)
  161. class Prefix(models.Model):
  162. """
  163. An IPv4 or IPv6 prefix, including mask length
  164. """
  165. family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
  166. prefix = IPNetworkField()
  167. site = models.ForeignKey('dcim.Site', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True)
  168. vrf = models.ForeignKey('VRF', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
  169. verbose_name='VRF')
  170. vlan = models.ForeignKey('VLAN', related_name='prefixes', on_delete=models.PROTECT, blank=True, null=True,
  171. verbose_name='VLAN')
  172. status = models.PositiveSmallIntegerField('Status', choices=PREFIX_STATUS_CHOICES, default=1)
  173. role = models.ForeignKey('Role', related_name='prefixes', on_delete=models.SET_NULL, blank=True, null=True)
  174. description = models.CharField(max_length=100, blank=True)
  175. objects = PrefixQuerySet.as_manager()
  176. class Meta:
  177. ordering = ['family', 'prefix']
  178. verbose_name_plural = 'prefixes'
  179. def __unicode__(self):
  180. return str(self.prefix)
  181. def get_absolute_url(self):
  182. return reverse('ipam:prefix', args=[self.pk])
  183. def save(self, *args, **kwargs):
  184. if self.prefix:
  185. # Clear host bits from prefix
  186. self.prefix = self.prefix.cidr
  187. # Infer address family from IPNetwork object
  188. self.family = self.prefix.version
  189. super(Prefix, self).save(*args, **kwargs)
  190. def to_csv(self):
  191. return ','.join([
  192. str(self.prefix),
  193. self.vrf.rd if self.vrf else '',
  194. self.site.name if self.site else '',
  195. self.get_status_display(),
  196. self.role.name if self.role else '',
  197. self.description,
  198. ])
  199. @property
  200. def new_subnet(self):
  201. if self.family == 4:
  202. if self.prefix.prefixlen <= 30:
  203. return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
  204. return None
  205. if self.family == 6:
  206. if self.prefix.prefixlen <= 126:
  207. return IPNetwork('{}/{}'.format(self.prefix.network, self.prefix.prefixlen + 1))
  208. return None
  209. def get_status_class(self):
  210. return STATUS_CHOICE_CLASSES[self.status]
  211. class IPAddress(models.Model):
  212. """
  213. An IPv4 or IPv6 address
  214. """
  215. family = models.PositiveSmallIntegerField(choices=AF_CHOICES, editable=False)
  216. address = IPAddressField()
  217. vrf = models.ForeignKey('VRF', related_name='ip_addresses', on_delete=models.PROTECT, blank=True, null=True,
  218. verbose_name='VRF')
  219. interface = models.ForeignKey(Interface, related_name='ip_addresses', on_delete=models.CASCADE, blank=True,
  220. null=True)
  221. nat_inside = models.OneToOneField('self', related_name='nat_outside', on_delete=models.SET_NULL, blank=True,
  222. null=True, verbose_name='NAT IP (inside)')
  223. description = models.CharField(max_length=100, blank=True)
  224. class Meta:
  225. ordering = ['family', 'address']
  226. verbose_name = 'IP address'
  227. verbose_name_plural = 'IP addresses'
  228. def __unicode__(self):
  229. return str(self.address)
  230. def get_absolute_url(self):
  231. return reverse('ipam:ipaddress', args=[self.pk])
  232. def save(self, *args, **kwargs):
  233. if self.address:
  234. # Infer address family from IPAddress object
  235. self.family = self.address.version
  236. super(IPAddress, self).save(*args, **kwargs)
  237. def to_csv(self):
  238. return ','.join([
  239. str(self.address),
  240. self.vrf.rd if self.vrf else '',
  241. self.device.identifier if self.device else '',
  242. self.interface.name if self.interface else '',
  243. 'True' if getattr(self, 'primary_for', False) else '',
  244. self.description,
  245. ])
  246. @property
  247. def device(self):
  248. if self.interface:
  249. return self.interface.device
  250. return None
  251. class VLAN(models.Model):
  252. """
  253. A VLAN within a site
  254. """
  255. site = models.ForeignKey('dcim.Site', related_name='vlans', on_delete=models.PROTECT)
  256. vid = models.PositiveSmallIntegerField(verbose_name='ID', validators=[
  257. MinValueValidator(1),
  258. MaxValueValidator(4094)
  259. ])
  260. name = models.CharField(max_length=30)
  261. status = models.PositiveSmallIntegerField('Status', choices=VLAN_STATUS_CHOICES, default=1)
  262. role = models.ForeignKey('Role', related_name='vlans', on_delete=models.SET_NULL, blank=True, null=True)
  263. class Meta:
  264. ordering = ['site', 'vid']
  265. verbose_name = 'VLAN'
  266. verbose_name_plural = 'VLANs'
  267. def __unicode__(self):
  268. return "{0} ({1})".format(self.vid, self.name)
  269. def get_absolute_url(self):
  270. return reverse('ipam:vlan', args=[self.pk])
  271. def to_csv(self):
  272. return ','.join([
  273. self.site.name,
  274. str(self.vid),
  275. self.name,
  276. self.get_status_display(),
  277. self.role.name if self.role else '',
  278. ])
  279. def get_status_class(self):
  280. return STATUS_CHOICE_CLASSES[self.status]