models.py 6.4 KB

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