circuits.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. from django.contrib.contenttypes.fields import GenericRelation
  2. from django.core.exceptions import ValidationError
  3. from django.db import models
  4. from django.urls import reverse
  5. from circuits.choices import *
  6. from dcim.models import LinkTermination
  7. from netbox.models import ChangeLoggedModel, OrganizationalModel, NetBoxModel
  8. from netbox.models.features import WebhooksMixin
  9. __all__ = (
  10. 'Circuit',
  11. 'CircuitTermination',
  12. 'CircuitType',
  13. )
  14. class CircuitType(OrganizationalModel):
  15. """
  16. Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
  17. "Long Haul," "Metro," or "Out-of-Band".
  18. """
  19. name = models.CharField(
  20. max_length=100,
  21. unique=True
  22. )
  23. slug = models.SlugField(
  24. max_length=100,
  25. unique=True
  26. )
  27. description = models.CharField(
  28. max_length=200,
  29. blank=True,
  30. )
  31. class Meta:
  32. ordering = ['name']
  33. def __str__(self):
  34. return self.name
  35. def get_absolute_url(self):
  36. return reverse('circuits:circuittype', args=[self.pk])
  37. class Circuit(NetBoxModel):
  38. """
  39. A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
  40. circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
  41. in Kbps.
  42. """
  43. cid = models.CharField(
  44. max_length=100,
  45. verbose_name='Circuit ID'
  46. )
  47. provider = models.ForeignKey(
  48. to='circuits.Provider',
  49. on_delete=models.PROTECT,
  50. related_name='circuits'
  51. )
  52. type = models.ForeignKey(
  53. to='CircuitType',
  54. on_delete=models.PROTECT,
  55. related_name='circuits'
  56. )
  57. status = models.CharField(
  58. max_length=50,
  59. choices=CircuitStatusChoices,
  60. default=CircuitStatusChoices.STATUS_ACTIVE
  61. )
  62. tenant = models.ForeignKey(
  63. to='tenancy.Tenant',
  64. on_delete=models.PROTECT,
  65. related_name='circuits',
  66. blank=True,
  67. null=True
  68. )
  69. install_date = models.DateField(
  70. blank=True,
  71. null=True,
  72. verbose_name='Installed'
  73. )
  74. termination_date = models.DateField(
  75. blank=True,
  76. null=True,
  77. verbose_name='Terminates'
  78. )
  79. commit_rate = models.PositiveIntegerField(
  80. blank=True,
  81. null=True,
  82. verbose_name='Commit rate (Kbps)')
  83. description = models.CharField(
  84. max_length=200,
  85. blank=True
  86. )
  87. comments = models.TextField(
  88. blank=True
  89. )
  90. # Generic relations
  91. contacts = GenericRelation(
  92. to='tenancy.ContactAssignment'
  93. )
  94. images = GenericRelation(
  95. to='extras.ImageAttachment'
  96. )
  97. # Cache associated CircuitTerminations
  98. termination_a = models.ForeignKey(
  99. to='circuits.CircuitTermination',
  100. on_delete=models.SET_NULL,
  101. related_name='+',
  102. editable=False,
  103. blank=True,
  104. null=True
  105. )
  106. termination_z = models.ForeignKey(
  107. to='circuits.CircuitTermination',
  108. on_delete=models.SET_NULL,
  109. related_name='+',
  110. editable=False,
  111. blank=True,
  112. null=True
  113. )
  114. clone_fields = [
  115. 'provider', 'type', 'status', 'tenant', 'install_date', 'termination_date', 'commit_rate', 'description',
  116. ]
  117. class Meta:
  118. ordering = ['provider', 'cid']
  119. unique_together = ['provider', 'cid']
  120. def __str__(self):
  121. return self.cid
  122. def get_absolute_url(self):
  123. return reverse('circuits:circuit', args=[self.pk])
  124. def get_status_color(self):
  125. return CircuitStatusChoices.colors.get(self.status)
  126. class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
  127. circuit = models.ForeignKey(
  128. to='circuits.Circuit',
  129. on_delete=models.CASCADE,
  130. related_name='terminations'
  131. )
  132. term_side = models.CharField(
  133. max_length=1,
  134. choices=CircuitTerminationSideChoices,
  135. verbose_name='Termination'
  136. )
  137. site = models.ForeignKey(
  138. to='dcim.Site',
  139. on_delete=models.PROTECT,
  140. related_name='circuit_terminations',
  141. blank=True,
  142. null=True
  143. )
  144. provider_network = models.ForeignKey(
  145. to='circuits.ProviderNetwork',
  146. on_delete=models.PROTECT,
  147. related_name='circuit_terminations',
  148. blank=True,
  149. null=True
  150. )
  151. port_speed = models.PositiveIntegerField(
  152. verbose_name='Port speed (Kbps)',
  153. blank=True,
  154. null=True
  155. )
  156. upstream_speed = models.PositiveIntegerField(
  157. blank=True,
  158. null=True,
  159. verbose_name='Upstream speed (Kbps)',
  160. help_text='Upstream speed, if different from port speed'
  161. )
  162. xconnect_id = models.CharField(
  163. max_length=50,
  164. blank=True,
  165. verbose_name='Cross-connect ID'
  166. )
  167. pp_info = models.CharField(
  168. max_length=100,
  169. blank=True,
  170. verbose_name='Patch panel/port(s)'
  171. )
  172. description = models.CharField(
  173. max_length=200,
  174. blank=True
  175. )
  176. class Meta:
  177. ordering = ['circuit', 'term_side']
  178. unique_together = ['circuit', 'term_side']
  179. def __str__(self):
  180. return f'Termination {self.term_side}: {self.site or self.provider_network}'
  181. def get_absolute_url(self):
  182. if self.site:
  183. return self.site.get_absolute_url()
  184. return self.provider_network.get_absolute_url()
  185. def clean(self):
  186. super().clean()
  187. # Must define either site *or* provider network
  188. if self.site is None and self.provider_network is None:
  189. raise ValidationError("A circuit termination must attach to either a site or a provider network.")
  190. if self.site and self.provider_network:
  191. raise ValidationError("A circuit termination cannot attach to both a site and a provider network.")
  192. def to_objectchange(self, action):
  193. objectchange = super().to_objectchange(action)
  194. objectchange.related_object = self.circuit
  195. return objectchange
  196. @property
  197. def parent_object(self):
  198. return self.circuit
  199. def get_peer_termination(self):
  200. peer_side = 'Z' if self.term_side == 'A' else 'A'
  201. try:
  202. return CircuitTermination.objects.prefetch_related('site').get(
  203. circuit=self.circuit,
  204. term_side=peer_side
  205. )
  206. except CircuitTermination.DoesNotExist:
  207. return None