vlans.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. from django.contrib.contenttypes.fields import GenericForeignKey
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ValidationError
  4. from django.core.validators import MaxValueValidator, MinValueValidator
  5. from django.db import models
  6. from django.urls import reverse
  7. from dcim.models import Interface
  8. from extras.utils import extras_features
  9. from ipam.choices import *
  10. from ipam.constants import *
  11. from ipam.querysets import VLANQuerySet
  12. from netbox.models import OrganizationalModel, PrimaryModel
  13. from utilities.querysets import RestrictedQuerySet
  14. from virtualization.models import VMInterface
  15. __all__ = (
  16. 'VLAN',
  17. 'VLANGroup',
  18. )
  19. @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
  20. class VLANGroup(OrganizationalModel):
  21. """
  22. A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
  23. """
  24. name = models.CharField(
  25. max_length=100
  26. )
  27. slug = models.SlugField(
  28. max_length=100
  29. )
  30. scope_type = models.ForeignKey(
  31. to=ContentType,
  32. on_delete=models.CASCADE,
  33. limit_choices_to=Q(model__in=VLANGROUP_SCOPE_TYPES),
  34. blank=True,
  35. null=True
  36. )
  37. scope_id = models.PositiveBigIntegerField(
  38. blank=True,
  39. null=True
  40. )
  41. scope = GenericForeignKey(
  42. ct_field='scope_type',
  43. fk_field='scope_id'
  44. )
  45. description = models.CharField(
  46. max_length=200,
  47. blank=True
  48. )
  49. objects = RestrictedQuerySet.as_manager()
  50. class Meta:
  51. ordering = ('name', 'pk') # Name may be non-unique
  52. unique_together = [
  53. ['scope_type', 'scope_id', 'name'],
  54. ['scope_type', 'scope_id', 'slug'],
  55. ]
  56. verbose_name = 'VLAN group'
  57. verbose_name_plural = 'VLAN groups'
  58. def __str__(self):
  59. return self.name
  60. def get_absolute_url(self):
  61. return reverse('ipam:vlangroup', args=[self.pk])
  62. def clean(self):
  63. super().clean()
  64. # Validate scope assignment
  65. if self.scope_type and not self.scope_id:
  66. raise ValidationError("Cannot set scope_type without scope_id.")
  67. if self.scope_id and not self.scope_type:
  68. raise ValidationError("Cannot set scope_id without scope_type.")
  69. def get_next_available_vid(self):
  70. """
  71. Return the first available VLAN ID (1-4094) in the group.
  72. """
  73. vlan_ids = VLAN.objects.filter(group=self).values_list('vid', flat=True)
  74. for i in range(1, 4095):
  75. if i not in vlan_ids:
  76. return i
  77. return None
  78. @extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
  79. class VLAN(PrimaryModel):
  80. """
  81. A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
  82. to a Site, however VLAN IDs need not be unique within a Site. A VLAN may optionally be assigned to a VLANGroup,
  83. within which all VLAN IDs and names but be unique.
  84. Like Prefixes, each VLAN is assigned an operational status and optionally a user-defined Role. A VLAN can have zero
  85. or more Prefixes assigned to it.
  86. """
  87. site = models.ForeignKey(
  88. to='dcim.Site',
  89. on_delete=models.PROTECT,
  90. related_name='vlans',
  91. blank=True,
  92. null=True
  93. )
  94. group = models.ForeignKey(
  95. to='ipam.VLANGroup',
  96. on_delete=models.PROTECT,
  97. related_name='vlans',
  98. blank=True,
  99. null=True
  100. )
  101. vid = models.PositiveSmallIntegerField(
  102. verbose_name='ID',
  103. validators=[MinValueValidator(1), MaxValueValidator(4094)]
  104. )
  105. name = models.CharField(
  106. max_length=64
  107. )
  108. tenant = models.ForeignKey(
  109. to='tenancy.Tenant',
  110. on_delete=models.PROTECT,
  111. related_name='vlans',
  112. blank=True,
  113. null=True
  114. )
  115. status = models.CharField(
  116. max_length=50,
  117. choices=VLANStatusChoices,
  118. default=VLANStatusChoices.STATUS_ACTIVE
  119. )
  120. role = models.ForeignKey(
  121. to='ipam.Role',
  122. on_delete=models.SET_NULL,
  123. related_name='vlans',
  124. blank=True,
  125. null=True
  126. )
  127. description = models.CharField(
  128. max_length=200,
  129. blank=True
  130. )
  131. objects = VLANQuerySet.as_manager()
  132. clone_fields = [
  133. 'site', 'group', 'tenant', 'status', 'role', 'description',
  134. ]
  135. class Meta:
  136. ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique
  137. unique_together = [
  138. ['group', 'vid'],
  139. ['group', 'name'],
  140. ]
  141. verbose_name = 'VLAN'
  142. verbose_name_plural = 'VLANs'
  143. def __str__(self):
  144. return f'{self.name} ({self.vid})'
  145. def get_absolute_url(self):
  146. return reverse('ipam:vlan', args=[self.pk])
  147. def clean(self):
  148. super().clean()
  149. # Validate VLAN group (if assigned)
  150. if self.group and self.site and self.group.scope != self.site:
  151. raise ValidationError({
  152. 'group': f"VLAN is assigned to group {self.group} (scope: {self.group.scope}); cannot also assign to "
  153. f"site {self.site}."
  154. })
  155. def get_status_class(self):
  156. return VLANStatusChoices.CSS_CLASSES.get(self.status)
  157. def get_interfaces(self):
  158. # Return all device interfaces assigned to this VLAN
  159. return Interface.objects.filter(
  160. Q(untagged_vlan_id=self.pk) |
  161. Q(tagged_vlans=self.pk)
  162. ).distinct()
  163. def get_vminterfaces(self):
  164. # Return all VM interfaces assigned to this VLAN
  165. return VMInterface.objects.filter(
  166. Q(untagged_vlan_id=self.pk) |
  167. Q(tagged_vlans=self.pk)
  168. ).distinct()