2
0

models.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  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 utilities.conversion import to_meters
  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. verbose_name=_("authentication type"),
  25. )
  26. auth_cipher = models.CharField(
  27. verbose_name=_('authentication cipher'),
  28. max_length=50,
  29. choices=WirelessAuthCipherChoices,
  30. blank=True
  31. )
  32. auth_psk = models.CharField(
  33. max_length=PSK_MAX_LENGTH,
  34. blank=True,
  35. verbose_name=_('pre-shared key')
  36. )
  37. class Meta:
  38. abstract = True
  39. class WirelessLANGroup(NestedGroupModel):
  40. """
  41. A nested grouping of WirelessLANs
  42. """
  43. name = models.CharField(
  44. verbose_name=_('name'),
  45. max_length=100,
  46. unique=True
  47. )
  48. slug = models.SlugField(
  49. verbose_name=_('slug'),
  50. max_length=100,
  51. unique=True
  52. )
  53. class Meta:
  54. ordering = ('name', 'pk')
  55. constraints = (
  56. models.UniqueConstraint(
  57. fields=('parent', 'name'),
  58. name='%(app_label)s_%(class)s_unique_parent_name'
  59. ),
  60. )
  61. verbose_name = _('wireless LAN group')
  62. verbose_name_plural = _('wireless LAN groups')
  63. def get_absolute_url(self):
  64. return reverse('wireless:wirelesslangroup', args=[self.pk])
  65. class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
  66. """
  67. A wireless network formed among an arbitrary number of access point and clients.
  68. """
  69. ssid = models.CharField(
  70. max_length=SSID_MAX_LENGTH,
  71. verbose_name=_('SSID')
  72. )
  73. group = models.ForeignKey(
  74. to='wireless.WirelessLANGroup',
  75. on_delete=models.SET_NULL,
  76. related_name='wireless_lans',
  77. blank=True,
  78. null=True
  79. )
  80. status = models.CharField(
  81. max_length=50,
  82. choices=WirelessLANStatusChoices,
  83. default=WirelessLANStatusChoices.STATUS_ACTIVE,
  84. verbose_name=_('status')
  85. )
  86. vlan = models.ForeignKey(
  87. to='ipam.VLAN',
  88. on_delete=models.PROTECT,
  89. blank=True,
  90. null=True,
  91. verbose_name=_('VLAN')
  92. )
  93. tenant = models.ForeignKey(
  94. to='tenancy.Tenant',
  95. on_delete=models.PROTECT,
  96. related_name='wireless_lans',
  97. blank=True,
  98. null=True
  99. )
  100. clone_fields = ('ssid', 'group', 'tenant', 'description')
  101. class Meta:
  102. ordering = ('ssid', 'pk')
  103. verbose_name = _('wireless LAN')
  104. verbose_name_plural = _('wireless LANs')
  105. def __str__(self):
  106. return self.ssid
  107. def get_absolute_url(self):
  108. return reverse('wireless:wirelesslan', args=[self.pk])
  109. def get_status_color(self):
  110. return WirelessLANStatusChoices.colors.get(self.status)
  111. def get_wireless_interface_types():
  112. # Wrap choices in a callable to avoid generating dummy migrations
  113. # when the choices are updated.
  114. return {'type__in': WIRELESS_IFACE_TYPES}
  115. class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
  116. """
  117. A point-to-point connection between two wireless Interfaces.
  118. """
  119. interface_a = 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 A'),
  125. )
  126. interface_b = models.ForeignKey(
  127. to='dcim.Interface',
  128. limit_choices_to=get_wireless_interface_types,
  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. distance = models.DecimalField(
  145. verbose_name=_('distance'),
  146. max_digits=8,
  147. decimal_places=2,
  148. blank=True,
  149. null=True
  150. )
  151. distance_unit = models.CharField(
  152. verbose_name=_('distance unit'),
  153. max_length=50,
  154. choices=WirelessLinkDistanceUnitChoices,
  155. blank=True,
  156. )
  157. # Stores the normalized distance (in meters) for database ordering
  158. _abs_distance = models.DecimalField(
  159. max_digits=10,
  160. decimal_places=4,
  161. blank=True,
  162. null=True
  163. )
  164. tenant = models.ForeignKey(
  165. to='tenancy.Tenant',
  166. on_delete=models.PROTECT,
  167. related_name='wireless_links',
  168. blank=True,
  169. null=True
  170. )
  171. # Cache the associated device for the A and B interfaces. This enables filtering of WirelessLinks by their
  172. # associated Devices.
  173. _interface_a_device = models.ForeignKey(
  174. to='dcim.Device',
  175. on_delete=models.CASCADE,
  176. related_name='+',
  177. blank=True,
  178. null=True
  179. )
  180. _interface_b_device = models.ForeignKey(
  181. to='dcim.Device',
  182. on_delete=models.CASCADE,
  183. related_name='+',
  184. blank=True,
  185. null=True
  186. )
  187. clone_fields = ('ssid', 'status')
  188. class Meta:
  189. ordering = ['pk']
  190. constraints = (
  191. models.UniqueConstraint(
  192. fields=('interface_a', 'interface_b'),
  193. name='%(app_label)s_%(class)s_unique_interfaces'
  194. ),
  195. )
  196. verbose_name = _('wireless link')
  197. verbose_name_plural = _('wireless links')
  198. def __str__(self):
  199. return self.ssid or f'#{self.pk}'
  200. def get_absolute_url(self):
  201. return reverse('wireless:wirelesslink', args=[self.pk])
  202. def get_status_color(self):
  203. return LinkStatusChoices.colors.get(self.status)
  204. def clean(self):
  205. super().clean()
  206. # Validate distance and distance_unit
  207. if self.distance is not None and not self.distance_unit:
  208. raise ValidationError(_("Must specify a unit when setting a wireless distance"))
  209. # Validate interface types
  210. if self.interface_a.type not in WIRELESS_IFACE_TYPES:
  211. raise ValidationError({
  212. 'interface_a': _(
  213. "{type} is not a wireless interface."
  214. ).format(type=self.interface_a.get_type_display())
  215. })
  216. if self.interface_b.type not in WIRELESS_IFACE_TYPES:
  217. raise ValidationError({
  218. 'interface_a': _(
  219. "{type} is not a wireless interface."
  220. ).format(type=self.interface_b.get_type_display())
  221. })
  222. def save(self, *args, **kwargs):
  223. # Store the given distance (if any) in meters for use in database ordering
  224. if self.distance is not None and self.distance_unit:
  225. self._abs_distance = to_meters(self.distance, self.distance_unit)
  226. else:
  227. self._abs_distance = None
  228. # Clear distance_unit if no distance is defined
  229. if self.distance is None:
  230. self.distance_unit = ''
  231. # Store the parent Device for the A and B interfaces
  232. self._interface_a_device = self.interface_a.device
  233. self._interface_b_device = self.interface_b.device
  234. super().save(*args, **kwargs)