models.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. from django.apps import apps
  2. from django.core.exceptions import ValidationError
  3. from django.db import models
  4. from django.urls import reverse
  5. from mptt.models import MPTTModel, TreeForeignKey
  6. from dcim.choices import LinkStatusChoices
  7. from dcim.constants import WIRELESS_IFACE_TYPES
  8. from netbox.models import NestedGroupModel, NetBoxModel
  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="Auth Type",
  25. )
  26. auth_cipher = models.CharField(
  27. max_length=50,
  28. choices=WirelessAuthCipherChoices,
  29. blank=True
  30. )
  31. auth_psk = models.CharField(
  32. max_length=PSK_MAX_LENGTH,
  33. blank=True,
  34. verbose_name='Pre-shared key'
  35. )
  36. class Meta:
  37. abstract = True
  38. class WirelessLANGroup(NestedGroupModel):
  39. """
  40. A nested grouping of WirelessLANs
  41. """
  42. name = models.CharField(
  43. max_length=100,
  44. unique=True
  45. )
  46. slug = models.SlugField(
  47. max_length=100,
  48. unique=True
  49. )
  50. parent = TreeForeignKey(
  51. to='self',
  52. on_delete=models.CASCADE,
  53. related_name='children',
  54. blank=True,
  55. null=True,
  56. db_index=True
  57. )
  58. description = models.CharField(
  59. max_length=200,
  60. blank=True
  61. )
  62. class Meta:
  63. ordering = ('name', 'pk')
  64. constraints = (
  65. models.UniqueConstraint(
  66. fields=('parent', 'name'),
  67. name='%(app_label)s_%(class)s_unique_parent_name'
  68. ),
  69. )
  70. verbose_name = 'Wireless LAN Group'
  71. def __str__(self):
  72. return self.name
  73. def get_absolute_url(self):
  74. return reverse('wireless:wirelesslangroup', args=[self.pk])
  75. class WirelessLAN(WirelessAuthenticationBase, NetBoxModel):
  76. """
  77. A wireless network formed among an arbitrary number of access point and clients.
  78. """
  79. ssid = models.CharField(
  80. max_length=SSID_MAX_LENGTH,
  81. verbose_name='SSID'
  82. )
  83. group = models.ForeignKey(
  84. to='wireless.WirelessLANGroup',
  85. on_delete=models.SET_NULL,
  86. related_name='wireless_lans',
  87. blank=True,
  88. null=True
  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. description = models.CharField(
  105. max_length=200,
  106. blank=True
  107. )
  108. clone_fields = ('ssid', 'group', 'tenant', 'description')
  109. class Meta:
  110. ordering = ('ssid', 'pk')
  111. verbose_name = 'Wireless LAN'
  112. def __str__(self):
  113. return self.ssid
  114. def get_absolute_url(self):
  115. return reverse('wireless:wirelesslan', args=[self.pk])
  116. def get_wireless_interface_types():
  117. # Wrap choices in a callable to avoid generating dummy migrations
  118. # when the choices are updated.
  119. return {'type__in': WIRELESS_IFACE_TYPES}
  120. class WirelessLink(WirelessAuthenticationBase, NetBoxModel):
  121. """
  122. A point-to-point connection between two wireless Interfaces.
  123. """
  124. interface_a = models.ForeignKey(
  125. to='dcim.Interface',
  126. limit_choices_to=get_wireless_interface_types,
  127. on_delete=models.PROTECT,
  128. related_name='+',
  129. verbose_name="Interface A",
  130. )
  131. interface_b = models.ForeignKey(
  132. to='dcim.Interface',
  133. limit_choices_to=get_wireless_interface_types,
  134. on_delete=models.PROTECT,
  135. related_name='+',
  136. verbose_name="Interface B",
  137. )
  138. ssid = models.CharField(
  139. max_length=SSID_MAX_LENGTH,
  140. blank=True,
  141. verbose_name='SSID'
  142. )
  143. status = models.CharField(
  144. max_length=50,
  145. choices=LinkStatusChoices,
  146. default=LinkStatusChoices.STATUS_CONNECTED
  147. )
  148. tenant = models.ForeignKey(
  149. to='tenancy.Tenant',
  150. on_delete=models.PROTECT,
  151. related_name='wireless_links',
  152. blank=True,
  153. null=True
  154. )
  155. description = models.CharField(
  156. max_length=200,
  157. blank=True
  158. )
  159. # Cache the associated device for the A and B interfaces. This enables filtering of WirelessLinks by their
  160. # associated Devices.
  161. _interface_a_device = models.ForeignKey(
  162. to='dcim.Device',
  163. on_delete=models.CASCADE,
  164. related_name='+',
  165. blank=True,
  166. null=True
  167. )
  168. _interface_b_device = models.ForeignKey(
  169. to='dcim.Device',
  170. on_delete=models.CASCADE,
  171. related_name='+',
  172. blank=True,
  173. null=True
  174. )
  175. clone_fields = ('ssid', 'status')
  176. class Meta:
  177. ordering = ['pk']
  178. constraints = (
  179. models.UniqueConstraint(
  180. fields=('interface_a', 'interface_b'),
  181. name='%(app_label)s_%(class)s_unique_interfaces'
  182. ),
  183. )
  184. def __str__(self):
  185. return f'#{self.pk}'
  186. @classmethod
  187. def get_prerequisite_models(cls):
  188. return [apps.get_model('dcim.Interface'), ]
  189. def get_absolute_url(self):
  190. return reverse('wireless:wirelesslink', args=[self.pk])
  191. def get_status_color(self):
  192. return LinkStatusChoices.colors.get(self.status)
  193. def clean(self):
  194. # Validate interface types
  195. if self.interface_a.type not in WIRELESS_IFACE_TYPES:
  196. raise ValidationError({
  197. 'interface_a': f"{self.interface_a.get_type_display()} is not a wireless interface."
  198. })
  199. if self.interface_b.type not in WIRELESS_IFACE_TYPES:
  200. raise ValidationError({
  201. 'interface_a': f"{self.interface_b.get_type_display()} is not a wireless interface."
  202. })
  203. def save(self, *args, **kwargs):
  204. # Store the parent Device for the A and B interfaces
  205. self._interface_a_device = self.interface_a.device
  206. self._interface_b_device = self.interface_b.device
  207. super().save(*args, **kwargs)