models.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. from django.core.exceptions import ValidationError
  2. from django.db import models
  3. from django.urls import reverse
  4. from mptt.models import MPTTModel
  5. from dcim.choices import LinkStatusChoices
  6. from dcim.constants import WIRELESS_IFACE_TYPES
  7. from netbox.models import NestedGroupModel, PrimaryModel
  8. from .choices import *
  9. from .constants import *
  10. __all__ = (
  11. 'WirelessLAN',
  12. 'WirelessLANGroup',
  13. 'WirelessLink',
  14. )
  15. class WirelessAuthenticationBase(models.Model):
  16. """
  17. Abstract model for attaching attributes related to wireless authentication.
  18. """
  19. auth_type = models.CharField(
  20. max_length=50,
  21. choices=WirelessAuthTypeChoices,
  22. blank=True,
  23. verbose_name="Auth Type",
  24. )
  25. auth_cipher = models.CharField(
  26. max_length=50,
  27. choices=WirelessAuthCipherChoices,
  28. blank=True
  29. )
  30. auth_psk = models.CharField(
  31. max_length=PSK_MAX_LENGTH,
  32. blank=True,
  33. verbose_name='Pre-shared key'
  34. )
  35. class Meta:
  36. abstract = True
  37. class WirelessLANGroup(NestedGroupModel):
  38. """
  39. A nested grouping of WirelessLANs
  40. """
  41. name = models.CharField(
  42. max_length=100,
  43. unique=True
  44. )
  45. slug = models.SlugField(
  46. max_length=100,
  47. unique=True
  48. )
  49. class Meta:
  50. ordering = ('name', 'pk')
  51. constraints = (
  52. models.UniqueConstraint(
  53. fields=('parent', 'name'),
  54. name='%(app_label)s_%(class)s_unique_parent_name'
  55. ),
  56. )
  57. verbose_name = 'Wireless LAN Group'
  58. def get_absolute_url(self):
  59. return reverse('wireless:wirelesslangroup', args=[self.pk])
  60. class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
  61. """
  62. A wireless network formed among an arbitrary number of access point and clients.
  63. """
  64. ssid = models.CharField(
  65. max_length=SSID_MAX_LENGTH,
  66. verbose_name='SSID'
  67. )
  68. group = models.ForeignKey(
  69. to='wireless.WirelessLANGroup',
  70. on_delete=models.SET_NULL,
  71. related_name='wireless_lans',
  72. blank=True,
  73. null=True
  74. )
  75. status = models.CharField(
  76. max_length=50,
  77. choices=WirelessLANStatusChoices,
  78. default=WirelessLANStatusChoices.STATUS_ACTIVE
  79. )
  80. vlan = models.ForeignKey(
  81. to='ipam.VLAN',
  82. on_delete=models.PROTECT,
  83. blank=True,
  84. null=True,
  85. verbose_name='VLAN'
  86. )
  87. tenant = models.ForeignKey(
  88. to='tenancy.Tenant',
  89. on_delete=models.PROTECT,
  90. related_name='wireless_lans',
  91. blank=True,
  92. null=True
  93. )
  94. clone_fields = ('ssid', 'group', 'tenant', 'description')
  95. class Meta:
  96. ordering = ('ssid', 'pk')
  97. verbose_name = 'Wireless LAN'
  98. def __str__(self):
  99. return self.ssid
  100. def get_absolute_url(self):
  101. return reverse('wireless:wirelesslan', args=[self.pk])
  102. def get_status_color(self):
  103. return WirelessLANStatusChoices.colors.get(self.status)
  104. def get_wireless_interface_types():
  105. # Wrap choices in a callable to avoid generating dummy migrations
  106. # when the choices are updated.
  107. return {'type__in': WIRELESS_IFACE_TYPES}
  108. class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
  109. """
  110. A point-to-point connection between two wireless Interfaces.
  111. """
  112. interface_a = models.ForeignKey(
  113. to='dcim.Interface',
  114. limit_choices_to=get_wireless_interface_types,
  115. on_delete=models.PROTECT,
  116. related_name='+',
  117. verbose_name="Interface A",
  118. )
  119. interface_b = models.ForeignKey(
  120. to='dcim.Interface',
  121. limit_choices_to=get_wireless_interface_types,
  122. on_delete=models.PROTECT,
  123. related_name='+',
  124. verbose_name="Interface B",
  125. )
  126. ssid = models.CharField(
  127. max_length=SSID_MAX_LENGTH,
  128. blank=True,
  129. verbose_name='SSID'
  130. )
  131. status = models.CharField(
  132. max_length=50,
  133. choices=LinkStatusChoices,
  134. default=LinkStatusChoices.STATUS_CONNECTED
  135. )
  136. tenant = models.ForeignKey(
  137. to='tenancy.Tenant',
  138. on_delete=models.PROTECT,
  139. related_name='wireless_links',
  140. blank=True,
  141. null=True
  142. )
  143. # Cache the associated device for the A and B interfaces. This enables filtering of WirelessLinks by their
  144. # associated Devices.
  145. _interface_a_device = models.ForeignKey(
  146. to='dcim.Device',
  147. on_delete=models.CASCADE,
  148. related_name='+',
  149. blank=True,
  150. null=True
  151. )
  152. _interface_b_device = models.ForeignKey(
  153. to='dcim.Device',
  154. on_delete=models.CASCADE,
  155. related_name='+',
  156. blank=True,
  157. null=True
  158. )
  159. clone_fields = ('ssid', 'status')
  160. class Meta:
  161. ordering = ['pk']
  162. constraints = (
  163. models.UniqueConstraint(
  164. fields=('interface_a', 'interface_b'),
  165. name='%(app_label)s_%(class)s_unique_interfaces'
  166. ),
  167. )
  168. def __str__(self):
  169. return self.ssid or f'#{self.pk}'
  170. def get_absolute_url(self):
  171. return reverse('wireless:wirelesslink', args=[self.pk])
  172. def get_status_color(self):
  173. return LinkStatusChoices.colors.get(self.status)
  174. def clean(self):
  175. # Validate interface types
  176. if self.interface_a.type not in WIRELESS_IFACE_TYPES:
  177. raise ValidationError({
  178. 'interface_a': f"{self.interface_a.get_type_display()} is not a wireless interface."
  179. })
  180. if self.interface_b.type not in WIRELESS_IFACE_TYPES:
  181. raise ValidationError({
  182. 'interface_a': f"{self.interface_b.get_type_display()} is not a wireless interface."
  183. })
  184. def save(self, *args, **kwargs):
  185. # Store the parent Device for the A and B interfaces
  186. self._interface_a_device = self.interface_a.device
  187. self._interface_b_device = self.interface_b.device
  188. super().save(*args, **kwargs)