Răsfoiți Sursa

#8157: General cleanup & fix tests

jeremystretch 3 ani în urmă
părinte
comite
53372a7471

+ 14 - 11
netbox/ipam/filtersets.py

@@ -930,8 +930,11 @@ class ServiceFilterSet(NetBoxModelFilterSet):
 # L2VPN
 #
 
-
 class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+    type = django_filters.MultipleChoiceFilter(
+        choices=L2VPNTypeChoices,
+        null_value=None
+    )
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
@@ -972,10 +975,10 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         label='L2VPN (ID)',
     )
     l2vpn = django_filters.ModelMultipleChoiceFilter(
-        field_name='l2vpn__name',
+        field_name='l2vpn__slug',
         queryset=L2VPN.objects.all(),
-        to_field_name='name',
-        label='L2VPN (name)',
+        to_field_name='slug',
+        label='L2VPN (slug)',
     )
     device = MultiValueCharFilter(
         method='filter_device',
@@ -987,17 +990,16 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='pk',
         label='Device (ID)',
     )
-    interface = django_filters.ModelMultipleChoiceFilter(
-        field_name='interface__name',
-        queryset=Interface.objects.all(),
-        to_field_name='name',
-        label='Interface (name)',
-    )
     interface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='interface',
         queryset=Interface.objects.all(),
         label='Interface (ID)',
     )
+    vminterface_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='vminterface',
+        queryset=VMInterface.objects.all(),
+        label='VM Interface (ID)',
+    )
     vlan = django_filters.ModelMultipleChoiceFilter(
         field_name='vlan__name',
         queryset=VLAN.objects.all(),
@@ -1013,10 +1015,11 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         queryset=VLAN.objects.all(),
         label='VLAN (ID)',
     )
+    assigned_object_type = ContentTypeFilter()
 
     class Meta:
         model = L2VPNTermination
-        fields = ['id', ]
+        fields = ('id', 'assigned_object_type_id')
 
     def search(self, queryset, name, value):
         if not value.strip():

+ 7 - 2
netbox/ipam/forms/bulk_edit.py

@@ -8,7 +8,7 @@ from ipam.models import ASN
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from utilities.forms import (
-    add_blank_choice, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField, StaticSelect,
+    add_blank_choice, BulkEditNullBooleanSelect, DynamicModelChoiceField, NumericArrayField, StaticSelect,
     DynamicModelMultipleChoiceField,
 )
 
@@ -445,6 +445,11 @@ class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
 
 
 class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
+    type = forms.ChoiceField(
+        choices=add_blank_choice(L2VPNTypeChoices),
+        required=False,
+        widget=StaticSelect()
+    )
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         required=False
@@ -456,7 +461,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
 
     model = L2VPN
     fieldsets = (
-        (None, ('tenant', 'description')),
+        (None, ('type', 'description', 'tenant')),
     )
     nullable_fields = ('tenant', 'description',)
 

+ 1 - 1
netbox/ipam/forms/bulk_import.py

@@ -438,7 +438,7 @@ class L2VPNCSVForm(NetBoxModelCSVForm):
     )
     type = CSVChoiceField(
         choices=L2VPNTypeChoices,
-        help_text='IP protocol'
+        help_text='L2VPN type'
     )
 
     class Meta:

+ 27 - 11
netbox/ipam/forms/filtersets.py

@@ -1,18 +1,19 @@
 from django import forms
+from django.contrib.contenttypes.models import ContentType
+from django.db.models import Q
 from django.utils.translation import gettext as _
 
 from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
-from virtualization.models import VirtualMachine
 from ipam.choices import *
 from ipam.constants import *
 from ipam.models import *
-from ipam.models import ASN
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import (
-    add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, MultipleChoiceField, StaticSelect,
-    TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
+    add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
+    MultipleChoiceField, StaticSelect, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
 )
