models.py 6.2 KB

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