فهرست منبع

#8157: General cleanup & fix tests

jeremystretch 3 سال پیش
والد
کامیت
53372a7471

+ 14 - 11
netbox/ipam/filtersets.py

@@ -930,8 +930,11 @@ class ServiceFilterSet(NetBoxModelFilterSet):
 # L2VPN
 # L2VPN
 #
 #
 
 
-
 class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
 class L2VPNFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
+    type = django_filters.MultipleChoiceFilter(
+        choices=L2VPNTypeChoices,
+        null_value=None
+    )
     import_target_id = django_filters.ModelMultipleChoiceFilter(
     import_target_id = django_filters.ModelMultipleChoiceFilter(
         field_name='import_targets',
         field_name='import_targets',
         queryset=RouteTarget.objects.all(),
         queryset=RouteTarget.objects.all(),
@@ -972,10 +975,10 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         label='L2VPN (ID)',
         label='L2VPN (ID)',
     )
     )
     l2vpn = django_filters.ModelMultipleChoiceFilter(
     l2vpn = django_filters.ModelMultipleChoiceFilter(
-        field_name='l2vpn__name',
+        field_name='l2vpn__slug',
         queryset=L2VPN.objects.all(),
         queryset=L2VPN.objects.all(),
-        to_field_name='name',
-        label='L2VPN (name)',
+        to_field_name='slug',
+        label='L2VPN (slug)',
     )
     )
     device = MultiValueCharFilter(
     device = MultiValueCharFilter(
         method='filter_device',
         method='filter_device',
@@ -987,17 +990,16 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='pk',
         field_name='pk',
         label='Device (ID)',
         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(
     interface_id = django_filters.ModelMultipleChoiceFilter(
         field_name='interface',
         field_name='interface',
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
         label='Interface (ID)',
         label='Interface (ID)',
     )
     )
+    vminterface_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='vminterface',
+        queryset=VMInterface.objects.all(),
+        label='VM Interface (ID)',
+    )
     vlan = django_filters.ModelMultipleChoiceFilter(
     vlan = django_filters.ModelMultipleChoiceFilter(
         field_name='vlan__name',
         field_name='vlan__name',
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
@@ -1013,10 +1015,11 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         queryset=VLAN.objects.all(),
         queryset=VLAN.objects.all(),
         label='VLAN (ID)',
         label='VLAN (ID)',
     )
     )
+    assigned_object_type = ContentTypeFilter()
 
 
     class Meta:
     class Meta:
         model = L2VPNTermination
         model = L2VPNTermination
-        fields = ['id', ]
+        fields = ('id', 'assigned_object_type_id')
 
 
     def search(self, queryset, name, value):
     def search(self, queryset, name, value):
         if not value.strip():
         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 netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 from tenancy.models import Tenant
 from utilities.forms import (
 from utilities.forms import (
-    add_blank_choice, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField, StaticSelect,
+    add_blank_choice, BulkEditNullBooleanSelect, DynamicModelChoiceField, NumericArrayField, StaticSelect,
     DynamicModelMultipleChoiceField,
     DynamicModelMultipleChoiceField,
 )
 )
 
 
@@ -445,6 +445,11 @@ class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
 
 
 
 
 class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
 class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
+    type = forms.ChoiceField(
+        choices=add_blank_choice(L2VPNTypeChoices),
+        required=False,
+        widget=StaticSelect()
+    )
     tenant = DynamicModelChoiceField(
     tenant = DynamicModelChoiceField(
         queryset=Tenant.objects.all(),
         queryset=Tenant.objects.all(),
         required=False
         required=False
@@ -456,7 +461,7 @@ class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = L2VPN
     model = L2VPN
     fieldsets = (
     fieldsets = (
-        (None, ('tenant', 'description')),
+        (None, ('type', 'description', 'tenant')),
     )
     )
     nullable_fields = ('tenant', 'description',)
     nullable_fields = ('tenant', 'description',)
 
 

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

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

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

@@ -1,18 +1,19 @@
 from django import forms
 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 django.utils.translation import gettext as _
 
 
 from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
 from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
-from virtualization.models import VirtualMachine
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.models import *
 from ipam.models import *
-from ipam.models import ASN
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm
 from tenancy.forms import TenancyFilterForm
 from utilities.forms import (
 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__ = (
 __all__ = (
     'AggregateFilterForm',
     'AggregateFilterForm',
@@ -482,7 +483,8 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
 class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
 class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
     model = L2VPN
     model = L2VPN
     fieldsets = (
     fieldsets = (
-        (None, ('type', )),
+        (None, ('q', 'tag')),
+        ('Attributes', ('type', 'import_target_id', 'export_target_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
         ('Tenant', ('tenant_group_id', 'tenant_id')),
     )
     )
     type = forms.ChoiceField(
     type = forms.ChoiceField(
@@ -490,17 +492,31 @@ class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
         required=False,
         required=False,
         widget=StaticSelect()
         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):
 class L2VPNTerminationFilterForm(NetBoxModelFilterSetForm):
     model = L2VPNTermination
     model = L2VPNTermination
     fieldsets = (
     fieldsets = (
-        (None, ('l2vpn', )),
+        (None, ('l2vpn_id', 'assigned_object_type_id')),
     )
     )
-    l2vpn = DynamicModelChoiceField(
+    l2vpn_id = DynamicModelChoiceField(
         queryset=L2VPN.objects.all(),
         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,
         required=False,
         query_params={
         query_params={
             'available_on_device': '$device'
             'available_on_device': '$device'
-        }
+        },
+        label='VLAN'
     )
     )
     interface = DynamicModelChoiceField(
     interface = DynamicModelChoiceField(
         queryset=Interface.objects.all(),
         queryset=Interface.objects.all(),
@@ -935,7 +936,8 @@ class L2VPNTerminationForm(NetBoxModelForm):
         required=False,
         required=False,
         query_params={
         query_params={
             'virtual_machine_id': '$virtual_machine'
             'virtual_machine_id': '$virtual_machine'
-        }
+        },
+        label='Interface'
     )
     )
 
 
     class Meta:
     class Meta:

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

@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
                 ('slug', models.SlugField()),
                 ('slug', models.SlugField()),
                 ('type', models.CharField(max_length=50)),
                 ('type', models.CharField(max_length=50)),
                 ('identifier', models.BigIntegerField(blank=True, null=True, unique=True)),
                 ('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')),
                 ('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')),
                 ('import_targets', models.ManyToManyField(blank=True, related_name='importing_l2vpns', to='ipam.routetarget')),
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
@@ -35,7 +35,7 @@ class Migration(migrations.Migration):
             ],
             ],
             options={
             options={
                 'verbose_name': 'L2VPN',
                 'verbose_name': 'L2VPN',
-                'ordering': ('identifier', 'name'),
+                'ordering': ('name', 'identifier'),
             },
             },
         ),
         ),
         migrations.CreateModel(
         migrations.CreateModel(
@@ -51,7 +51,7 @@ class Migration(migrations.Migration):
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
                 ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
             ],
             ],
             options={
             options={
-                'verbose_name': 'L2VPN Termination',
+                'verbose_name': 'L2VPN termination',
                 'ordering': ('l2vpn',),
                 '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)
         # Populate the address field with the next available IP (if any)
         if next_available_ip := self.get_next_available_ip():
         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
         return attrs
 
 

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

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

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

@@ -1,8 +1,8 @@
 import django_tables2 as tables
 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 netbox.tables import NetBoxTable, columns
+from tenancy.tables import TenancyColumnsMixin
 
 
 __all__ = (
 __all__ = (
     'L2VPNTable',
     'L2VPNTable',
@@ -16,7 +16,7 @@ L2VPN_TARGETS = """
 """
 """
 
 
 
 
-class L2VPNTable(NetBoxTable):
+class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
     pk = columns.ToggleColumn()
     pk = columns.ToggleColumn()
     name = tables.Column(
     name = tables.Column(
         linkify=True
         linkify=True
@@ -32,7 +32,10 @@ class L2VPNTable(NetBoxTable):
 
 
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = L2VPN
         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')
         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 6', vid=656),
             VLAN(name='VLAN 7', vid=657)
             VLAN(name='VLAN 7', vid=657)
         )
         )
-
         VLAN.objects.bulk_create(vlans)
         VLAN.objects.bulk_create(vlans)
 
 
         l2vpns = (
         l2vpns = (
@@ -985,7 +984,6 @@ class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
         )
         )
-
         L2VPNTermination.objects.bulk_create(l2vpnterminations)
         L2VPNTermination.objects.bulk_create(l2vpnterminations)
 
 
         cls.create_data = [
         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 django.test import TestCase
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 
 
+from dcim.choices import InterfaceTypeChoices
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
 from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Rack, Region, Site, SiteGroup
 from ipam.choices import *
 from ipam.choices import *
 from ipam.filtersets import *
 from ipam.filtersets import *
@@ -1472,12 +1474,54 @@ class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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 = (
         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)
         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):
 class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1486,44 +1530,33 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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 = (
         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)
         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)
         VLAN.objects.bulk_create(vlans)
 
 
         l2vpns = (
         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)
         L2VPN.objects.bulk_create(l2vpns)
 
 
@@ -1534,27 +1567,34 @@ class L2VPNTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
             L2VPNTermination(l2vpn=l2vpns[0], assigned_object=interfaces[0]),
             L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
             L2VPNTermination(l2vpn=l2vpns[1], assigned_object=interfaces[1]),
             L2VPNTermination(l2vpn=l2vpns[2], assigned_object=interfaces[2]),
             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)
         L2VPNTermination.objects.bulk_create(l2vpnterminations)
 
 
-    def test_l2vpns(self):
+    def test_l2vpn(self):
         l2vpns = L2VPN.objects.all()[:2]
         l2vpns = L2VPN.objects.all()[:2]
         params = {'l2vpn_id': [l2vpns[0].pk, l2vpns[1].pk]}
         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]
         interfaces = Interface.objects.all()[:2]
         params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
         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)
         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)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
 
 
-    def test_vlans(self):
+    def test_vlan(self):
         vlans = VLAN.objects.all()[:2]
         vlans = VLAN.objects.all()[:2]
         params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
         params = {'vlan_id': [vlans[0].pk, vlans[1].pk]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

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

@@ -1,18 +1,14 @@
 import datetime
 import datetime
 
 
-from django.contrib.contenttypes.models import ContentType
 from django.test import override_settings
 from django.test import override_settings
 from django.urls import reverse
 from django.urls import reverse
 from netaddr import IPNetwork
 from netaddr import IPNetwork
 
 
 from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site, Interface
 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.choices import *
 from ipam.models import *
 from ipam.models import *
 from tenancy.models import Tenant
 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):
 class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -772,9 +768,9 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         RouteTarget.objects.bulk_create(rts)
         RouteTarget.objects.bulk_create(rts)
 
 
         l2vpns = (
         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)
         L2VPN.objects.bulk_create(l2vpns)
@@ -782,7 +778,7 @@ class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
         cls.form_data = {
         cls.form_data = {
             'name': 'L2VPN 8',
             'name': 'L2VPN 8',
             'slug': 'l2vpn-8',
             'slug': 'l2vpn-8',
-            'type': 'vxlan',
+            'type': L2VPNTypeChoices.TYPE_VXLAN,
             'identifier': 123,
             'identifier': 123,
             'description': 'Description',
             'description': 'Description',
             'import_targets': [rts[0].pk],
             'import_targets': [rts[0].pk],
@@ -805,21 +801,9 @@ class L2VPNTerminationTestCase(
 
 
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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')
         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 = (
         vlans = (
             VLAN(name='Vlan 1', vid=1001),
             VLAN(name='Vlan 1', vid=1001),
@@ -846,9 +830,9 @@ class L2VPNTerminationTestCase(
 
 
         cls.csv_data = (
         cls.csv_data = (
             "l2vpn,vlan",
             "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 = {}
         cls.bulk_edit_data = {}
@@ -857,6 +841,7 @@ class L2VPNTerminationTestCase(
     # Custom assertions
     # Custom assertions
     #
     #
 
 
+    # TODO: Remove this
     def assertInstanceEqual(self, instance, data, exclude=None, api=False):
     def assertInstanceEqual(self, instance, data, exclude=None, api=False):
         """
         """
         Override parent
         Override parent

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

@@ -6,46 +6,40 @@
 {% block content %}
 {% block content %}
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-6">
 	<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>
 	<div class="col col-md-6">
 	<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>
 </div>
 <div class="row mb-3">
 <div class="row mb-3">
@@ -58,24 +52,24 @@
 </div>
 </div>
 <div class="row mb-3">
 <div class="row mb-3">
 	<div class="col col-md-12">
 	<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>
         </div>
+      {% endif %}
     </div>
     </div>
+  </div>
 </div>
 </div>
 <div class="row mb-3">
 <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>
 </div>
 </div>
 {% endblock %}
 {% endblock %}

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

@@ -18,12 +18,12 @@
           </li>
           </li>
           <li role="presentation" class="nav-item">
           <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 %}">
             <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>
             </button>
           </li>
           </li>
           <li role="presentation" class="nav-item">
           <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 %}">
             <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>
             </button>
           </li>
           </li>
         </ul>
         </ul>