+from virtualization.models import VirtualMachine
 
 __all__ = (
     'AggregateFilterForm',
@@ -482,7 +483,8 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
 class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = L2VPN
     fieldsets = (
-        (None, ('type', )),
+        (None, ('q', 'tag')),
+        ('Attributes', ('type', 'import_target_id', 'export_target_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
     )
     type = forms.ChoiceField(
@@ -490,17 +492,31 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         required=False,
         widget=StaticSelect()
     )
+    import_target_id = DynamicModelMultipleChoiceField(
+        queryset=RouteTarget.objects.all(),
+        required=False,
+        label=_('Import targets')
+    )
+    export_target_id = DynamicModelMultipleChoiceField(
+        queryset=RouteTarget.objects.all(),
+        required=False,
+        label=_('Export targets')
+    )
+    tag = TagFilterField(model)
 
 
 class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
     model = L2VPNTermination
     fieldsets = (
-        (None, ('l2vpn', )),
+        (None, ('l2vpn_id', 'assigned_object_type_id')),
     )
-    l2vpn = DynamicModelChoiceField(
+    l2vpn_id = DynamicModelChoiceField(
         queryset=L2VPN.objects.all(),
-        required=True,
-        query_params={},
-        label='L2VPN',
-        fetch_trigger='open'
+        required=False,
+        label='L2VPN'
+    )
+    assigned_object_type_id = ContentTypeMultipleChoiceField(
+        queryset=ContentType.objects.all(),
+        required=False,
+        label='Object type'
     )

+ 4 - 2
netbox/ipam/forms/models.py

@@ -916,7 +916,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
         required=False,
         query_params={
             'available_on_device': '$device'
-        }
+        },
+        label='VLAN'
     )
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
@@ -935,7 +936,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
         required=False,
         query_params={
             'virtual_machine_id': '$virtual_machine'
-        }
+        },
+        label='Interface'
     )
 
     class Meta:

+ 3 - 3
netbox/ipam/migrations/0059_l2vpn.py

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
                 ('slug', models.SlugField()),
                 ('type', models.CharField(max_length=50)),
                 ('identifier', models.BigIntegerField(blank=True, null=True, unique=True)),
-                ('description', models.TextField(blank=True, null=True)),
+                ('description', models.CharField(blank=True, max_length=200)),
                 ('export_targets', models.ManyToManyField(blank=True, related_name='exporting_l2vpns', to='ipam.routetarget')),
                 ('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')),
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
             ],
             options={
                 'verbose_name': 'L2VPN',
-                'ordering': ('identifier', 'name'),
+                'ordering': ('name', 'identifier'),
             },
         ),
         migrations.CreateModel(
@@ -51,7 +51,7 @@ class Migration(migrations.Migration):
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
             ],
             options={
-                'verbose_name': 'L2VPN Termination',
+                'verbose_name': 'L2VPN termination',
                 'ordering': ('l2vpn',),
             },
         ),

+ 1 - 1
netbox/ipam/models/ip.py

@@ -931,7 +931,7 @@ class IPAddress(NetBoxModel):
 
         # Populate the address field with the next available IP (if any)
         if next_available_ip := self.get_next_available_ip():
-            attrs['address'] = next_available_ip
+            attrs['address'] = f'{next_available_ip}/{self.address.prefixlen}'
 
         return attrs
 

+ 7 - 4
netbox/ipam/models/l2vpn.py

@@ -31,7 +31,10 @@ class L2VPN(NetBoxModel):
         related_name='exporting_l2vpns',
         blank=True
     )
-    description = models.TextField(null=True, blank=True)
+    description = models.CharField(
+        max_length=200,
+        blank=True
+    )
     tenant = models.ForeignKey(
         to='tenancy.Tenant',
         on_delete=models.PROTECT,
@@ -44,7 +47,7 @@ class L2VPN(NetBoxModel):
     )
 
     class Meta:
-        ordering = ('identifier', 'name')
+        ordering = ('name', 'identifier')
         verbose_name = 'L2VPN'
 
     def __str__(self):
@@ -76,7 +79,7 @@ class L2VPNTermination(NetBoxModel):
 
     class Meta:
         ordering = ('l2vpn',)
