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