circuits.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 circuits.choices import *
  6. from dcim.models import CabledObjectModel
  7. from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
  8. from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ImageAttachmentsMixin, TagsMixin
  9. from utilities.fields import ColorField
  10. __all__ = (
  11. 'Circuit',
  12. 'CircuitTermination',
  13. 'CircuitType',
  14. )
  15. class CircuitType(OrganizationalModel):
  16. """
  17. Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
  18. "Long Haul," "Metro," or "Out-of-Band".
  19. """
  20. color = ColorField(
  21. verbose_name=_('color'),
  22. blank=True
  23. )
  24. def get_absolute_url(self):
  25. return reverse('circuits:circuittype', args=[self.pk])
  26. class Meta:
  27. ordering = ('name',)
  28. verbose_name = _('circuit type')
  29. verbose_name_plural = _('circuit types')
  30. class Circuit(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
  31. """
  32. A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
  33. circuits. Each circuit is also assigned a CircuitType and a Site, and may optionally be assigned to a particular
  34. ProviderAccount. Circuit port speed and commit rate are measured in Kbps.
  35. """
  36. cid = models.CharField(
  37. max_length=100,
  38. verbose_name=_('circuit ID'),
  39. help_text=_('Unique circuit ID')
  40. )
  41. provider = models.ForeignKey(
  42. to='circuits.Provider',
  43. on_delete=models.PROTECT,
  44. related_name='circuits'
  45. )
  46. provider_account = models.ForeignKey(
  47. to='circuits.ProviderAccount',
  48. on_delete=models.PROTECT,
  49. related_name='circuits',
  50. blank=True,
  51. null=True
  52. )
  53. type = models.ForeignKey(
  54. to='CircuitType',
  55. on_delete=models.PROTECT,
  56. related_name='circuits'
  57. )
  58. status = models.CharField(
  59. verbose_name=_('status'),
  60. max_length=50,
  61. choices=CircuitStatusChoices,
  62. default=CircuitStatusChoices.STATUS_ACTIVE
  63. )
  64. tenant = models.ForeignKey(
  65. to='tenancy.Tenant',
  66. on_delete=models.PROTECT,
  67. related_name='circuits',
  68. blank=True,
  69. null=True
  70. )
  71. install_date = models.DateField(
  72. blank=True,
  73. null=True,
  74. verbose_name=_('installed')
  75. )
  76. termination_date = models.DateField(
  77. blank=True,
  78. null=True,
  79. verbose_name=_('terminates')
  80. )
  81. commit_rate = models.PositiveIntegerField(
  82. blank=True,
  83. null=True,
  84. verbose_name=_('commit rate (Kbps)'),
  85. help_text=_("Committed rate")
  86. )
  87. # Cache associated CircuitTerminations
  88. termination_a = models.ForeignKey(
  89. to='circuits.CircuitTermination',
  90. on_delete=models.SET_NULL,
  91. related_name='+',
  92. editable=False,
  93. blank=True,
  94. null=True
  95. )
  96. termination_z = models.ForeignKey(
  97. to='circuits.CircuitTermination',
  98. on_delete=models.SET_NULL,
  99. related_name='+',
  100. editable=False,
  101. blank=True,
  102. null=True
  103. )
  104. clone_fields = (
  105. 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate',
  106. 'description',
  107. )
  108. prerequisite_models = (
  109. 'circuits.CircuitType',
  110. 'circuits.Provider',
  111. )
  112. class Meta:
  113. ordering = ['provider', 'provider_account', 'cid']
  114. constraints = (
  115. models.UniqueConstraint(
  116. fields=('provider', 'cid'),
  117. name='%(app_label)s_%(class)s_unique_provider_cid'
  118. ),
  119. models.UniqueConstraint(
  120. fields=('provider_account', 'cid'),
  121. name='%(app_label)s_%(class)s_unique_provideraccount_cid'
  122. ),
  123. )
  124. verbose_name = _('circuit')
  125. verbose_name_plural = _('circuits')
  126. def __str__(self):
  127. return self.cid
  128. def get_absolute_url(self):
  129. return reverse('circuits:circuit', args=[self.pk])
  130. def get_status_color(self):
  131. return CircuitStatusChoices.colors.get(self.status)
  132. def clean(self):
  133. super().clean()
  134. if self.provider_account and self.provider != self.provider_account.provider:
  135. raise ValidationError({'provider_account': "The assigned account must belong to the assigned provider."})
  136. class CircuitTermination(
  137. CustomFieldsMixin,
  138. CustomLinksMixin,
  139. TagsMixin,
  140. ChangeLoggedModel,
  141. CabledObjectModel
  142. ):
  143. circuit = models.ForeignKey(
  144. to='circuits.Circuit',
  145. on_delete=models.CASCADE,
  146. related_name='terminations'
  147. )
  148. term_side = models.CharField(
  149. max_length=1,
  150. choices=CircuitTerminationSideChoices,
  151. verbose_name=_('termination')
  152. )
  153. site = models.ForeignKey(
  154. to='dcim.Site',
  155. on_delete=models.PROTECT,
  156. related_name='circuit_terminations',
  157. blank=True,
  158. null=True
  159. )
  160. provider_network = models.ForeignKey(
  161. to='circuits.ProviderNetwork',
  162. on_delete=models.PROTECT,
  163. related_name='circuit_terminations',
  164. blank=True,
  165. null=True
  166. )
  167. port_speed = models.PositiveIntegerField(
  168. verbose_name=_('port speed (Kbps)'),
  169. blank=True,
  170. null=True,
  171. help_text=_('Physical circuit speed')
  172. )
  173. upstream_speed = models.PositiveIntegerField(
  174. blank=True,
  175. null=True,
  176. verbose_name=_('upstream speed (Kbps)'),
  177. help_text=_('Upstream speed, if different from port speed')
  178. )
  179. xconnect_id = models.CharField(
  180. max_length=50,
  181. blank=True,
  182. verbose_name=_('cross-connect ID'),
  183. help_text=_('ID of the local cross-connect')
  184. )
  185. pp_info = models.CharField(
  186. max_length=100,
  187. blank=True,
  188. verbose_name=_('patch panel/port(s)'),
  189. help_text=_('Patch panel ID and port number(s)')
  190. )
  191. description = models.CharField(
  192. verbose_name=_('description'),
  193. max_length=200,
  194. blank=True
  195. )
  196. class Meta:
  197. ordering = ['circuit', 'term_side']
  198. constraints = (
  199. models.UniqueConstraint(
  200. fields=('circuit', 'term_side'),
  201. name='%(app_label)s_%(class)s_unique_circuit_term_side'
  202. ),
  203. )
  204. verbose_name = _('circuit termination')
  205. verbose_name_plural = _('circuit terminations')
  206. def __str__(self):
  207. return f'{self.circuit}: Termination {self.term_side}'
  208. def get_absolute_url(self):
  209. return self.circuit.get_absolute_url()
  210. def clean(self):
  211. super().clean()
  212. # Must define either site *or* provider network
  213. if self.site is None and self.provider_network is None:
  214. raise ValidationError("A circuit termination must attach to either a site or a provider network.")
  215. if self.site and self.provider_network:
  216. raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
  217. def to_objectchange(self, action):
  218. objectchange = super().to_objectchange(action)
  219. objectchange.related_object = self.circuit
  220. return objectchange
  221. @property
  222. def parent_object(self):
  223. return self.circuit
  224. def get_peer_termination(self):
  225. peer_side = 'Z' if self.term_side == 'A' else 'A'
  226. try:
  227. return CircuitTermination.objects.prefetch_related('site').get(
  228. circuit=self.circuit,
  229. term_side=peer_side
  230. )
  231. except CircuitTermination.DoesNotExist:
  232. return None