-        verbose_name = 'L2VPN Termination'
+        verbose_name = 'L2VPN termination'
         constraints = (
             models.UniqueConstraint(
                 fields=('assigned_object_type', 'assigned_object_id'),
@@ -102,7 +105,7 @@ class L2VPNTermination(NetBoxModel):
                 raise ValidationError(f'L2VPN Termination already assigned ({self.assigned_object})')
 
         # Only check if L2VPN is set and is of type P2P
-        if self.l2vpn and self.l2vpn.type in L2VPNTypeChoices.P2P:
+        if hasattr(self, 'l2vpn') and self.l2vpn.type in L2VPNTypeChoices.P2P:
             terminations_count = L2VPNTermination.objects.filter(l2vpn=self.l2vpn).exclude(pk=self.pk).count()
             if terminations_count >= 2:
                 l2vpn_type = self.l2vpn.get_type_display()

+ 7 - 4
netbox/ipam/tables/l2vpn.py

@@ -1,8 +1,8 @@
 import django_tables2 as tables
 
-from ipam.models import *
-from ipam.models.l2vpn import L2VPN, L2VPNTermination
+from ipam.models import L2VPN, L2VPNTermination
 from netbox.tables import NetBoxTable, columns
+from tenancy.tables import TenancyColumnsMixin
 
 __all__ = (
     'L2VPNTable',
@@ -16,7 +16,7 @@ L2VPN_TARGETS = """
 """
 
 
-class L2VPNTable(NetBoxTable):
+class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
     pk = columns.ToggleColumn()
     name = tables.Column(
         linkify=True
@@ -32,7 +32,10 @@ class L2VPNTable(NetBoxTable):
 
     class Meta(NetBoxTable.Meta):
         model = L2VPN
-        fields = ('pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'actions')
+        fields = (
+            'pk', 'name', 'slug', 'type', 'description', 'import_targets', 'export_targets', 'tenant', 'tenant_group',
+            'actions',
+        )
         default_columns = ('pk', 'name', 'type', 'description', 'actions')
 
 

+ 0 - 2
netbox/ipam/tests/test_api.py

@@ -970,7 +970,6 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
             VLAN(name='VLAN 6', vid=656),
             VLAN(name='VLAN 7', vid=657)
         )
-
         VLAN.objects.bulk_create(vlans)
 
         l2vpns = (
@@ -985,7 +984,6 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
         )
-
         L2VPNTermination.objects.bulk_create(l2vpnterminations)
 
         cls.create_data = [

+ 82 - 42
netbox/ipam/tests/test_filtersets.py

@@ -1,6 +1,8 @@
+from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 from netaddr import IPNetwork
 
+from dcim.choices import InterfaceTypeChoices
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
 from ipam.choices import *
 from ipam.filtersets import *
@@ -1472,12 +1474,54 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
     @classmethod
     def setUpTestData(cls):
 
+        route_targets = (
+            RouteTarget(name='1:1'),
+            RouteTarget(name='1:2'),
+            RouteTarget(name='1:3'),
+            RouteTarget(name='2:1'),
+            RouteTarget(name='2:2'),
+            RouteTarget(name='2:3'),
+        )
+        RouteTarget.objects.bulk_create(route_targets)
+
         l2vpns = (
-            L2VPN(name='L2VPN 1', type='vxlan', identifier=650001),
-            L2VPN(name='L2VPN 2', type='vpws', identifier=650002),
-            L2VPN(name='L2VPN 3', type='vpls'),  # No RD
+            L2VPN(name='L2VPN 1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
+            L2VPN(name='L2VPN 2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
+            L2VPN(name='L2VPN 3', type=L2VPNTypeChoices.TYPE_VPLS),
         )
         L2VPN.objects.bulk_create(l2vpns)
+        l2vpns[0].import_targets.add(route_targets[0])
+        l2vpns[1].import_targets.add(route_targets[1])
+        l2vpns[2].import_targets.add(route_targets[2])
+        l2vpns[0].export_targets.add(route_targets[3])
+        l2vpns[1].export_targets.add(route_targets[4])
+        l2vpns[2].export_targets.add(route_targets[5])
+
+    def test_name(self):
+        params = {'name': ['L2VPN 1', 'L2VPN 2']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_identifier(self):
+        params = {'identifier': ['65001', '65002']}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_type(self):
+        params = {'type': [L2VPNTypeChoices.TYPE_VXLAN, L2VPNTypeChoices.TYPE_VPWS]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_import_targets(self):
+        route_targets = RouteTarget.objects.filter(name__in=['1:1', '1:2'])
+        params = {'import_target_id': [route_targets[0].pk, route_targets[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'import_target': [route_targets[0].name, route_targets[1].name]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+    def test_export_targets(self):
+        route_targets = RouteTarget.objects.filter(name__in=['2:1', '2:2'])
+        params = {'export_target_id': [route_targets[0].pk, route_targets[1].pk]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+        params = {'export_target': [route_targets[0].name, route_targets[1].name]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
 class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1486,44 +1530,33 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
 
     @classmethod
     def setUpTestData(cls):
-
-        site = Site.objects.create(name='Site 1')
-        manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
-        device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
-        device_role = DeviceRole.objects.create(name='Switch')
-        device = Device.objects.create(
-            name='Device 1',
-            site=site,
-            device_type=device_type,
-            device_role=device_role,
-            status='active'
-        )
-
+        device = create_test_device('Device 1')
         interfaces = (
-            Interface(name='Interface 1', device=device, type='1000baset'),
-            Interface(name='Interface 2', device=device, type='1000baset'),
-            Interface(name='Interface 3', device=device, type='1000baset'),
-            Interface(name='Interface 4', device=device, type='1000baset'),
-            Interface(name='Interface 5', device=device, type='1000baset'),
-            Interface(name='Interface 6', device=device, type='1000baset')
+            Interface(name='Interface 1', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(name='Interface 2', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
+            Interface(name='Interface 3', device=device, type=InterfaceTypeChoices.TYPE_1GE_FIXED),
         )
-
         Interface.objects.bulk_create(interfaces)
 
-        vlans = (
-            VLAN(name='VLAN 1', vid=651),
-            VLAN(name='VLAN 2', vid=652),
-            VLAN(name='VLAN 3', vid=653),
-            VLAN(name='VLAN 4', vid=654),
-            VLAN(name='VLAN 5', vid=655)
+        vm = create_test_virtualmachine('Virtual Machine 1')
+        vminterfaces = (
+            VMInterface(name='Interface 1', virtual_machine=vm),
+            VMInterface(name='Interface 2', virtual_machine=vm),
+            VMInterface(name='Interface 3', virtual_machine=vm),
         )
+        VMInterface.objects.bulk_create(vminterfaces)
 
+        vlans = (
+            VLAN(name='VLAN 1', vid=101),
+            VLAN(name='VLAN 2', vid=102),
+            VLAN(name='VLAN 3', vid=103),
+        )
         VLAN.objects.bulk_create(vlans)
 
         l2vpns = (
-            L2VPN(name='L2VPN 1', type='vxlan', identifier=650001),
-            L2VPN(name='L2VPN 2', type='vpws', identifier=650002),
-            L2VPN(name='L2VPN 3', type='vpls'),  # No RD,
+            L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=65001),
+            L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=65002),
+            L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'),  # No RD,
         )
         L2VPN.objects.bulk_create(l2vpns)
 
@@ -1534,27 +1567,34 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
             L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
             L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]),
+            L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vminterfaces[0]),
+            L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vminterfaces[1]),
+            L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vminterfaces[2]),
         )
-
         L2VPNTermination.objects.bulk_create(l2vpnterminations)
 
-    def test_l2vpns(self):
+    def test_l2vpn(self):
         l2vpns = L2VPN.objects.all()[:2]
         params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
-        params = {'l2vpn': ['L2VPN 1', 'L2VPN 2']}
-        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+        params = {'l2vpn': [l2vpns[0].slug, l2vpns[1].slug]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
+
+    def test_content_type(self):
+        params = {'assigned_object_type_id': ContentType.objects.get(model='vlan').pk}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
 
-    def test_interfaces(self):
+    def test_interface(self):
         interfaces = Interface.objects.all()[:2]
         params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
-        qs = self.filterset(params, self.queryset).qs
-        results = qs.all()
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-        params = {'interface': ['Interface 1', 'Interface 2']}
+
+    def test_vminterface(self):
+        vminterfaces = VMInterface.objects.all()[:2]
+        params = {'vminterface_id': [vminterfaces[0].pk, vminterfaces[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
-    def test_vlans(self):
+    def test_vlan(self):
         vlans = VLAN.objects.all()[:2]
         params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

+ 11 - 26
netbox/ipam/tests/test_views.py

@@ -1,18 +1,14 @@
 import datetime
 
-from django.contrib.contenttypes.models import ContentType
 from django.test import override_settings
 from django.urls import reverse
 from netaddr import IPNetwork
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface
-from extras.choices import ObjectChangeActionChoices
-from extras.models import ObjectChange
 from ipam.choices import *
 from ipam.models import *
 from tenancy.models import Tenant
-from users.models import ObjectPermission
-from utilities.testing import ViewTestCases, create_tags, post_data
+from utilities.testing import ViewTestCases, create_test_device, create_tags
 
 
 class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -772,9 +768,9 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         RouteTarget.objects.bulk_create(rts)
 
         l2vpns = (
-            L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier='650001'),
-            L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vxlan', identifier='650002'),
-            L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vxlan', identifier='650003')
+            L2VPN(name='L2VPN 1', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650001'),
+            L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650002'),
+            L2VPN(name='L2VPN 3', slug='l2vpn-3', type=L2VPNTypeChoices.TYPE_VXLAN, identifier='650003')
         )
 
         L2VPN.objects.bulk_create(l2vpns)
@@ -782,7 +778,7 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         cls.form_data = {
             'name': 'L2VPN 8',
             'slug': 'l2vpn-8',
-            'type': 'vxlan',
+            'type': L2VPNTypeChoices.TYPE_VXLAN,
             'identifier': 123,
             'description': 'Description',
             'import_targets': [rts[0].pk],
@@ -805,21 +801,9 @@ class L2VPNTerminationTestCase(
 
     @classmethod
     def setUpTestData(cls):
-        site = Site.objects.create(name='Site 1')
-        manufacturer = Manufacturer.objects.create(name='Manufacturer 1')
-        device_type = DeviceType.objects.create(model='Device Type 1', manufacturer=manufacturer)
-        device_role = DeviceRole.objects.create(name='Switch')
-        device = Device.objects.create(
-            name='Device 1',
-            site=site,
-            device_type=device_type,
-            device_role=device_role,
-            status='active'
-        )
-
+        device = create_test_device('Device 1')
         interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
-        l2vpn = L2VPN.objects.create(name='L2VPN 1', type='vxlan', identifier=650001)
-        l2vpn_vlans = L2VPN.objects.create(name='L2VPN 2', type='vxlan', identifier=650002)
+        l2vpn = L2VPN.objects.create(name='L2VPN 1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=650001)
 
         vlans = (
             VLAN(name='Vlan 1', vid=1001),
@@ -846,9 +830,9 @@ class L2VPNTerminationTestCase(
 
         cls.csv_data = (
             "l2vpn,vlan",
-            "L2VPN 2,Vlan 4",
-            "L2VPN 2,Vlan 5",
-            "L2VPN 2,Vlan 6",
+            "L2VPN 1,Vlan 4",
+            "L2VPN 1,Vlan 5",
+            "L2VPN 1,Vlan 6",
         )
 
         cls.bulk_edit_data = {}
@@ -857,6 +841,7 @@ class L2VPNTerminationTestCase(
     # Custom assertions
     #
 
+    # TODO: Remove this
     def assertInstanceEqual(self, instance, data, exclude=None, api=False):
         """
         Override parent

+ 46 - 52
netbox/templates/ipam/l2vpn.html

@@ -6,46 +6,40 @@
 {% block content %}
 <div class="row mb-3">
 	<div class="col col-md-6">
-        <div class="card">
-            <h5 class="card-header">
-                L2VPN Attributes
-            </h5>
-            <div class="card-body">
-                <table class="table table-hover attr-table
-                    <tr>
-                        <th scope="row">Name</th>
-                        <td>{{ object.name|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Slug</th>
-                        <td>{{ object.slug|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Identifier</th>
-                        <td>{{ object.identifier|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Type</th>
-                        <td>{{ object.get_type_display }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Description</th>
-                        <td>{{ object.description|placeholder }}</td>
-                    </tr>
-                    <tr>
-                        <th scope="row">Tenant</th>
-                        <td>{{ object.tenant|placeholder }}</td>
-                    </tr>
-                </table>
-            </div>
-        </div>
-        {% include 'inc/panels/contacts.html' %}
-        {% plugin_left_page object %}
+    <div class="card">
+      <h5 class="card-header">L2VPN Attributes</h5>
+      <div class="card-body">
+        <table class="table table-hover attr-table">
+          <tr>
+            <th scope="row">Name</th>
+            <td>{{ object.name|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Identifier</th>
+            <td>{{ object.identifier|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Type</th>
+            <td>{{ object.get_type_display }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Description</th>
+            <td>{{ object.description|placeholder }}</td>
+          </tr>
+          <tr>
+            <th scope="row">Tenant</th>
+            <td>{{ object.tenant|linkify|placeholder }}</td>
+          </tr>
+        </table>
+      </div>
+    </div>
+    {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:l2vpn_list' %}
+    {% plugin_left_page object %}
 	</div>
 	<div class="col col-md-6">
-        {% include 'inc/panels/tags.html' with tags=object.tags.all url='circuits:circuit_list' %}
-        {% include 'inc/panels/custom_fields.html' %}
-        {% plugin_right_page object %}
+      {% include 'inc/panels/contacts.html' %}
+      {% include 'inc/panels/custom_fields.html' %}
+      {% plugin_right_page object %}
     </div>
 </div>
 <div class="row mb-3">
@@ -58,24 +52,24 @@
 </div>
 <div class="row mb-3">
 	<div class="col col-md-12">
-        <div class="card">
-          <h5 class="card-header">Terminations</h5>
-          <div class="card-body">
-            {% render_table terminations_table 'inc/table.html' %}
-          </div>
-          {% if perms.ipam.add_l2vpntermination %}
-            <div class="card-footer text-end noprint">
-              <a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
-                <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Termination
-              </a>
-            </div>
-          {% endif %}
+    <div class="card">
+      <h5 class="card-header">Terminations</h5>
+      <div class="card-body">
+        {% render_table terminations_table 'inc/table.html' %}
+      </div>
+      {% if perms.ipam.add_l2vpntermination %}
+        <div class="card-footer text-end noprint">
+          <a href="{% url 'ipam:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm">
+            <i class="mdi mdi-plus-thick" aria-hidden="true"></i> Add a Termination
+          </a>
         </div>
+      {% endif %}
     </div>
+  </div>
 </div>
 <div class="row mb-3">
-	<div class="col col-md-12">
-        {% plugin_full_width_page object %}
+  <div class="col col-md-12">
+    {% plugin_full_width_page object %}
   </div>
 </div>
 {% endblock %}

+ 2 - 2
netbox/templates/ipam/l2vpntermination_edit.html

@@ -18,12 +18,12 @@
           </li>
           <li role="presentation" class="nav-item">
             <button role="tab" type="button" id="interface_tab" data-bs-toggle="tab" aria-controls="interface" data-bs-target="#interface" class="nav-link {% if form.initial.interface %}active{% endif %}">
-              Interface
+              Device
             </button>
           </li>
           <li role="presentation" class="nav-item">
             <button role="tab" type="button" id="vminterface_tab" data-bs-toggle="tab" aria-controls="vminterface" data-bs-target="#vminterface" class="nav-link {% if form.initial.vminterface %}active{% endif %}">
-              VM Interface
+              Virtual Machine
             </button>
           </li>
         </ul>