Coverage for vpn/models/l2vpn.py: 53%
76 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 django.contrib.contenttypes.fields import GenericForeignKey
2from django.core.exceptions import ValidationError
3from django.db import models
4from django.utils.functional import cached_property
5from django.utils.translation import gettext_lazy as _
7from core.models import ObjectType
8from netbox.models import NetBoxModel, PrimaryModel
9from netbox.models.features import ContactsMixin
10from vpn.choices import L2VPNStatusChoices, L2VPNTypeChoices
12__all__ = (
13 'L2VPN',
14 'L2VPNTermination',
15)
18class L2VPN(ContactsMixin, PrimaryModel):
19 name = models.CharField(
20 verbose_name=_('name'),
21 max_length=100,
22 unique=True,
23 db_collation="natural_sort"
24 )
25 slug = models.SlugField(
26 verbose_name=_('slug'),
27 max_length=100,
28 unique=True
29 )
30 type = models.CharField(
31 verbose_name=_('type'),
32 max_length=50,
33 choices=L2VPNTypeChoices
34 )
35 status = models.CharField(
36 verbose_name=_('status'),
37 max_length=50,
38 choices=L2VPNStatusChoices,
39 default=L2VPNStatusChoices.STATUS_ACTIVE,
40 )
41 identifier = models.BigIntegerField(
42 verbose_name=_('identifier'),
43 null=True,
44 blank=True
45 )
46 import_targets = models.ManyToManyField(
47 to='ipam.RouteTarget',
48 related_name='importing_l2vpns',
49 blank=True,
50 )
51 export_targets = models.ManyToManyField(
52 to='ipam.RouteTarget',
53 related_name='exporting_l2vpns',
54 blank=True
55 )
56 tenant = models.ForeignKey(
57 to='tenancy.Tenant',
58 on_delete=models.PROTECT,
59 related_name='l2vpns',
60 blank=True,
61 null=True
62 )
64 clone_fields = ('type', 'status')
66 class Meta:
67 ordering = ('name', 'identifier')
68 verbose_name = _('L2VPN')
69 verbose_name_plural = _('L2VPNs')
71 def __str__(self):
72 if self.identifier:
73 return f'{self.name} ({self.identifier})'
74 return f'{self.name}'
76 def get_status_color(self):
77 return L2VPNStatusChoices.colors.get(self.status)
79 @cached_property
80 def can_add_termination(self):
81 if self.type in L2VPNTypeChoices.P2P and self.terminations.count() >= 2:
82 return False
83 return True
86class L2VPNTermination(NetBoxModel):
87 l2vpn = models.ForeignKey(
88 to='vpn.L2VPN',
89 on_delete=models.CASCADE,
90 related_name='terminations'
91 )
92 assigned_object_type = models.ForeignKey(
93 to='contenttypes.ContentType',
94 on_delete=models.PROTECT,
95 related_name='+'
96 )
97 assigned_object_id = models.PositiveBigIntegerField()
98 assigned_object = GenericForeignKey(
99 ct_field='assigned_object_type',
100 fk_field='assigned_object_id'
101 )
103 clone_fields = ('l2vpn',)
104 prerequisite_models = (
105 'vpn.L2VPN',
106 )
108 class Meta:
109 ordering = ('l2vpn',)
110 constraints = (
111 models.UniqueConstraint(
112 fields=('assigned_object_type', 'assigned_object_id'),
113 name='vpn_l2vpntermination_assigned_object'
114 ),
115 )
116 verbose_name = _('L2VPN termination')
117 verbose_name_plural = _('L2VPN terminations')
119 def __str__(self):
120 if self.pk is not None:
121 return f'{self.assigned_object} <> {self.l2vpn}'
122 return super().__str__()
124 def clean(self):
125 # Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown.
126 if self.assigned_object:
127 obj_id = self.assigned_object.pk
128 obj_type = ObjectType.objects.get_for_model(self.assigned_object)
129 terminations = L2VPNTermination.objects.filter(assigned_object_id=obj_id, assigned_object_type=obj_type)
130 if terminations.exclude(pk=self.pk).exists():
131 raise ValidationError(
132 _('L2VPN Termination already assigned ({assigned_object})').format(
133 assigned_object=self.assigned_object
134 )
135 )
137 # Only check if L2VPN is set and is of type P2P
138 if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P:
139 terminations_count = L2VPNTermination.objects.filter(l2vpn=self.l2vpn).exclude(pk=self.pk).count()
140 if terminations_count >= 2:
141 l2vpn_type = self.l2vpn.get_type_display()
142 raise ValidationError(
143 _(
144 '{l2vpn_type} L2VPNs cannot have more than two terminations; found {terminations_count} '
145 'already defined.'
146 ).format(l2vpn_type=l2vpn_type, terminations_count=terminations_count))
148 @property
149 def assigned_object_parent(self):
150 obj_type = ObjectType.objects.get_for_model(self.assigned_object)
151 if obj_type.model == 'vminterface':
152 return self.assigned_object.virtual_machine
153 if obj_type.model == 'interface':
154 return self.assigned_object.device
155 if obj_type.model == 'vminterface':
156 return self.assigned_object.virtual_machine
157 return None
159 @property
160 def assigned_object_site(self):
161 return self.assigned_object_parent.site