virtual_circuits.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. from functools import cached_property
  2. from django.contrib.contenttypes.fields import GenericRelation
  3. from django.core.exceptions import ValidationError
  4. from django.db import models
  5. from django.urls import reverse
  6. from django.utils.translation import gettext_lazy as _
  7. from circuits.choices import *
  8. from netbox.models import ChangeLoggedModel, PrimaryModel
  9. from netbox.models.features import ContactsMixin, CustomFieldsMixin, CustomLinksMixin, ExportTemplatesMixin, TagsMixin
  10. from .base import BaseCircuitType
  11. __all__ = (
  12. 'VirtualCircuit',
  13. 'VirtualCircuitTermination',
  14. 'VirtualCircuitType',
  15. )
  16. class VirtualCircuitType(BaseCircuitType):
  17. """
  18. Like physical circuits, virtual circuits can be organized by their functional role. For example, a user might wish
  19. to categorize virtual circuits by their technological nature or by product name.
  20. """
  21. class Meta:
  22. ordering = ('name',)
  23. verbose_name = _('virtual circuit type')
  24. verbose_name_plural = _('virtual circuit types')
  25. class VirtualCircuit(ContactsMixin, PrimaryModel):
  26. """
  27. A virtual connection between two or more endpoints, delivered across one or more physical circuits.
  28. """
  29. cid = models.CharField(
  30. max_length=100,
  31. verbose_name=_('circuit ID'),
  32. help_text=_('Unique circuit ID')
  33. )
  34. provider_network = models.ForeignKey(
  35. to='circuits.ProviderNetwork',
  36. on_delete=models.PROTECT,
  37. related_name='virtual_circuits'
  38. )
  39. provider_account = models.ForeignKey(
  40. to='circuits.ProviderAccount',
  41. on_delete=models.PROTECT,
  42. related_name='virtual_circuits',
  43. blank=True,
  44. null=True
  45. )
  46. type = models.ForeignKey(
  47. to='circuits.VirtualCircuitType',
  48. on_delete=models.PROTECT,
  49. related_name='virtual_circuits'
  50. )
  51. status = models.CharField(
  52. verbose_name=_('status'),
  53. max_length=50,
  54. choices=CircuitStatusChoices,
  55. default=CircuitStatusChoices.STATUS_ACTIVE
  56. )
  57. tenant = models.ForeignKey(
  58. to='tenancy.Tenant',
  59. on_delete=models.PROTECT,
  60. related_name='virtual_circuits',
  61. blank=True,
  62. null=True
  63. )
  64. group_assignments = GenericRelation(
  65. to='circuits.CircuitGroupAssignment',
  66. content_type_field='member_type',
  67. object_id_field='member_id',
  68. related_query_name='virtual_circuit'
  69. )
  70. clone_fields = (
  71. 'provider_network', 'provider_account', 'status', 'tenant', 'description',
  72. )
  73. prerequisite_models = (
  74. 'circuits.ProviderNetwork',
  75. 'circuits.VirtualCircuitType',
  76. )
  77. class Meta:
  78. ordering = ['provider_network', 'provider_account', 'cid']
  79. constraints = (
  80. models.UniqueConstraint(
  81. fields=('provider_network', 'cid'),
  82. name='%(app_label)s_%(class)s_unique_provider_network_cid'
  83. ),
  84. models.UniqueConstraint(
  85. fields=('provider_account', 'cid'),
  86. name='%(app_label)s_%(class)s_unique_provideraccount_cid'
  87. ),
  88. )
  89. indexes = (
  90. models.Index(fields=('provider_network', 'provider_account', 'cid')), # Default ordering
  91. )
  92. verbose_name = _('virtual circuit')
  93. verbose_name_plural = _('virtual circuits')
  94. def __str__(self):
  95. return self.cid
  96. def get_status_color(self):
  97. return CircuitStatusChoices.colors.get(self.status)
  98. def clean(self):
  99. super().clean()
  100. if self.provider_account and self.provider_network.provider != self.provider_account.provider:
  101. raise ValidationError({
  102. 'provider_account': "The assigned account must belong to the provider of the assigned network."
  103. })
  104. @property
  105. def provider(self):
  106. return self.provider_network.provider
  107. class VirtualCircuitTermination(
  108. CustomFieldsMixin,
  109. CustomLinksMixin,
  110. ExportTemplatesMixin,
  111. TagsMixin,
  112. ChangeLoggedModel
  113. ):
  114. virtual_circuit = models.ForeignKey(
  115. to='circuits.VirtualCircuit',
  116. on_delete=models.CASCADE,
  117. related_name='terminations'
  118. )
  119. role = models.CharField(
  120. verbose_name=_('role'),
  121. max_length=50,
  122. choices=VirtualCircuitTerminationRoleChoices,
  123. default=VirtualCircuitTerminationRoleChoices.ROLE_PEER
  124. )
  125. interface = models.OneToOneField(
  126. to='dcim.Interface',
  127. on_delete=models.CASCADE,
  128. related_name='virtual_circuit_termination'
  129. )
  130. description = models.CharField(
  131. verbose_name=_('description'),
  132. max_length=200,
  133. blank=True
  134. )
  135. class Meta:
  136. ordering = ['virtual_circuit', 'role', 'pk']
  137. indexes = (
  138. models.Index(fields=('virtual_circuit', 'role', 'id')), # Default ordering
  139. )
  140. verbose_name = _('virtual circuit termination')
  141. verbose_name_plural = _('virtual circuit terminations')
  142. def __str__(self):
  143. return f'{self.virtual_circuit}: {self.get_role_display()} termination'
  144. def get_absolute_url(self):
  145. return reverse('circuits:virtualcircuittermination', args=[self.pk])
  146. def get_role_color(self):
  147. return VirtualCircuitTerminationRoleChoices.colors.get(self.role)
  148. def to_objectchange(self, action):
  149. objectchange = super().to_objectchange(action)
  150. objectchange.related_object = self.virtual_circuit
  151. return objectchange
  152. @property
  153. def parent_object(self):
  154. return self.virtual_circuit
  155. @cached_property
  156. def peer_terminations(self):
  157. if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
  158. return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
  159. role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
  160. )
  161. if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
  162. return self.virtual_circuit.terminations.filter(
  163. role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
  164. )
  165. if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
  166. return self.virtual_circuit.terminations.filter(
  167. role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
  168. )
  169. # Fallback for unexpected roles
  170. return self.virtual_circuit.terminations.none()
  171. def clean(self):
  172. super().clean()
  173. if self.interface and not self.interface.is_virtual:
  174. raise ValidationError("Virtual circuits may be terminated only to virtual interfaces.")