diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py
index 32dcdc5bbe6..09933f2deab 100644
--- a/netbox/dcim/api/serializers.py
+++ b/netbox/dcim/api/serializers.py
@@ -2,8 +2,8 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
-from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from timezone_field.rest_framework import TimeZoneSerializerField
@@ -12,8 +12,7 @@
from dcim.models import *
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import (
- NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
- NestedVRFSerializer,
+ NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
@@ -27,6 +26,7 @@
from users.api.nested_serializers import NestedUserSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedClusterSerializer
+from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
from wireless.choices import *
from wireless.models import WirelessLAN
diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py
index ffd3879a850..36540f3e358 100644
--- a/netbox/dcim/filtersets.py
+++ b/netbox/dcim/filtersets.py
@@ -5,7 +5,7 @@
from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
from ipam.filtersets import PrimaryIPFilterSet
-from ipam.models import ASN, L2VPN, IPAddress, VRF
+from ipam.models import ASN, IPAddress, VRF
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
)
@@ -17,6 +17,7 @@
TreeNodeMultipleChoiceFilter,
)
from virtualization.models import Cluster
+from vpn.models import L2VPN
from wireless.choices import WirelessRoleChoices, WirelessChannelChoices
from .choices import *
from .constants import *
diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py
index d0d32118745..1c8713a28df 100644
--- a/netbox/dcim/forms/filtersets.py
+++ b/netbox/dcim/forms/filtersets.py
@@ -7,12 +7,13 @@
from dcim.models import *
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
-from ipam.models import ASN, L2VPN, VRF
+from ipam.models import ASN, VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.widgets import APISelectMultiple, NumberWithOptions
+from vpn.models import L2VPN
from wireless.choices import *
__all__ = (
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 705af763704..94ae2d6a69d 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -730,7 +730,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_query_name='interface'
)
l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
+ to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='interface',
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index a24f9ea6d34..bf2ce9de4dc 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -316,8 +316,8 @@
{% if perms.dcim.add_interface %}
Child Interface
{% endif %}
- {% if perms.ipam.add_l2vpntermination %}
- L2VPN Termination
+ {% if perms.vpn.add_l2vpntermination %}
+ L2VPN Termination
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
Assign FHRP Group
diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py
index 9e150e2cb09..17d8d74a7fc 100644
--- a/netbox/ipam/api/nested_serializers.py
+++ b/netbox/ipam/api/nested_serializers.py
@@ -2,7 +2,6 @@
from rest_framework import serializers
from ipam import models
-from ipam.models.l2vpn import L2VPNTermination, L2VPN
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
@@ -14,8 +13,6 @@
'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
- 'NestedL2VPNSerializer',
- 'NestedL2VPNTerminationSerializer',
'NestedPrefixSerializer',
'NestedRIRSerializer',
'NestedRoleSerializer',
@@ -223,28 +220,3 @@ class NestedServiceSerializer(WritableNestedSerializer):
class Meta:
model = models.Service
fields = ['id', 'url', 'display', 'name', 'protocol', 'ports']
-
-#
-# L2VPN
-#
-
-
-class NestedL2VPNSerializer(WritableNestedSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
-
- class Meta:
- model = L2VPN
- fields = [
- 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type'
- ]
-
-
-class NestedL2VPNTerminationSerializer(WritableNestedSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
- l2vpn = NestedL2VPNSerializer()
-
- class Meta:
- model = L2VPNTermination
- fields = [
- 'id', 'url', 'display', 'l2vpn'
- ]
diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py
index 6882de56dbf..33aa55a93ed 100644
--- a/netbox/ipam/api/serializers.py
+++ b/netbox/ipam/api/serializers.py
@@ -12,8 +12,9 @@
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
-from .nested_serializers import *
+from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from .field_serializers import IPAddressField, IPNetworkField
+from .nested_serializers import *
#
@@ -479,54 +480,3 @@ class Meta:
'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'ports', 'protocol', 'ipaddresses',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
]
-
-#
-# L2VPN
-#
-
-
-class L2VPNSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpn-detail')
- type = ChoiceField(choices=L2VPNTypeChoices, required=False)
- import_targets = SerializedPKRelatedField(
- queryset=RouteTarget.objects.all(),
- serializer=NestedRouteTargetSerializer,
- required=False,
- many=True
- )
- export_targets = SerializedPKRelatedField(
- queryset=RouteTarget.objects.all(),
- serializer=NestedRouteTargetSerializer,
- required=False,
- many=True
- )
- tenant = NestedTenantSerializer(required=False, allow_null=True)
-
- class Meta:
- model = L2VPN
- fields = [
- 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
- 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
- ]
-
-
-class L2VPNTerminationSerializer(NetBoxModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='ipam-api:l2vpntermination-detail')
- l2vpn = NestedL2VPNSerializer()
- assigned_object_type = ContentTypeField(
- queryset=ContentType.objects.all()
- )
- assigned_object = serializers.SerializerMethodField(read_only=True)
-
- class Meta:
- model = L2VPNTermination
- fields = [
- 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
- 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
- ]
-
- @extend_schema_field(serializers.JSONField(allow_null=True))
- def get_assigned_object(self, instance):
- serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
- context = {'request': self.context['request']}
- return serializer(instance.assigned_object, context=context).data
diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py
index 442fd22403a..bae9d80486c 100644
--- a/netbox/ipam/api/urls.py
+++ b/netbox/ipam/api/urls.py
@@ -23,8 +23,6 @@
router.register('vlans', views.VLANViewSet)
router.register('service-templates', views.ServiceTemplateViewSet)
router.register('services', views.ServiceViewSet)
-router.register('l2vpns', views.L2VPNViewSet)
-router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'ipam-api'
diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py
index 662b393de40..688fe42e2c0 100644
--- a/netbox/ipam/api/views.py
+++ b/netbox/ipam/api/views.py
@@ -14,7 +14,6 @@
from dcim.models import Site
from ipam import filtersets
from ipam.models import *
-from ipam.models import L2VPN, L2VPNTermination
from ipam.utils import get_next_available_prefix
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
@@ -178,18 +177,6 @@ class ServiceViewSet(NetBoxModelViewSet):
filterset_class = filtersets.ServiceFilterSet
-class L2VPNViewSet(NetBoxModelViewSet):
- queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
- serializer_class = serializers.L2VPNSerializer
- filterset_class = filtersets.L2VPNFilterSet
-
-
-class L2VPNTerminationViewSet(NetBoxModelViewSet):
- queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
- serializer_class = serializers.L2VPNTerminationSerializer
- filterset_class = filtersets.L2VPNTerminationFilterSet
-
-
#
# Views
#
diff --git a/netbox/ipam/choices.py b/netbox/ipam/choices.py
index 436cbd04098..017fd043054 100644
--- a/netbox/ipam/choices.py
+++ b/netbox/ipam/choices.py
@@ -172,52 +172,3 @@ class ServiceProtocolChoices(ChoiceSet):
(PROTOCOL_UDP, 'UDP'),
(PROTOCOL_SCTP, 'SCTP'),
)
-
-
-class L2VPNTypeChoices(ChoiceSet):
- TYPE_VPLS = 'vpls'
- TYPE_VPWS = 'vpws'
- TYPE_EPL = 'epl'
- TYPE_EVPL = 'evpl'
- TYPE_EPLAN = 'ep-lan'
- TYPE_EVPLAN = 'evp-lan'
- TYPE_EPTREE = 'ep-tree'
- TYPE_EVPTREE = 'evp-tree'
- TYPE_VXLAN = 'vxlan'
- TYPE_VXLAN_EVPN = 'vxlan-evpn'
- TYPE_MPLS_EVPN = 'mpls-evpn'
- TYPE_PBB_EVPN = 'pbb-evpn'
-
- CHOICES = (
- ('VPLS', (
- (TYPE_VPWS, 'VPWS'),
- (TYPE_VPLS, 'VPLS'),
- )),
- ('VXLAN', (
- (TYPE_VXLAN, 'VXLAN'),
- (TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
- )),
- ('L2VPN E-VPN', (
- (TYPE_MPLS_EVPN, 'MPLS EVPN'),
- (TYPE_PBB_EVPN, 'PBB EVPN'),
- )),
- ('E-Line', (
- (TYPE_EPL, 'EPL'),
- (TYPE_EVPL, 'EVPL'),
- )),
- ('E-LAN', (
- (TYPE_EPLAN, 'Ethernet Private LAN'),
- (TYPE_EVPLAN, 'Ethernet Virtual Private LAN'),
- )),
- ('E-Tree', (
- (TYPE_EPTREE, 'Ethernet Private Tree'),
- (TYPE_EVPTREE, 'Ethernet Virtual Private Tree'),
- )),
- )
-
- P2P = (
- TYPE_VPWS,
- TYPE_EPL,
- TYPE_EPLAN,
- TYPE_EPTREE
- )
diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py
index f26fce2b51b..6dffd32870e 100644
--- a/netbox/ipam/constants.py
+++ b/netbox/ipam/constants.py
@@ -86,9 +86,3 @@
# 16-bit port number
SERVICE_PORT_MIN = 1
SERVICE_PORT_MAX = 65535
-
-L2VPN_ASSIGNMENT_MODELS = Q(
- Q(app_label='dcim', model='interface') |
- Q(app_label='ipam', model='vlan') |
- Q(app_label='virtualization', model='vminterface')
-)
diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py
index ba944e3ada9..08d22dd2383 100644
--- a/netbox/ipam/filtersets.py
+++ b/netbox/ipam/filtersets.py
@@ -4,8 +4,8 @@
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.utils.translation import gettext as _
-from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
from netaddr.core import AddrFormatError
from dcim.models import Device, Interface, Region, Site, SiteGroup
@@ -15,6 +15,7 @@
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
from virtualization.models import VirtualMachine, VMInterface
+from vpn.models import L2VPN
from .choices import *
from .models import *
@@ -26,8 +27,6 @@
'FHRPGroupFilterSet',
'IPAddressFilterSet',
'IPRangeFilterSet',
- 'L2VPNFilterSet',
- 'L2VPNTerminationFilterSet',
'PrefixFilterSet',
'PrimaryIPFilterSet',
'RIRFilterSet',
@@ -1059,182 +1058,6 @@ def search(self, queryset, name, value):
return queryset.filter(qs_filter)
-#
-# 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(),
- label=_('Import target'),
- )
- import_target = django_filters.ModelMultipleChoiceFilter(
- field_name='import_targets__name',
- queryset=RouteTarget.objects.all(),
- to_field_name='name',
- label=_('Import target (name)'),
- )
- export_target_id = django_filters.ModelMultipleChoiceFilter(
- field_name='export_targets',
- queryset=RouteTarget.objects.all(),
- label=_('Export target'),
- )
- export_target = django_filters.ModelMultipleChoiceFilter(
- field_name='export_targets__name',
- queryset=RouteTarget.objects.all(),
- to_field_name='name',
- label=_('Export target (name)'),
- )
-
- class Meta:
- model = L2VPN
- fields = ['id', 'identifier', 'name', 'slug', 'type', 'description']
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
- try:
- qs_filter |= Q(identifier=int(value))
- except ValueError:
- pass
- return queryset.filter(qs_filter)
-
-
-class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
- l2vpn_id = django_filters.ModelMultipleChoiceFilter(
- queryset=L2VPN.objects.all(),
- label=_('L2VPN (ID)'),
- )
- l2vpn = django_filters.ModelMultipleChoiceFilter(
- field_name='l2vpn__slug',
- queryset=L2VPN.objects.all(),
- to_field_name='slug',
- label=_('L2VPN (slug)'),
- )
- region = MultiValueCharFilter(
- method='filter_region',
- field_name='slug',
- label=_('Region (slug)'),
- )
- region_id = MultiValueNumberFilter(
- method='filter_region',
- field_name='pk',
- label=_('Region (ID)'),
- )
- site = MultiValueCharFilter(
- method='filter_site',
- field_name='slug',
- label=_('Site (slug)'),
- )
- site_id = MultiValueNumberFilter(
- method='filter_site',
- field_name='pk',
- label=_('Site (ID)'),
- )
- device = django_filters.ModelMultipleChoiceFilter(
- field_name='interface__device__name',
- queryset=Device.objects.all(),
- to_field_name='name',
- label=_('Device (name)'),
- )
- device_id = django_filters.ModelMultipleChoiceFilter(
- field_name='interface__device',
- queryset=Device.objects.all(),
- label=_('Device (ID)'),
- )
- virtual_machine = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__virtual_machine__name',
- queryset=VirtualMachine.objects.all(),
- to_field_name='name',
- label=_('Virtual machine (name)'),
- )
- virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__virtual_machine',
- queryset=VirtualMachine.objects.all(),
- label=_('Virtual machine (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 = django_filters.ModelMultipleChoiceFilter(
- field_name='vminterface__name',
- queryset=VMInterface.objects.all(),
- to_field_name='name',
- label=_('VM interface (name)'),
- )
- 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(),
- to_field_name='name',
- label=_('VLAN (name)'),
- )
- vlan_vid = django_filters.NumberFilter(
- field_name='vlan__vid',
- label=_('VLAN number (1-4094)'),
- )
- vlan_id = django_filters.ModelMultipleChoiceFilter(
- field_name='vlan',
- queryset=VLAN.objects.all(),
- label=_('VLAN (ID)'),
- )
- assigned_object_type = ContentTypeFilter()
-
- class Meta:
- model = L2VPNTermination
- fields = ('id', 'assigned_object_type_id')
-
- def search(self, queryset, name, value):
- if not value.strip():
- return queryset
- qs_filter = Q(l2vpn__name__icontains=value)
- return queryset.filter(qs_filter)
-
- def filter_assigned_object(self, queryset, name, value):
- qs = queryset.filter(
- Q(**{'{}__in'.format(name): value})
- )
- return qs
-
- def filter_site(self, queryset, name, value):
- qs = queryset.filter(
- Q(
- Q(**{'vlan__site__{}__in'.format(name): value}) |
- Q(**{'interface__device__site__{}__in'.format(name): value}) |
- Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
- )
- )
- return qs
-
- def filter_region(self, queryset, name, value):
- qs = queryset.filter(
- Q(
- Q(**{'vlan__site__region__{}__in'.format(name): value}) |
- Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
- Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
- )
- )
- return qs
-
-
class PrimaryIPFilterSet(django_filters.FilterSet):
"""
An inheritable FilterSet for models which support primary IP assignment.
diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py
index f0a8286fc8f..bf4825be994 100644
--- a/netbox/ipam/forms/bulk_edit.py
+++ b/netbox/ipam/forms/bulk_edit.py
@@ -23,8 +23,6 @@
'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
- 'L2VPNBulkEditForm',
- 'L2VPNTerminationBulkEditForm',
'PrefixBulkEditForm',
'RIRBulkEditForm',
'RoleBulkEditForm',
@@ -596,32 +594,3 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm):
class ServiceBulkEditForm(ServiceTemplateBulkEditForm):
model = Service
-
-
-class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
- type = forms.ChoiceField(
- label=_('Type'),
- choices=add_blank_choice(L2VPNTypeChoices),
- required=False
- )
- tenant = DynamicModelChoiceField(
- label=_('Tenant'),
- queryset=Tenant.objects.all(),
- required=False
- )
- description = forms.CharField(
- label=_('Description'),
- max_length=200,
- required=False
- )
- comments = CommentField()
-
- model = L2VPN
- fieldsets = (
- (None, ('type', 'tenant', 'description')),
- )
- nullable_fields = ('tenant', 'description', 'comments')
-
-
-class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):
- model = L2VPN
diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py
index ed3ceec2b30..0627a676546 100644
--- a/netbox/ipam/forms/bulk_import.py
+++ b/netbox/ipam/forms/bulk_import.py
@@ -1,6 +1,5 @@
from django import forms
from django.contrib.contenttypes.models import ContentType
-from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface, Site
@@ -21,8 +20,6 @@
'FHRPGroupImportForm',
'IPAddressImportForm',
'IPRangeImportForm',
- 'L2VPNImportForm',
- 'L2VPNTerminationImportForm',
'PrefixImportForm',
'RIRImportForm',
'RoleImportForm',
@@ -529,92 +526,3 @@ def clean_ipaddresses(self):
)
return self.cleaned_data['ipaddresses']
-
-
-class L2VPNImportForm(NetBoxModelImportForm):
- tenant = CSVModelChoiceField(
- label=_('Tenant'),
- queryset=Tenant.objects.all(),
- required=False,
- to_field_name='name',
- )
- type = CSVChoiceField(
- label=_('Type'),
- choices=L2VPNTypeChoices,
- help_text=_('L2VPN type')
- )
-
- class Meta:
- model = L2VPN
- fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
- 'comments', 'tags')
-
-
-class L2VPNTerminationImportForm(NetBoxModelImportForm):
- l2vpn = CSVModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=True,
- to_field_name='name',
- label=_('L2VPN'),
- )
- device = CSVModelChoiceField(
- label=_('Device'),
- queryset=Device.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Parent device (for interface)')
- )
- virtual_machine = CSVModelChoiceField(
- label=_('Virtual machine'),
- queryset=VirtualMachine.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Parent virtual machine (for interface)')
- )
- interface = CSVModelChoiceField(
- label=_('Interface'),
- queryset=Interface.objects.none(), # Can also refer to VMInterface
- required=False,
- to_field_name='name',
- help_text=_('Assigned interface (device or VM)')
- )
- vlan = CSVModelChoiceField(
- label=_('VLAN'),
- queryset=VLAN.objects.all(),
- required=False,
- to_field_name='name',
- help_text=_('Assigned VLAN')
- )
-
- class Meta:
- model = L2VPNTermination
- fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
-
- def __init__(self, data=None, *args, **kwargs):
- super().__init__(data, *args, **kwargs)
-
- if data:
-
- # Limit interface queryset by device or VM
- if data.get('device'):
- self.fields['interface'].queryset = Interface.objects.filter(
- **{f"device__{self.fields['device'].to_field_name}": data['device']}
- )
- elif data.get('virtual_machine'):
- self.fields['interface'].queryset = VMInterface.objects.filter(
- **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
- )
-
- def clean(self):
- super().clean()
-
- if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
- raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
- if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
- raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
- if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
- raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
-
- # if this is an update we might not have interface or vlan in the form data
- if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
- self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py
index a8ca91901d3..c7dad372d5b 100644
--- a/netbox/ipam/forms/filtersets.py
+++ b/netbox/ipam/forms/filtersets.py
@@ -1,5 +1,4 @@
from django import forms
-from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
from dcim.models import Location, Rack, Region, Site, SiteGroup, Device
@@ -9,10 +8,9 @@
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice
-from utilities.forms.fields import (
- ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
-)
+from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from virtualization.models import VirtualMachine
+from vpn.models import L2VPN
__all__ = (
'AggregateFilterForm',
@@ -21,8 +19,6 @@
'FHRPGroupFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
- 'L2VPNFilterForm',
- 'L2VPNTerminationFilterForm',
'PrefixFilterForm',
'RIRFilterForm',
'RoleFilterForm',
@@ -539,90 +535,3 @@ class ServiceFilterForm(ServiceTemplateFilterForm):
label=_('Virtual Machine'),
)
tag = TagFilterField(model)
-
-
-class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
- model = L2VPN
- fieldsets = (
- (None, ('q', 'filter_id', 'tag')),
- (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
- (_('Tenant'), ('tenant_group_id', 'tenant_id')),
- )
- type = forms.ChoiceField(
- label=_('Type'),
- choices=add_blank_choice(L2VPNTypeChoices),
- required=False
- )
- 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, ('filter_id', 'l2vpn_id',)),
- (_('Assigned Object'), (
- 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
- )),
- )
- l2vpn_id = DynamicModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=False,
- label=_('L2VPN')
- )
- assigned_object_type_id = ContentTypeMultipleChoiceField(
- queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
- required=False,
- label=_('Assigned Object Type'),
- limit_choices_to=L2VPN_ASSIGNMENT_MODELS
- )
- region_id = DynamicModelMultipleChoiceField(
- queryset=Region.objects.all(),
- required=False,
- label=_('Region')
- )
- site_id = DynamicModelMultipleChoiceField(
- queryset=Site.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'region_id': '$region_id'
- },
- label=_('Site')
- )
- device_id = DynamicModelMultipleChoiceField(
- queryset=Device.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('Device')
- )
- vlan_id = DynamicModelMultipleChoiceField(
- queryset=VLAN.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('VLAN')
- )
- virtual_machine_id = DynamicModelMultipleChoiceField(
- queryset=VirtualMachine.objects.all(),
- required=False,
- null_option='None',
- query_params={
- 'site_id': '$site_id'
- },
- label=_('Virtual Machine')
- )
diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py
index dd9e6b3e438..6c445ef2733 100644
--- a/netbox/ipam/forms/model_forms.py
+++ b/netbox/ipam/forms/model_forms.py
@@ -29,8 +29,6 @@
'IPAddressBulkAddForm',
'IPAddressForm',
'IPRangeForm',
- 'L2VPNForm',
- 'L2VPNTerminationForm',
'PrefixForm',
'RIRForm',
'RoleForm',
@@ -754,97 +752,3 @@ def clean(self):
self.cleaned_data['description'] = service_template.description
elif not all(self.cleaned_data[f] for f in ('name', 'protocol', 'ports')):
raise forms.ValidationError("Must specify name, protocol, and port(s) if not using a service template.")
-
-
-#
-# L2VPN
-#
-
-
-class L2VPNForm(TenancyForm, NetBoxModelForm):
- slug = SlugField()
- import_targets = DynamicModelMultipleChoiceField(
- label=_('Import targets'),
- queryset=RouteTarget.objects.all(),
- required=False
- )
- export_targets = DynamicModelMultipleChoiceField(
- label=_('Export targets'),
- queryset=RouteTarget.objects.all(),
- required=False
- )
- comments = CommentField()
-
- fieldsets = (
- (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
- (_('Route Targets'), ('import_targets', 'export_targets')),
- (_('Tenancy'), ('tenant_group', 'tenant')),
- )
-
- class Meta:
- model = L2VPN
- fields = (
- 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
- 'comments', 'tags'
- )
-
-
-class L2VPNTerminationForm(NetBoxModelForm):
- l2vpn = DynamicModelChoiceField(
- queryset=L2VPN.objects.all(),
- required=True,
- query_params={},
- label=_('L2VPN'),
- fetch_trigger='open'
- )
- vlan = DynamicModelChoiceField(
- queryset=VLAN.objects.all(),
- required=False,
- selector=True,
- label=_('VLAN')
- )
- interface = DynamicModelChoiceField(
- label=_('Interface'),
- queryset=Interface.objects.all(),
- required=False,
- selector=True
- )
- vminterface = DynamicModelChoiceField(
- queryset=VMInterface.objects.all(),
- required=False,
- selector=True,
- label=_('Interface')
- )
-
- class Meta:
- model = L2VPNTermination
- fields = ('l2vpn', )
-
- def __init__(self, *args, **kwargs):
- instance = kwargs.get('instance')
- initial = kwargs.get('initial', {}).copy()
-
- if instance:
- if type(instance.assigned_object) is Interface:
- initial['interface'] = instance.assigned_object
- elif type(instance.assigned_object) is VLAN:
- initial['vlan'] = instance.assigned_object
- elif type(instance.assigned_object) is VMInterface:
- initial['vminterface'] = instance.assigned_object
- kwargs['initial'] = initial
-
- super().__init__(*args, **kwargs)
-
- def clean(self):
- super().clean()
-
- interface = self.cleaned_data.get('interface')
- vminterface = self.cleaned_data.get('vminterface')
- vlan = self.cleaned_data.get('vlan')
-
- if not (interface or vminterface or vlan):
- raise ValidationError(_('A termination must specify an interface or VLAN.'))
- if len([x for x in (interface, vminterface, vlan) if x]) > 1:
- raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
-
- self.instance.assigned_object = interface or vminterface or vlan
diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py
index 596b5eb7851..6627c540e55 100644
--- a/netbox/ipam/graphql/schema.py
+++ b/netbox/ipam/graphql/schema.py
@@ -1,9 +1,8 @@
import graphene
-from ipam import models
-from utilities.graphql_optimizer import gql_query_optimizer
+from ipam import models
from netbox.graphql.fields import ObjectField, ObjectListField
-
+from utilities.graphql_optimizer import gql_query_optimizer
from .types import *
@@ -38,18 +37,6 @@ def resolve_ip_address_list(root, info, **kwargs):
def resolve_ip_range_list(root, info, **kwargs):
return gql_query_optimizer(models.IPRange.objects.all(), info)
- l2vpn = ObjectField(L2VPNType)
- l2vpn_list = ObjectListField(L2VPNType)
-
- def resolve_l2vpn_list(root, info, **kwargs):
- return gql_query_optimizer(models.L2VPN.objects.all(), info)
-
- l2vpn_termination = ObjectField(L2VPNTerminationType)
- l2vpn_termination_list = ObjectListField(L2VPNTerminationType)
-
- def resolve_l2vpn_termination_list(root, info, **kwargs):
- return gql_query_optimizer(models.L2VPNTermination.objects.all(), info)
-
prefix = ObjectField(PrefixType)
prefix_list = ObjectListField(PrefixType)
diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py
index 6e834512e0f..b4350f9f259 100644
--- a/netbox/ipam/graphql/types.py
+++ b/netbox/ipam/graphql/types.py
@@ -1,6 +1,5 @@
import graphene
-from extras.graphql.mixins import ContactsMixin
from ipam import filtersets, models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@@ -13,8 +12,6 @@
'FHRPGroupAssignmentType',
'IPAddressType',
'IPRangeType',
- 'L2VPNType',
- 'L2VPNTerminationType',
'PrefixType',
'RIRType',
'RoleType',
@@ -188,19 +185,3 @@ class Meta:
model = models.VRF
fields = '__all__'
filterset_class = filtersets.VRFFilterSet
-
-
-class L2VPNType(ContactsMixin, NetBoxObjectType):
- class Meta:
- model = models.L2VPN
- fields = '__all__'
- filtersets_class = filtersets.L2VPNFilterSet
-
-
-class L2VPNTerminationType(NetBoxObjectType):
- assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
-
- class Meta:
- model = models.L2VPNTermination
- exclude = ('assigned_object_type', 'assigned_object_id')
- filtersets_class = filtersets.L2VPNTerminationFilterSet
diff --git a/netbox/ipam/migrations/0068_move_l2vpn.py b/netbox/ipam/migrations/0068_move_l2vpn.py
new file mode 100644
index 00000000000..b1a059de1b1
--- /dev/null
+++ b/netbox/ipam/migrations/0068_move_l2vpn.py
@@ -0,0 +1,64 @@
+from django.db import migrations
+
+
+def update_content_types(apps, schema_editor):
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+
+ # Delete the new ContentTypes effected by the new models in the vpn app
+ ContentType.objects.filter(app_label='vpn', model='l2vpn').delete()
+ ContentType.objects.filter(app_label='vpn', model='l2vpntermination').delete()
+
+ # Update the app labels of the original ContentTypes for ipam.L2VPN and ipam.L2VPNTermination to ensure
+ # that any foreign key references are preserved
+ ContentType.objects.filter(app_label='ipam', model='l2vpn').update(app_label='vpn')
+ ContentType.objects.filter(app_label='ipam', model='l2vpntermination').update(app_label='vpn')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ipam', '0067_ipaddress_index_host'),
+ ]
+
+ operations = [
+ migrations.RemoveConstraint(
+ model_name='l2vpntermination',
+ name='ipam_l2vpntermination_assigned_object',
+ ),
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='assigned_object_type',
+ ),
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='l2vpn',
+ ),
+ migrations.RemoveField(
+ model_name='l2vpntermination',
+ name='tags',
+ ),
+ migrations.DeleteModel(
+ name='L2VPN',
+ ),
+ migrations.DeleteModel(
+ name='L2VPNTermination',
+ ),
+ ],
+ database_operations=[
+ migrations.AlterModelTable(
+ name='L2VPN',
+ table='vpn_l2vpn',
+ ),
+ migrations.AlterModelTable(
+ name='L2VPNTermination',
+ table='vpn_l2vpntermination',
+ ),
+ ],
+ ),
+ migrations.RunPython(
+ code=update_content_types,
+ reverse_code=migrations.RunPython.noop
+ ),
+ ]
diff --git a/netbox/ipam/models/__init__.py b/netbox/ipam/models/__init__.py
index a00919ee0eb..0d0b3d6ac05 100644
--- a/netbox/ipam/models/__init__.py
+++ b/netbox/ipam/models/__init__.py
@@ -3,27 +3,5 @@
from .fhrp import *
from .vrfs import *
from .ip import *
-from .l2vpn import *
from .services import *
from .vlans import *
-
-__all__ = (
- 'ASN',
- 'ASNRange',
- 'Aggregate',
- 'IPAddress',
- 'IPRange',
- 'FHRPGroup',
- 'FHRPGroupAssignment',
- 'L2VPN',
- 'L2VPNTermination',
- 'Prefix',
- 'RIR',
- 'Role',
- 'RouteTarget',
- 'Service',
- 'ServiceTemplate',
- 'VLAN',
- 'VLANGroup',
- 'VRF',
-)
diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py
index b6aed539885..1327a6e9df1 100644
--- a/netbox/ipam/models/vlans.py
+++ b/netbox/ipam/models/vlans.py
@@ -183,9 +183,8 @@ class VLAN(PrimaryModel):
null=True,
help_text=_("The primary function of this VLAN")
)
-
l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
+ to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='vlan'
diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py
index c08acce1b92..a1cddbb1a8e 100644
--- a/netbox/ipam/search.py
+++ b/netbox/ipam/search.py
@@ -1,5 +1,5 @@
-from . import models
from netbox.search import SearchIndex, register_search
+from . import models
@register_search
@@ -69,18 +69,6 @@ class IPRangeIndex(SearchIndex):
display_attrs = ('vrf', 'tenant', 'status', 'role', 'description')
-@register_search
-class L2VPNIndex(SearchIndex):
- model = models.L2VPN
- fields = (
- ('name', 100),
- ('slug', 110),
- ('description', 500),
- ('comments', 5000),
- )
- display_attrs = ('type', 'identifier', 'tenant', 'description')
-
-
@register_search
class PrefixIndex(SearchIndex):
model = models.Prefix
diff --git a/netbox/ipam/tables/__init__.py b/netbox/ipam/tables/__init__.py
index 7d04a5fea77..95676b82c66 100644
--- a/netbox/ipam/tables/__init__.py
+++ b/netbox/ipam/tables/__init__.py
@@ -1,7 +1,6 @@
from .asn import *
from .fhrp import *
from .ip import *
-from .l2vpn import *
from .services import *
from .vlans import *
from .vrfs import *
diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py
index d696c8dae7a..cb633e162bf 100644
--- a/netbox/ipam/tests/test_api.py
+++ b/netbox/ipam/tests/test_api.py
@@ -1100,96 +1100,3 @@ def setUpTestData(cls):
'ports': [6],
},
]
-
-
-class L2VPNTest(APIViewTestCases.APIViewTestCase):
- model = L2VPN
- brief_fields = ['display', 'id', 'identifier', 'name', 'slug', 'type', 'url']
- create_data = [
- {
- 'name': 'L2VPN 4',
- 'slug': 'l2vpn-4',
- 'type': 'vxlan',
- 'identifier': 33343344
- },
- {
- 'name': 'L2VPN 5',
- 'slug': 'l2vpn-5',
- 'type': 'vxlan',
- 'identifier': 33343345
- },
- {
- 'name': 'L2VPN 6',
- 'slug': 'l2vpn-6',
- 'type': 'vpws',
- 'identifier': 33343346
- },
- ]
- bulk_update_data = {
- 'description': 'New description',
- }
-
- @classmethod
- def setUpTestData(cls):
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
-
-class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
- model = L2VPNTermination
- brief_fields = ['display', 'id', 'l2vpn', 'url']
-
- @classmethod
- def setUpTestData(cls):
-
- 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),
- VLAN(name='VLAN 6', vid=656),
- VLAN(name='VLAN 7', vid=657)
- )
- VLAN.objects.bulk_create(vlans)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
- L2VPNTermination.objects.bulk_create(l2vpnterminations)
-
- cls.create_data = [
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[3].pk,
- },
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[4].pk,
- },
- {
- 'l2vpn': l2vpns[0].pk,
- 'assigned_object_type': 'ipam.vlan',
- 'assigned_object_id': vlans[5].pk,
- },
- ]
-
- cls.bulk_update_data = {
- 'l2vpn': l2vpns[2].pk
- }
diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py
index 95237605647..07f3e637f7c 100644
--- a/netbox/ipam/tests/test_filtersets.py
+++ b/netbox/ipam/tests/test_filtersets.py
@@ -7,9 +7,9 @@
from ipam.choices import *
from ipam.filtersets import *
from ipam.models import *
+from tenancy.models import Tenant, TenantGroup
from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
-from tenancy.models import Tenant, TenantGroup
class ASNRangeTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -1616,163 +1616,3 @@ def test_ipaddress(self):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipaddress': [str(ips[0].address), str(ips[1].address)]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
-
-class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
- queryset = L2VPN.objects.all()
- filterset = L2VPNFilterSet
-
- @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', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
- L2VPN(name='L2VPN 3', slug='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_slug(self):
- params = {'slug': ['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):
- queryset = L2VPNTermination.objects.all()
- filterset = L2VPNTerminationFilterSet
-
- @classmethod
- def setUpTestData(cls):
- device = create_test_device('Device 1')
- interfaces = (
- 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)
-
- 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', 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)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]),
- 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_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(), 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_interface(self):
- interfaces = Interface.objects.all()[:2]
- params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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_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)
- params = {'vlan': ['VLAN 1', 'VLAN 2']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
-
- def test_site(self):
- site = Site.objects.all().first()
- params = {'site_id': [site.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'site': ['site-1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
-
- def test_device(self):
- device = Device.objects.all().first()
- params = {'device_id': [device.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'device': ['Device 1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
-
- def test_virtual_machine(self):
- virtual_machine = VirtualMachine.objects.all().first()
- params = {'virtual_machine_id': [virtual_machine.pk]}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
- params = {'virtual_machine': ['Virtual Machine 1']}
- self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py
index 06cd9b445aa..5a37807a7bc 100644
--- a/netbox/ipam/tests/test_models.py
+++ b/netbox/ipam/tests/test_models.py
@@ -1,10 +1,9 @@
-from netaddr import IPNetwork, IPSet
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
+from netaddr import IPNetwork, IPSet
-from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site
-from ipam.choices import IPAddressRoleChoices, PrefixStatusChoices
-from ipam.models import Aggregate, IPAddress, IPRange, Prefix, RIR, VLAN, VLANGroup, VRF, L2VPN, L2VPNTermination
+from ipam.choices import *
+from ipam.models import *
class TestAggregate(TestCase):
@@ -539,76 +538,3 @@ def test_get_next_available_vid(self):
VLAN.objects.create(name='VLAN 104', vid=104, group=vlangroup)
self.assertEqual(vlangroup.get_next_available_vid(), 105)
-
-
-class TestL2VPNTermination(TestCase):
-
- @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)
- role = DeviceRole.objects.create(name='Switch')
- device = Device.objects.create(
- name='Device 1',
- site=site,
- device_type=device_type,
- role=role,
- status='active'
- )
-
- 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.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),
- VLAN(name='VLAN 6', vid=656),
- VLAN(name='VLAN 7', vid=657)
- )
-
- VLAN.objects.bulk_create(vlans)
-
- l2vpns = (
- L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
- L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
- L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
- )
- L2VPN.objects.bulk_create(l2vpns)
-
- l2vpnterminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
-
- L2VPNTermination.objects.bulk_create(l2vpnterminations)
-
- def test_duplicate_interface_terminations(self):
- device = Device.objects.first()
- interface = Interface.objects.filter(device=device).first()
- l2vpn = L2VPN.objects.first()
-
- L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface)
- duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface)
-
- self.assertRaises(ValidationError, duplicate.clean)
-
- def test_duplicate_vlan_terminations(self):
- vlan = Interface.objects.first()
- l2vpn = L2VPN.objects.first()
-
- L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
- duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan)
- self.assertRaises(ValidationError, duplicate.clean)
diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py
index a37584f0f13..bc42341ba37 100644
--- a/netbox/ipam/tests/test_views.py
+++ b/netbox/ipam/tests/test_views.py
@@ -9,7 +9,7 @@
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
-from utilities.testing import ViewTestCases, create_test_device, create_tags
+from utilities.testing import ViewTestCases, create_tags
class ASNRangeTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -986,142 +986,3 @@ def test_create_from_template(self):
self.assertEqual(instance.protocol, service_template.protocol)
self.assertEqual(instance.ports, service_template.ports)
self.assertEqual(instance.description, service_template.description)
-
-
-class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
- model = L2VPN
-
- @classmethod
- def setUpTestData(cls):
- rts = (
- RouteTarget(name='64534:123'),
- RouteTarget(name='64534:321')
- )
- RouteTarget.objects.bulk_create(rts)
-
- l2vpns = (
- 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)
-
- cls.csv_data = (
- 'name,slug,type,identifier',
- 'L2VPN 5,l2vpn-5,vxlan,456',
- 'L2VPN 6,l2vpn-6,vxlan,444',
- )
-
- cls.csv_update_data = (
- 'id,name,description',
- f'{l2vpns[0].pk},L2VPN 7,New description 7',
- f'{l2vpns[1].pk},L2VPN 8,New description 8',
- )
-
- cls.bulk_edit_data = {
- 'description': 'New Description',
- }
-
- cls.form_data = {
- 'name': 'L2VPN 8',
- 'slug': 'l2vpn-8',
- 'type': L2VPNTypeChoices.TYPE_VXLAN,
- 'identifier': 123,
- 'description': 'Description',
- 'import_targets': [rts[0].pk],
- 'export_targets': [rts[1].pk]
- }
-
-
-class L2VPNTerminationTestCase(
- ViewTestCases.GetObjectViewTestCase,
- ViewTestCases.GetObjectChangelogViewTestCase,
- ViewTestCases.CreateObjectViewTestCase,
- ViewTestCases.EditObjectViewTestCase,
- ViewTestCases.DeleteObjectViewTestCase,
- ViewTestCases.ListObjectsViewTestCase,
- ViewTestCases.BulkImportObjectsViewTestCase,
- ViewTestCases.BulkDeleteObjectsViewTestCase,
-):
-
- model = L2VPNTermination
-
- @classmethod
- def setUpTestData(cls):
- device = create_test_device('Device 1')
- interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
- l2vpns = (
- 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.objects.bulk_create(l2vpns)
-
- vlans = (
- VLAN(name='Vlan 1', vid=1001),
- VLAN(name='Vlan 2', vid=1002),
- VLAN(name='Vlan 3', vid=1003),
- VLAN(name='Vlan 4', vid=1004),
- VLAN(name='Vlan 5', vid=1005),
- VLAN(name='Vlan 6', vid=1006)
- )
- VLAN.objects.bulk_create(vlans)
-
- terminations = (
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
- L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
- )
- L2VPNTermination.objects.bulk_create(terminations)
-
- cls.form_data = {
- 'l2vpn': l2vpns[0].pk,
- 'device': device.pk,
- 'interface': interface.pk,
- }
-
- cls.csv_data = (
- "l2vpn,vlan",
- "L2VPN 1,Vlan 4",
- "L2VPN 1,Vlan 5",
- "L2VPN 1,Vlan 6",
- )
-
- cls.csv_update_data = (
- f"id,l2vpn",
- f"{terminations[0].pk},{l2vpns[0].name}",
- f"{terminations[1].pk},{l2vpns[0].name}",
- f"{terminations[2].pk},{l2vpns[0].name}",
- )
-
- cls.bulk_edit_data = {}
-
- # TODO: Fix L2VPNTerminationImportForm validation to support bulk updates
- def test_bulk_update_objects_with_permission(self):
- pass
-
- #
- # Custom assertions
- #
-
- # TODO: Remove this
- def assertInstanceEqual(self, instance, data, exclude=None, api=False):
- """
- Override parent
- """
- if exclude is None:
- exclude = []
-
- fields = [k for k in data.keys() if k not in exclude]
- model_dict = self.model_to_dict(instance, fields=fields, api=api)
-
- # Omit any dictionary keys which are not instance attributes or have been excluded
- relevant_data = {
- k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
- }
-
- # Handle relations on the model
- for k, v in model_dict.items():
- if isinstance(v, object) and hasattr(v, 'first'):
- model_dict[k] = v.first().pk
-
- self.assertDictEqual(model_dict, relevant_data)
diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py
index 3bfe34b7bc7..61deeff4be2 100644
--- a/netbox/ipam/urls.py
+++ b/netbox/ipam/urls.py
@@ -131,20 +131,4 @@
path('services/edit/', views.ServiceBulkEditView.as_view(), name='service_bulk_edit'),
path('services/delete/', views.ServiceBulkDeleteView.as_view(), name='service_bulk_delete'),
path('services//', include(get_model_urls('ipam', 'service'))),
-
- # L2VPN
- path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
- path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
- path('l2vpns/import/', views.L2VPNBulkImportView.as_view(), name='l2vpn_import'),
- path('l2vpns/edit/', views.L2VPNBulkEditView.as_view(), name='l2vpn_bulk_edit'),
- path('l2vpns/delete/', views.L2VPNBulkDeleteView.as_view(), name='l2vpn_bulk_delete'),
- path('l2vpns//', include(get_model_urls('ipam', 'l2vpn'))),
-
- # L2VPN terminations
- path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
- path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
- path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
- path('l2vpn-terminations/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'),
- path('l2vpn-terminations/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'),
- path('l2vpn-terminations//', include(get_model_urls('ipam', 'l2vpntermination'))),
]
diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py
index 48ea637d909..5c1ac6620b5 100644
--- a/netbox/ipam/views.py
+++ b/netbox/ipam/views.py
@@ -1,5 +1,5 @@
from django.contrib.contenttypes.models import ContentType
-from django.db.models import F, Prefetch
+from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
@@ -9,7 +9,6 @@
from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site
from netbox.views import generic
-from tenancy.views import ObjectContactsView
from utilities.tables import get_table_ordering
from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view
@@ -19,7 +18,6 @@
from .choices import PrefixStatusChoices
from .constants import *
from .models import *
-from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
@@ -1243,112 +1241,3 @@ class ServiceBulkDeleteView(generic.BulkDeleteView):
queryset = Service.objects.prefetch_related('device', 'virtual_machine')
filterset = filtersets.ServiceFilterSet
table = tables.ServiceTable
-
-
-# L2VPN
-
-class L2VPNListView(generic.ObjectListView):
- queryset = L2VPN.objects.all()
- table = L2VPNTable
- filterset = filtersets.L2VPNFilterSet
- filterset_form = forms.L2VPNFilterForm
-
-
-@register_model_view(L2VPN)
-class L2VPNView(generic.ObjectView):
- queryset = L2VPN.objects.all()
-
- def get_extra_context(self, request, instance):
- import_targets_table = tables.RouteTargetTable(
- instance.import_targets.prefetch_related('tenant'),
- orderable=False
- )
- export_targets_table = tables.RouteTargetTable(
- instance.export_targets.prefetch_related('tenant'),
- orderable=False
- )
-
- return {
- 'import_targets_table': import_targets_table,
- 'export_targets_table': export_targets_table,
- }
-
-
-@register_model_view(L2VPN, 'edit')
-class L2VPNEditView(generic.ObjectEditView):
- queryset = L2VPN.objects.all()
- form = forms.L2VPNForm
-
-
-@register_model_view(L2VPN, 'delete')
-class L2VPNDeleteView(generic.ObjectDeleteView):
- queryset = L2VPN.objects.all()
-
-
-class L2VPNBulkImportView(generic.BulkImportView):
- queryset = L2VPN.objects.all()
- model_form = forms.L2VPNImportForm
-
-
-class L2VPNBulkEditView(generic.BulkEditView):
- queryset = L2VPN.objects.all()
- filterset = filtersets.L2VPNFilterSet
- table = tables.L2VPNTable
- form = forms.L2VPNBulkEditForm
-
-
-class L2VPNBulkDeleteView(generic.BulkDeleteView):
- queryset = L2VPN.objects.all()
- filterset = filtersets.L2VPNFilterSet
- table = tables.L2VPNTable
-
-
-@register_model_view(L2VPN, 'contacts')
-class L2VPNContactsView(ObjectContactsView):
- queryset = L2VPN.objects.all()
-
-
-#
-# L2VPN terminations
-#
-
-class L2VPNTerminationListView(generic.ObjectListView):
- queryset = L2VPNTermination.objects.all()
- table = L2VPNTerminationTable
- filterset = filtersets.L2VPNTerminationFilterSet
- filterset_form = forms.L2VPNTerminationFilterForm
-
-
-@register_model_view(L2VPNTermination)
-class L2VPNTerminationView(generic.ObjectView):
- queryset = L2VPNTermination.objects.all()
-
-
-@register_model_view(L2VPNTermination, 'edit')
-class L2VPNTerminationEditView(generic.ObjectEditView):
- queryset = L2VPNTermination.objects.all()
- form = forms.L2VPNTerminationForm
- template_name = 'ipam/l2vpntermination_edit.html'
-
-
-@register_model_view(L2VPNTermination, 'delete')
-class L2VPNTerminationDeleteView(generic.ObjectDeleteView):
- queryset = L2VPNTermination.objects.all()
-
-
-class L2VPNTerminationBulkImportView(generic.BulkImportView):
- queryset = L2VPNTermination.objects.all()
- model_form = forms.L2VPNTerminationImportForm
-
-
-class L2VPNTerminationBulkEditView(generic.BulkEditView):
- queryset = L2VPNTermination.objects.all()
- filterset = filtersets.L2VPNTerminationFilterSet
- table = tables.L2VPNTerminationTable
- form = forms.L2VPNTerminationBulkEditForm
-
-
-class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView):
- queryset = L2VPNTermination.objects.all()
- filterset = filtersets.L2VPNTerminationFilterSet
- table = tables.L2VPNTerminationTable
diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py
index e99b84b10d0..49aee354048 100644
--- a/netbox/netbox/navigation/menu.py
+++ b/netbox/netbox/navigation/menu.py
@@ -209,8 +209,8 @@
MenuGroup(
label=_('L2VPNs'),
items=(
- get_model_item('ipam', 'l2vpn', _('L2VPNs')),
- get_model_item('ipam', 'l2vpntermination', _('Terminations')),
+ get_model_item('vpn', 'l2vpn', _('L2VPNs')),
+ get_model_item('vpn', 'l2vpntermination', _('Terminations')),
),
),
MenuGroup(
diff --git a/netbox/templates/ipam/routetarget.html b/netbox/templates/ipam/routetarget.html
index 497dc8a3916..7894e946f12 100644
--- a/netbox/templates/ipam/routetarget.html
+++ b/netbox/templates/ipam/routetarget.html
@@ -59,7 +59,7 @@
@@ -68,7 +68,7 @@
diff --git a/netbox/templates/ipam/l2vpn.html b/netbox/templates/vpn/l2vpn.html
similarity index 85%
rename from netbox/templates/ipam/l2vpn.html
rename to netbox/templates/vpn/l2vpn.html
index af95aba9fcf..2176a537f1c 100644
--- a/netbox/templates/ipam/l2vpn.html
+++ b/netbox/templates/vpn/l2vpn.html
@@ -34,7 +34,7 @@
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:l2vpn_list' %}
+ {% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpn_list' %}
{% plugin_left_page object %}
@@ -56,12 +56,12 @@
- {% if perms.ipam.add_l2vpntermination %}
+ {% if perms.vpn.add_l2vpntermination %}
diff --git a/netbox/templates/ipam/l2vpntermination.html b/netbox/templates/vpn/l2vpntermination.html
similarity index 96%
rename from netbox/templates/ipam/l2vpntermination.html
rename to netbox/templates/vpn/l2vpntermination.html
index cc316bf39e7..0e753948193 100644
--- a/netbox/templates/ipam/l2vpntermination.html
+++ b/netbox/templates/vpn/l2vpntermination.html
@@ -25,7 +25,7 @@
{% include 'inc/panels/custom_fields.html' %}
- {% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:l2vpntermination_list' %}
+ {% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpntermination_list' %}
diff --git a/netbox/templates/ipam/l2vpntermination_edit.html b/netbox/templates/vpn/l2vpntermination_edit.html
similarity index 100%
rename from netbox/templates/ipam/l2vpntermination_edit.html
rename to netbox/templates/vpn/l2vpntermination_edit.html
diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py
index 95b2152a5ca..7ed36388b51 100644
--- a/netbox/virtualization/api/serializers.py
+++ b/netbox/virtualization/api/serializers.py
@@ -6,15 +6,14 @@
)
from dcim.choices import InterfaceModeChoices
from extras.api.nested_serializers import NestedConfigTemplateSerializer
-from ipam.api.nested_serializers import (
- NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer,
-)
+from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
+from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
from .nested_serializers import *
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py
index 5eb3fea1cb4..ba0c4cc6dd4 100644
--- a/netbox/virtualization/forms/filtersets.py
+++ b/netbox/virtualization/forms/filtersets.py
@@ -4,13 +4,14 @@
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
-from ipam.models import L2VPN, VRF
+from ipam.models import VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES
from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
from virtualization.choices import *
from virtualization.models import *
+from vpn.models import L2VPN
__all__ = (
'ClusterFilterForm',
diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py
index 2126f2541f7..1824aae99e2 100644
--- a/netbox/virtualization/models/virtualmachines.py
+++ b/netbox/virtualization/models/virtualmachines.py
@@ -358,7 +358,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
related_query_name='vminterface',
)
l2vpn_terminations = GenericRelation(
- to='ipam.L2VPNTermination',
+ to='vpn.L2VPNTermination',
content_type_field='assigned_object_type',
object_id_field='assigned_object_id',
related_query_name='vminterface',
diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py
index 1eeb06ea859..632e6878a2b 100644
--- a/netbox/virtualization/tables/virtualmachines.py
+++ b/netbox/virtualization/tables/virtualmachines.py
@@ -24,8 +24,8 @@
{% if perms.ipam.add_ipaddress %}
IP Address
{% endif %}
- {% if perms.ipam.add_l2vpntermination %}
- L2VPN Termination
+ {% if perms.vpn.add_l2vpntermination %}
+ L2VPN Termination
{% endif %}
{% if perms.ipam.add_fhrpgroupassignment %}
Assign FHRP Group
diff --git a/netbox/vpn/api/nested_serializers.py b/netbox/vpn/api/nested_serializers.py
index c9c92d30892..f2627869bc5 100644
--- a/netbox/vpn/api/nested_serializers.py
+++ b/netbox/vpn/api/nested_serializers.py
@@ -9,6 +9,8 @@
'NestedIPSecPolicySerializer',
'NestedIPSecProfileSerializer',
'NestedIPSecProposalSerializer',
+ 'NestedL2VPNSerializer',
+ 'NestedL2VPNTerminationSerializer',
'NestedTunnelSerializer',
'NestedTunnelTerminationSerializer',
)
@@ -82,3 +84,28 @@ class NestedIPSecProfileSerializer(WritableNestedSerializer):
class Meta:
model = models.IPSecProfile
fields = ('id', 'url', 'display', 'name')
+
+
+#
+# L2VPN
+#
+
+class NestedL2VPNSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
+
+ class Meta:
+ model = models.L2VPN
+ fields = [
+ 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type'
+ ]
+
+
+class NestedL2VPNTerminationSerializer(WritableNestedSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
+ l2vpn = NestedL2VPNSerializer()
+
+ class Meta:
+ model = models.L2VPNTermination
+ fields = [
+ 'id', 'url', 'display', 'l2vpn'
+ ]
diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py
index 1a517fe5916..cd464cf2284 100644
--- a/netbox/vpn/api/serializers.py
+++ b/netbox/vpn/api/serializers.py
@@ -2,7 +2,8 @@
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
-from ipam.api.nested_serializers import NestedIPAddressSerializer
+from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
+from ipam.models import RouteTarget
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
@@ -18,6 +19,8 @@
'IPSecPolicySerializer',
'IPSecProfileSerializer',
'IPSecProposalSerializer',
+ 'L2VPNSerializer',
+ 'L2VPNTerminationSerializer',
'TunnelSerializer',
'TunnelTerminationSerializer',
)
@@ -191,3 +194,54 @@ class Meta:
'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
'custom_fields', 'created', 'last_updated',
)
+
+
+#
+# L2VPN
+#
+
+class L2VPNSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
+ type = ChoiceField(choices=L2VPNTypeChoices, required=False)
+ import_targets = SerializedPKRelatedField(
+ queryset=RouteTarget.objects.all(),
+ serializer=NestedRouteTargetSerializer,
+ required=False,
+ many=True
+ )
+ export_targets = SerializedPKRelatedField(
+ queryset=RouteTarget.objects.all(),
+ serializer=NestedRouteTargetSerializer,
+ required=False,
+ many=True
+ )
+ tenant = NestedTenantSerializer(required=False, allow_null=True)
+
+ class Meta:
+ model = L2VPN
+ fields = [
+ 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
+ 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
+ ]
+
+
+class L2VPNTerminationSerializer(NetBoxModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
+ l2vpn = NestedL2VPNSerializer()
+ assigned_object_type = ContentTypeField(
+ queryset=ContentType.objects.all()
+ )
+ assigned_object = serializers.SerializerMethodField(read_only=True)
+
+ class Meta:
+ model = L2VPNTermination
+ fields = [
+ 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
+ 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
+ ]
+
+ @extend_schema_field(serializers.JSONField(allow_null=True))
+ def get_assigned_object(self, instance):
+ serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
+ context = {'request': self.context['request']}
+ return serializer(instance.assigned_object, context=context).data
diff --git a/netbox/vpn/api/urls.py b/netbox/vpn/api/urls.py
index f646174d507..8938532ddd6 100644
--- a/netbox/vpn/api/urls.py
+++ b/netbox/vpn/api/urls.py
@@ -10,6 +10,8 @@
router.register('ipsec-profiles', views.IPSecProfileViewSet)
router.register('tunnels', views.TunnelViewSet)
router.register('tunnel-terminations', views.TunnelTerminationViewSet)
+router.register('l2vpns', views.L2VPNViewSet)
+router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)
app_name = 'vpn-api'
urlpatterns = router.urls
diff --git a/netbox/vpn/api/views.py b/netbox/vpn/api/views.py
index c0ccab7ab74..9a691a171c3 100644
--- a/netbox/vpn/api/views.py
+++ b/netbox/vpn/api/views.py
@@ -12,6 +12,8 @@
'IPSecPolicyViewSet',
'IPSecProfileViewSet',
'IPSecProposalViewSet',
+ 'L2VPNViewSet',
+ 'L2VPNTerminationViewSet',
'TunnelTerminationViewSet',
'TunnelViewSet',
'VPNRootView',
@@ -72,3 +74,15 @@ class IPSecProfileViewSet(NetBoxModelViewSet):
queryset = IPSecProfile.objects.all()
serializer_class = serializers.IPSecProfileSerializer
filterset_class = filtersets.IPSecProfileFilterSet
+
+
+class L2VPNViewSet(NetBoxModelViewSet):
+ queryset = L2VPN.objects.prefetch_related('import_targets', 'export_targets', 'tenant', 'tags')
+ serializer_class = serializers.L2VPNSerializer
+ filterset_class = filtersets.L2VPNFilterSet
+
+
+class L2VPNTerminationViewSet(NetBoxModelViewSet):
+ queryset = L2VPNTermination.objects.prefetch_related('assigned_object')
+ serializer_class = serializers.L2VPNTerminationSerializer
+ filterset_class = filtersets.L2VPNTerminationFilterSet
diff --git a/netbox/vpn/choices.py b/netbox/vpn/choices.py
index a932c5055e8..a272060e918 100644
--- a/netbox/vpn/choices.py
+++ b/netbox/vpn/choices.py
@@ -199,3 +199,56 @@ class DHGroupChoices(ChoiceSet):
(GROUP_33, _('Group {n}').format(n=33)),
(GROUP_34, _('Group {n}').format(n=34)),
)
+
+
+#
+# L2VPN
+#
+
+class L2VPNTypeChoices(ChoiceSet):
+ TYPE_VPLS = 'vpls'
+ TYPE_VPWS = 'vpws'
+ TYPE_EPL = 'epl'
+ TYPE_EVPL = 'evpl'
+ TYPE_EPLAN = 'ep-lan'
+ TYPE_EVPLAN = 'evp-lan'
+ TYPE_EPTREE = 'ep-tree'
+ TYPE_EVPTREE = 'evp-tree'
+ TYPE_VXLAN = 'vxlan'
+ TYPE_VXLAN_EVPN = 'vxlan-evpn'
+ TYPE_MPLS_EVPN = 'mpls-evpn'
+ TYPE_PBB_EVPN = 'pbb-evpn'
+
+ CHOICES = (
+ ('VPLS', (
+ (TYPE_VPWS, 'VPWS'),
+ (TYPE_VPLS, 'VPLS'),
+ )),
+ ('VXLAN', (
+ (TYPE_VXLAN, 'VXLAN'),
+ (TYPE_VXLAN_EVPN, 'VXLAN-EVPN'),
+ )),
+ ('L2VPN E-VPN', (
+ (TYPE_MPLS_EVPN, 'MPLS EVPN'),
+ (TYPE_PBB_EVPN, 'PBB EVPN'),
+ )),
+ ('E-Line', (
+ (TYPE_EPL, 'EPL'),
+ (TYPE_EVPL, 'EVPL'),
+ )),
+ ('E-LAN', (
+ (TYPE_EPLAN, _('Ethernet Private LAN')),
+ (TYPE_EVPLAN, _('Ethernet Virtual Private LAN')),
+ )),
+ ('E-Tree', (
+ (TYPE_EPTREE, _('Ethernet Private Tree')),
+ (TYPE_EVPTREE, _('Ethernet Virtual Private Tree')),
+ )),
+ )
+
+ P2P = (
+ TYPE_VPWS,
+ TYPE_EPL,
+ TYPE_EPLAN,
+ TYPE_EPTREE
+ )
diff --git a/netbox/vpn/constants.py b/netbox/vpn/constants.py
new file mode 100644
index 00000000000..55e398dcd64
--- /dev/null
+++ b/netbox/vpn/constants.py
@@ -0,0 +1,7 @@
+from django.db.models import Q
+
+L2VPN_ASSIGNMENT_MODELS = Q(
+ Q(app_label='dcim', model='interface') |
+ Q(app_label='ipam', model='vlan') |
+ Q(app_label='virtualization', model='vminterface')
+)
diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py
index c0bd140c326..249de9ca2d0 100644
--- a/netbox/vpn/filtersets.py
+++ b/netbox/vpn/filtersets.py
@@ -2,12 +2,12 @@
from django.db.models import Q
from django.utils.translation import gettext as _
-from dcim.models import Interface
-from ipam.models import IPAddress
+from dcim.models import Device, Interface
+from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.filtersets import NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
-from virtualization.models import VMInterface
+from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .models import *
@@ -17,6 +17,8 @@
'IPSecPolicyFilterSet',
'IPSecProfileFilterSet',
'IPSecProposalFilterSet',
+ 'L2VPNFilterSet',
+ 'L2VPNTerminationFilterSet',
'TunnelFilterSet',
'TunnelTerminationFilterSet',
)
@@ -239,3 +241,175 @@ def search(self, queryset, name, value):
Q(description__icontains=value) |
Q(comments__icontains=value)
)
+
+
+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(),
+ label=_('Import target'),
+ )
+ import_target = django_filters.ModelMultipleChoiceFilter(
+ field_name='import_targets__name',
+ queryset=RouteTarget.objects.all(),
+ to_field_name='name',
+ label=_('Import target (name)'),
+ )
+ export_target_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='export_targets',
+ queryset=RouteTarget.objects.all(),
+ label=_('Export target'),
+ )
+ export_target = django_filters.ModelMultipleChoiceFilter(
+ field_name='export_targets__name',
+ queryset=RouteTarget.objects.all(),
+ to_field_name='name',
+ label=_('Export target (name)'),
+ )
+
+ class Meta:
+ model = L2VPN
+ fields = ['id', 'identifier', 'name', 'slug', 'type', 'description']
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ qs_filter = Q(name__icontains=value) | Q(description__icontains=value)
+ try:
+ qs_filter |= Q(identifier=int(value))
+ except ValueError:
+ pass
+ return queryset.filter(qs_filter)
+
+
+class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
+ l2vpn_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=L2VPN.objects.all(),
+ label=_('L2VPN (ID)'),
+ )
+ l2vpn = django_filters.ModelMultipleChoiceFilter(
+ field_name='l2vpn__slug',
+ queryset=L2VPN.objects.all(),
+ to_field_name='slug',
+ label=_('L2VPN (slug)'),
+ )
+ region = MultiValueCharFilter(
+ method='filter_region',
+ field_name='slug',
+ label=_('Region (slug)'),
+ )
+ region_id = MultiValueNumberFilter(
+ method='filter_region',
+ field_name='pk',
+ label=_('Region (ID)'),
+ )
+ site = MultiValueCharFilter(
+ method='filter_site',
+ field_name='slug',
+ label=_('Site (slug)'),
+ )
+ site_id = MultiValueNumberFilter(
+ method='filter_site',
+ field_name='pk',
+ label=_('Site (ID)'),
+ )
+ device = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface__device__name',
+ queryset=Device.objects.all(),
+ to_field_name='name',
+ label=_('Device (name)'),
+ )
+ device_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='interface__device',
+ queryset=Device.objects.all(),
+ label=_('Device (ID)'),
+ )
+ virtual_machine = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface__virtual_machine__name',
+ queryset=VirtualMachine.objects.all(),
+ to_field_name='name',
+ label=_('Virtual machine (name)'),
+ )
+ virtual_machine_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface__virtual_machine',
+ queryset=VirtualMachine.objects.all(),
+ label=_('Virtual machine (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 = django_filters.ModelMultipleChoiceFilter(
+ field_name='vminterface__name',
+ queryset=VMInterface.objects.all(),
+ to_field_name='name',
+ label=_('VM interface (name)'),
+ )
+ 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(),
+ to_field_name='name',
+ label=_('VLAN (name)'),
+ )
+ vlan_vid = django_filters.NumberFilter(
+ field_name='vlan__vid',
+ label=_('VLAN number (1-4094)'),
+ )
+ vlan_id = django_filters.ModelMultipleChoiceFilter(
+ field_name='vlan',
+ queryset=VLAN.objects.all(),
+ label=_('VLAN (ID)'),
+ )
+ assigned_object_type = ContentTypeFilter()
+
+ class Meta:
+ model = L2VPNTermination
+ fields = ('id', 'assigned_object_type_id')
+
+ def search(self, queryset, name, value):
+ if not value.strip():
+ return queryset
+ qs_filter = Q(l2vpn__name__icontains=value)
+ return queryset.filter(qs_filter)
+
+ def filter_assigned_object(self, queryset, name, value):
+ qs = queryset.filter(
+ Q(**{'{}__in'.format(name): value})
+ )
+ return qs
+
+ def filter_site(self, queryset, name, value):
+ qs = queryset.filter(
+ Q(
+ Q(**{'vlan__site__{}__in'.format(name): value}) |
+ Q(**{'interface__device__site__{}__in'.format(name): value}) |
+ Q(**{'vminterface__virtual_machine__site__{}__in'.format(name): value})
+ )
+ )
+ return qs
+
+ def filter_region(self, queryset, name, value):
+ qs = queryset.filter(
+ Q(
+ Q(**{'vlan__site__region__{}__in'.format(name): value}) |
+ Q(**{'interface__device__site__region__{}__in'.format(name): value}) |
+ Q(**{'vminterface__virtual_machine__site__region__{}__in'.format(name): value})
+ )
+ )
+ return qs
diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py
index a7b097b5c94..4cbfd950d85 100644
--- a/netbox/vpn/forms/bulk_edit.py
+++ b/netbox/vpn/forms/bulk_edit.py
@@ -14,6 +14,8 @@
'IPSecPolicyBulkEditForm',
'IPSecProfileBulkEditForm',
'IPSecProposalBulkEditForm',
+ 'L2VPNBulkEditForm',
+ 'L2VPNTerminationBulkEditForm',
'TunnelBulkEditForm',
'TunnelTerminationBulkEditForm',
)
@@ -241,3 +243,32 @@ class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = (
'description', 'comments',
)
+
+
+class L2VPNBulkEditForm(NetBoxModelBulkEditForm):
+ type = forms.ChoiceField(
+ label=_('Type'),
+ choices=add_blank_choice(L2VPNTypeChoices),
+ required=False
+ )
+ tenant = DynamicModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False
+ )
+ description = forms.CharField(
+ label=_('Description'),
+ max_length=200,
+ required=False
+ )
+ comments = CommentField()
+
+ model = L2VPN
+ fieldsets = (
+ (None, ('type', 'tenant', 'description')),
+ )
+ nullable_fields = ('tenant', 'description', 'comments')
+
+
+class L2VPNTerminationBulkEditForm(NetBoxModelBulkEditForm):
+ model = L2VPN
diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py
index 5b42cc761e4..33e93d28fb3 100644
--- a/netbox/vpn/forms/bulk_import.py
+++ b/netbox/vpn/forms/bulk_import.py
@@ -1,7 +1,8 @@
+from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
-from ipam.models import IPAddress
+from ipam.models import IPAddress, VLAN
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
@@ -15,6 +16,8 @@
'IPSecPolicyImportForm',
'IPSecProfileImportForm',
'IPSecProposalImportForm',
+ 'L2VPNImportForm',
+ 'L2VPNTerminationImportForm',
'TunnelImportForm',
'TunnelTerminationImportForm',
)
@@ -228,3 +231,92 @@ class Meta:
fields = (
'name', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
)
+
+
+class L2VPNImportForm(NetBoxModelImportForm):
+ tenant = CSVModelChoiceField(
+ label=_('Tenant'),
+ queryset=Tenant.objects.all(),
+ required=False,
+ to_field_name='name',
+ )
+ type = CSVChoiceField(
+ label=_('Type'),
+ choices=L2VPNTypeChoices,
+ help_text=_('L2VPN type')
+ )
+
+ class Meta:
+ model = L2VPN
+ fields = ('identifier', 'name', 'slug', 'tenant', 'type', 'description',
+ 'comments', 'tags')
+
+
+class L2VPNTerminationImportForm(NetBoxModelImportForm):
+ l2vpn = CSVModelChoiceField(
+ queryset=L2VPN.objects.all(),
+ required=True,
+ to_field_name='name',
+ label=_('L2VPN'),
+ )
+ device = CSVModelChoiceField(
+ label=_('Device'),
+ queryset=Device.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Parent device (for interface)')
+ )
+ virtual_machine = CSVModelChoiceField(
+ label=_('Virtual machine'),
+ queryset=VirtualMachine.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Parent virtual machine (for interface)')
+ )
+ interface = CSVModelChoiceField(
+ label=_('Interface'),
+ queryset=Interface.objects.none(), # Can also refer to VMInterface
+ required=False,
+ to_field_name='name',
+ help_text=_('Assigned interface (device or VM)')
+ )
+ vlan = CSVModelChoiceField(
+ label=_('VLAN'),
+ queryset=VLAN.objects.all(),
+ required=False,
+ to_field_name='name',
+ help_text=_('Assigned VLAN')
+ )
+
+ class Meta:
+ model = L2VPNTermination
+ fields = ('l2vpn', 'device', 'virtual_machine', 'interface', 'vlan', 'tags')
+
+ def __init__(self, data=None, *args, **kwargs):
+ super().__init__(data, *args, **kwargs)
+
+ if data:
+
+ # Limit interface queryset by device or VM
+ if data.get('device'):
+ self.fields['interface'].queryset = Interface.objects.filter(
+ **{f"device__{self.fields['device'].to_field_name}": data['device']}
+ )
+ elif data.get('virtual_machine'):
+ self.fields['interface'].queryset = VMInterface.objects.filter(
+ **{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
+ )
+
+ def clean(self):
+ super().clean()
+
+ if self.cleaned_data.get('device') and self.cleaned_data.get('virtual_machine'):
+ raise ValidationError(_('Cannot import device and VM interface terminations simultaneously.'))
+ if not self.instance and not (self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')):
+ raise ValidationError(_('Each termination must specify either an interface or a VLAN.'))
+ if self.cleaned_data.get('interface') and self.cleaned_data.get('vlan'):
+ raise ValidationError(_('Cannot assign both an interface and a VLAN.'))
+
+ # if this is an update we might not have interface or vlan in the form data
+ if self.cleaned_data.get('interface') or self.cleaned_data.get('vlan'):
+ self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vlan')
diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py
index ec146919a70..91ca8a8dcd1 100644
--- a/netbox/vpn/forms/filtersets.py
+++ b/netbox/vpn/forms/filtersets.py
@@ -1,10 +1,18 @@
from django import forms
+from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
+from dcim.models import Device, Region, Site
+from ipam.models import RouteTarget, VLAN
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import TenancyFilterForm
-from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField
+from utilities.forms.fields import (
+ ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
+)
+from utilities.forms.utils import add_blank_choice
+from virtualization.models import VirtualMachine
from vpn.choices import *
+from vpn.constants import L2VPN_ASSIGNMENT_MODELS
from vpn.models import *
__all__ = (
@@ -13,6 +21,8 @@
'IPSecPolicyFilterForm',
'IPSecProfileFilterForm',
'IPSecProposalFilterForm',
+ 'L2VPNFilterForm',
+ 'L2VPNTerminationFilterForm',
'TunnelFilterForm',
'TunnelTerminationFilterForm',
)
@@ -180,3 +190,90 @@ class IPSecProfileFilterForm(NetBoxModelFilterSetForm):
label=_('IPSec policy')
)
tag = TagFilterField(model)
+
+
+class L2VPNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
+ model = L2VPN
+ fieldsets = (
+ (None, ('q', 'filter_id', 'tag')),
+ (_('Attributes'), ('type', 'import_target_id', 'export_target_id')),
+ (_('Tenant'), ('tenant_group_id', 'tenant_id')),
+ )
+ type = forms.ChoiceField(
+ label=_('Type'),
+ choices=add_blank_choice(L2VPNTypeChoices),
+ required=False
+ )
+ 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, ('filter_id', 'l2vpn_id',)),
+ (_('Assigned Object'), (
+ 'assigned_object_type_id', 'region_id', 'site_id', 'device_id', 'virtual_machine_id', 'vlan_id',
+ )),
+ )
+ l2vpn_id = DynamicModelChoiceField(
+ queryset=L2VPN.objects.all(),
+ required=False,
+ label=_('L2VPN')
+ )
+ assigned_object_type_id = ContentTypeMultipleChoiceField(
+ queryset=ContentType.objects.filter(L2VPN_ASSIGNMENT_MODELS),
+ required=False,
+ label=_('Assigned Object Type'),
+ limit_choices_to=L2VPN_ASSIGNMENT_MODELS
+ )
+ region_id = DynamicModelMultipleChoiceField(
+ queryset=Region.objects.all(),
+ required=False,
+ label=_('Region')
+ )
+ site_id = DynamicModelMultipleChoiceField(
+ queryset=Site.objects.all(),
+ required=False,
+ null_option='None',
+ query_params={
+ 'region_id': '$region_id'
+ },
+ label=_('Site')
+ )
+ device_id = DynamicModelMultipleChoiceField(
+ queryset=Device.objects.all(),
+ required=False,
+ null_option='None',
+ query_params={
+ 'site_id': '$site_id'
+ },
+ label=_('Device')
+ )
+ vlan_id = DynamicModelMultipleChoiceField(
+ queryset=VLAN.objects.all(),
+ required=False,
+ null_option='None',
+ query_params={
+ 'site_id': '$site_id'
+ },
+ label=_('VLAN')
+ )
+ virtual_machine_id = DynamicModelMultipleChoiceField(
+ queryset=VirtualMachine.objects.all(),
+ required=False,
+ null_option='None',
+ query_params={
+ 'site_id': '$site_id'
+ },
+ label=_('Virtual Machine')
+ )
diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py
index 35fa2cad3ae..e61993ddd6c 100644
--- a/netbox/vpn/forms/model_forms.py
+++ b/netbox/vpn/forms/model_forms.py
@@ -1,11 +1,12 @@
from django import forms
+from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from dcim.models import Device, Interface
-from ipam.models import IPAddress
+from ipam.models import IPAddress, RouteTarget, VLAN
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
-from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField
from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import HTMXSelect
from virtualization.models import VirtualMachine, VMInterface
@@ -18,6 +19,8 @@
'IPSecPolicyForm',
'IPSecProfileForm',
'IPSecProposalForm',
+ 'L2VPNForm',
+ 'L2VPNTerminationForm',
'TunnelCreateForm',
'TunnelForm',
'TunnelTerminationForm',
@@ -355,3 +358,96 @@ class Meta:
fields = [
'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'description', 'comments', 'tags',
]
+
+
+#
+# L2VPN
+#
+
+class L2VPNForm(TenancyForm, NetBoxModelForm):
+ slug = SlugField()
+ import_targets = DynamicModelMultipleChoiceField(
+ label=_('Import targets'),
+ queryset=RouteTarget.objects.all(),
+ required=False
+ )
+ export_targets = DynamicModelMultipleChoiceField(
+ label=_('Export targets'),
+ queryset=RouteTarget.objects.all(),
+ required=False
+ )
+ comments = CommentField()
+
+ fieldsets = (
+ (_('L2VPN'), ('name', 'slug', 'type', 'identifier', 'description', 'tags')),
+ (_('Route Targets'), ('import_targets', 'export_targets')),
+ (_('Tenancy'), ('tenant_group', 'tenant')),
+ )
+
+ class Meta:
+ model = L2VPN
+ fields = (
+ 'name', 'slug', 'type', 'identifier', 'import_targets', 'export_targets', 'tenant', 'description',
+ 'comments', 'tags'
+ )
+
+
+class L2VPNTerminationForm(NetBoxModelForm):
+ l2vpn = DynamicModelChoiceField(
+ queryset=L2VPN.objects.all(),
+ required=True,
+ query_params={},
+ label=_('L2VPN'),
+ fetch_trigger='open'
+ )
+ vlan = DynamicModelChoiceField(
+ queryset=VLAN.objects.all(),
+ required=False,
+ selector=True,
+ label=_('VLAN')
+ )
+ interface = DynamicModelChoiceField(
+ label=_('Interface'),
+ queryset=Interface.objects.all(),
+ required=False,
+ selector=True
+ )
+ vminterface = DynamicModelChoiceField(
+ queryset=VMInterface.objects.all(),
+ required=False,
+ selector=True,
+ label=_('Interface')
+ )
+
+ class Meta:
+ model = L2VPNTermination
+ fields = ('l2vpn', )
+
+ def __init__(self, *args, **kwargs):
+ instance = kwargs.get('instance')
+ initial = kwargs.get('initial', {}).copy()
+
+ if instance:
+ if type(instance.assigned_object) is Interface:
+ initial['interface'] = instance.assigned_object
+ elif type(instance.assigned_object) is VLAN:
+ initial['vlan'] = instance.assigned_object
+ elif type(instance.assigned_object) is VMInterface:
+ initial['vminterface'] = instance.assigned_object
+ kwargs['initial'] = initial
+
+ super().__init__(*args, **kwargs)
+
+ def clean(self):
+ super().clean()
+
+ interface = self.cleaned_data.get('interface')
+ vminterface = self.cleaned_data.get('vminterface')
+ vlan = self.cleaned_data.get('vlan')
+
+ if not (interface or vminterface or vlan):
+ raise ValidationError(_('A termination must specify an interface or VLAN.'))
+ if len([x for x in (interface, vminterface, vlan) if x]) > 1:
+ raise ValidationError(_('A termination can only have one terminating object (an interface or VLAN).'))
+
+ self.instance.assigned_object = interface or vminterface or vlan
diff --git a/netbox/vpn/graphql/gfk_mixins.py b/netbox/vpn/graphql/gfk_mixins.py
new file mode 100644
index 00000000000..72272f7adf5
--- /dev/null
+++ b/netbox/vpn/graphql/gfk_mixins.py
@@ -0,0 +1,30 @@
+import graphene
+
+from dcim.graphql.types import InterfaceType
+from dcim.models import Interface
+from ipam.graphql.types import VLANType
+from ipam.models import VLAN
+from virtualization.graphql.types import VMInterfaceType
+from virtualization.models import VMInterface
+
+__all__ = (
+ 'L2VPNAssignmentType',
+)
+
+
+class L2VPNAssignmentType(graphene.Union):
+ class Meta:
+ types = (
+ InterfaceType,
+ VLANType,
+ VMInterfaceType,
+ )
+
+ @classmethod
+ def resolve_type(cls, instance, info):
+ if type(instance) is Interface:
+ return InterfaceType
+ if type(instance) is VLAN:
+ return VLANType
+ if type(instance) is VMInterface:
+ return VMInterfaceType
diff --git a/netbox/vpn/graphql/schema.py b/netbox/vpn/graphql/schema.py
index 64e6808823d..9c8e1e50277 100644
--- a/netbox/vpn/graphql/schema.py
+++ b/netbox/vpn/graphql/schema.py
@@ -38,6 +38,18 @@ def resolve_ipsec_profile_list(root, info, **kwargs):
def resolve_ipsec_proposal_list(root, info, **kwargs):
return gql_query_optimizer(models.IPSecProposal.objects.all(), info)
+ l2vpn = ObjectField(L2VPNType)
+ l2vpn_list = ObjectListField(L2VPNType)
+
+ def resolve_l2vpn_list(root, info, **kwargs):
+ return gql_query_optimizer(models.L2VPN.objects.all(), info)
+
+ l2vpn_termination = ObjectField(L2VPNTerminationType)
+ l2vpn_termination_list = ObjectListField(L2VPNTerminationType)
+
+ def resolve_l2vpn_termination_list(root, info, **kwargs):
+ return gql_query_optimizer(models.L2VPNTermination.objects.all(), info)
+
tunnel = ObjectField(TunnelType)
tunnel_list = ObjectListField(TunnelType)
diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py
index f46e8b69702..840a44c7bf4 100644
--- a/netbox/vpn/graphql/types.py
+++ b/netbox/vpn/graphql/types.py
@@ -1,4 +1,6 @@
-from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
+import graphene
+
+from extras.graphql.mixins import ContactsMixin, CustomFieldsMixin, TagsMixin
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
from vpn import filtersets, models
@@ -8,6 +10,8 @@
'IPSecPolicyType',
'IPSecProfileType',
'IPSecProposalType',
+ 'L2VPNType',
+ 'L2VPNTerminationType',
'TunnelTerminationType',
'TunnelType',
)
@@ -67,3 +71,19 @@ class Meta:
model = models.IPSecProfile
fields = '__all__'
filterset_class = filtersets.IPSecProfileFilterSet
+
+
+class L2VPNType(ContactsMixin, NetBoxObjectType):
+ class Meta:
+ model = models.L2VPN
+ fields = '__all__'
+ filtersets_class = filtersets.L2VPNFilterSet
+
+
+class L2VPNTerminationType(NetBoxObjectType):
+ assigned_object = graphene.Field('vpn.graphql.gfk_mixins.L2VPNAssignmentType')
+
+ class Meta:
+ model = models.L2VPNTermination
+ exclude = ('assigned_object_type', 'assigned_object_id')
+ filtersets_class = filtersets.L2VPNTerminationFilterSet
diff --git a/netbox/vpn/migrations/0002_move_l2vpn.py b/netbox/vpn/migrations/0002_move_l2vpn.py
new file mode 100644
index 00000000000..3ec49f8302e
--- /dev/null
+++ b/netbox/vpn/migrations/0002_move_l2vpn.py
@@ -0,0 +1,73 @@
+from django.db import migrations, models
+import django.db.models.deletion
+import taggit.managers
+import utilities.json
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0099_cachedvalue_ordering'),
+ ('contenttypes', '0002_remove_content_type_name'),
+ ('tenancy', '0012_contactassignment_custom_fields'),
+ ('ipam', '0068_move_l2vpn'),
+ ('vpn', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.SeparateDatabaseAndState(
+ state_operations=[
+ migrations.CreateModel(
+ name='L2VPN',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
+ ('description', models.CharField(blank=True, max_length=200)),
+ ('comments', models.TextField(blank=True)),
+ ('name', models.CharField(max_length=100, unique=True)),
+ ('slug', models.SlugField(max_length=100, unique=True)),
+ ('type', models.CharField(max_length=50)),
+ ('identifier', models.BigIntegerField(blank=True, null=True)),
+ ('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')),
+ ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='l2vpns', to='tenancy.tenant')),
+ ],
+ options={
+ 'verbose_name': 'L2VPN',
+ 'verbose_name_plural': 'L2VPNs',
+ 'ordering': ('name', 'identifier'),
+ },
+ ),
+ migrations.CreateModel(
+ name='L2VPNTermination',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
+ ('created', models.DateTimeField(auto_now_add=True, null=True)),
+ ('last_updated', models.DateTimeField(auto_now=True, null=True)),
+ ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
+ ('assigned_object_id', models.PositiveBigIntegerField()),
+ ('assigned_object_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'dcim'), ('model', 'interface')), models.Q(('app_label', 'ipam'), ('model', 'vlan')), models.Q(('app_label', 'virtualization'), ('model', 'vminterface')), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
+ ('l2vpn', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='vpn.l2vpn')),
+ ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
+ ],
+ options={
+ 'verbose_name': 'L2VPN termination',
+ 'verbose_name_plural': 'L2VPN terminations',
+ 'ordering': ('l2vpn',),
+ },
+ ),
+ ],
+ # Tables have been renamed from ipam
+ database_operations=[],
+ ),
+ migrations.AddConstraint(
+ model_name='l2vpntermination',
+ constraint=models.UniqueConstraint(
+ fields=('assigned_object_type', 'assigned_object_id'),
+ name='vpn_l2vpntermination_assigned_object'
+ ),
+ ),
+ ]
diff --git a/netbox/vpn/models/__init__.py b/netbox/vpn/models/__init__.py
index 3b70eb41839..2e76b980b77 100644
--- a/netbox/vpn/models/__init__.py
+++ b/netbox/vpn/models/__init__.py
@@ -1,2 +1,3 @@
from .crypto import *
+from .l2vpn import *
from .tunnels import *
diff --git a/netbox/ipam/models/l2vpn.py b/netbox/vpn/models/l2vpn.py
similarity index 93%
rename from netbox/ipam/models/l2vpn.py
rename to netbox/vpn/models/l2vpn.py
index a2742a8f3ab..f1a14228314 100644
--- a/netbox/ipam/models/l2vpn.py
+++ b/netbox/vpn/models/l2vpn.py
@@ -6,10 +6,10 @@
from django.utils.translation import gettext_lazy as _
from core.models import ContentType
-from ipam.choices import L2VPNTypeChoices
-from ipam.constants import L2VPN_ASSIGNMENT_MODELS
from netbox.models import NetBoxModel, PrimaryModel
from netbox.models.features import ContactsMixin
+from vpn.choices import L2VPNTypeChoices
+from vpn.constants import L2VPN_ASSIGNMENT_MODELS
__all__ = (
'L2VPN',
@@ -69,7 +69,7 @@ def __str__(self):
return f'{self.name}'
def get_absolute_url(self):
- return reverse('ipam:l2vpn', args=[self.pk])
+ return reverse('vpn:l2vpn', args=[self.pk])
@cached_property
def can_add_termination(self):
@@ -81,7 +81,7 @@ def can_add_termination(self):
class L2VPNTermination(NetBoxModel):
l2vpn = models.ForeignKey(
- to='ipam.L2VPN',
+ to='vpn.L2VPN',
on_delete=models.CASCADE,
related_name='terminations'
)
@@ -99,7 +99,7 @@ class L2VPNTermination(NetBoxModel):
clone_fields = ('l2vpn',)
prerequisite_models = (
- 'ipam.L2VPN',
+ 'vpn.L2VPN',
)
class Meta:
@@ -107,7 +107,7 @@ class Meta:
constraints = (
models.UniqueConstraint(
fields=('assigned_object_type', 'assigned_object_id'),
- name='ipam_l2vpntermination_assigned_object'
+ name='vpn_l2vpntermination_assigned_object'
),
)
verbose_name = _('L2VPN termination')
@@ -119,7 +119,7 @@ def __str__(self):
return super().__str__()
def get_absolute_url(self):
- return reverse('ipam:l2vpntermination', args=[self.pk])
+ return reverse('vpn:l2vpntermination', args=[self.pk])
def clean(self):
# Only check is assigned_object is set. Required otherwise we have an Integrity Error thrown.
diff --git a/netbox/vpn/search.py b/netbox/vpn/search.py
index 70b0c644f52..d0b2ad0c6c4 100644
--- a/netbox/vpn/search.py
+++ b/netbox/vpn/search.py
@@ -63,3 +63,15 @@ class IPSecProfileIndex(SearchIndex):
('comments', 5000),
)
display_attrs = ('description',)
+
+
+@register_search
+class L2VPNIndex(SearchIndex):
+ model = models.L2VPN
+ fields = (
+ ('name', 100),
+ ('slug', 110),
+ ('description', 500),
+ ('comments', 5000),
+ )
+ display_attrs = ('type', 'identifier', 'tenant', 'description')
diff --git a/netbox/vpn/tables/__init__.py b/netbox/vpn/tables/__init__.py
new file mode 100644
index 00000000000..2e76b980b77
--- /dev/null
+++ b/netbox/vpn/tables/__init__.py
@@ -0,0 +1,3 @@
+from .crypto import *
+from .l2vpn import *
+from .tunnels import *
diff --git a/netbox/vpn/tables.py b/netbox/vpn/tables/crypto.py
similarity index 65%
rename from netbox/vpn/tables.py
rename to netbox/vpn/tables/crypto.py
index 304467586e4..cd6d3c24df6 100644
--- a/netbox/vpn/tables.py
+++ b/netbox/vpn/tables/crypto.py
@@ -1,8 +1,6 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
-from django_tables2.utils import Accessor
-from tenancy.tables import TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
from vpn.models import *
@@ -12,88 +10,9 @@
'IPSecPolicyTable',
'IPSecProposalTable',
'IPSecProfileTable',
- 'TunnelTable',
- 'TunnelTerminationTable',
)
-class TunnelTable(TenancyColumnsMixin, NetBoxTable):
- name = tables.Column(
- verbose_name=_('Name'),
- linkify=True
- )
- status = columns.ChoiceFieldColumn(
- verbose_name=_('Status')
- )
- ipsec_profile = tables.Column(
- verbose_name=_('IPSec profile'),
- linkify=True
- )
- terminations_count = columns.LinkedCountColumn(
- accessor=Accessor('count_terminations'),
- viewname='vpn:tunneltermination_list',
- url_params={'tunnel_id': 'pk'},
- verbose_name=_('Terminations')
- )
- comments = columns.MarkdownColumn(
- verbose_name=_('Comments'),
- )
- tags = columns.TagColumn(
- url_name='vpn:tunnel_list'
- )
-
- class Meta(NetBoxTable.Meta):
- model = Tunnel
- fields = (
- 'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'tunnel_id',
- 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
- )
- default_columns = ('pk', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'terminations_count')
-
-
-class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
- tunnel = tables.Column(
- verbose_name=_('Tunnel'),
- linkify=True
- )
- role = columns.ChoiceFieldColumn(
- verbose_name=_('Role')
- )
- termination_parent = tables.Column(
- accessor='termination__parent_object',
- linkify=True,
- orderable=False,
- verbose_name=_('Host')
- )
- termination = tables.Column(
- verbose_name=_('Termination'),
- linkify=True
- )
- ip_addresses = tables.ManyToManyColumn(
- accessor=tables.A('termination__ip_addresses'),
- orderable=False,
- linkify_item=True,
- verbose_name=_('IP Addresses')
- )
- outside_ip = tables.Column(
- verbose_name=_('Outside IP'),
- linkify=True
- )
- tags = columns.TagColumn(
- url_name='vpn:tunneltermination_list'
- )
-
- class Meta(NetBoxTable.Meta):
- model = TunnelTermination
- fields = (
- 'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip', 'tags',
- 'created', 'last_updated',
- )
- default_columns = (
- 'pk', 'id', 'tunnel', 'role', 'termination_parent', 'termination', 'ip_addresses', 'outside_ip',
- )
-
-
class IKEProposalTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
diff --git a/netbox/ipam/tables/l2vpn.py b/netbox/vpn/tables/l2vpn.py
similarity index 96%
rename from netbox/ipam/tables/l2vpn.py
rename to netbox/vpn/tables/l2vpn.py
index 8635ab62a75..1f8b2c0d7ab 100644
--- a/netbox/ipam/tables/l2vpn.py
+++ b/netbox/vpn/tables/l2vpn.py
@@ -1,9 +1,9 @@
-from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
-from ipam.models import L2VPN, L2VPNTermination
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
+from vpn.models import L2VPN, L2VPNTermination
__all__ = (
'L2VPNTable',
@@ -37,7 +37,7 @@ class L2VPNTable(TenancyColumnsMixin, NetBoxTable):
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
- url_name='ipam:l2vpn_list'
+ url_name='vpn:l2vpn_list'
)
class Meta(NetBoxTable.Meta):
diff --git a/netbox/vpn/tables/tunnels.py b/netbox/vpn/tables/tunnels.py
new file mode 100644
index 00000000000..4023607ffdc
--- /dev/null
+++ b/netbox/vpn/tables/tunnels.py
@@ -0,0 +1,87 @@
+import django_tables2 as tables
+from django.utils.translation import gettext_lazy as _
+from django_tables2.utils import Accessor
+
+from netbox.tables import NetBoxTable, columns
+from tenancy.tables import TenancyColumnsMixin
+from vpn.models import *
+
+__all__ = (
+ 'TunnelTable',
+ 'TunnelTerminationTable',
+)
+
+
+class TunnelTable(TenancyColumnsMixin, NetBoxTable):
+ name = tables.Column(
+ verbose_name=_('Name'),
+ linkify=True
+ )
+ status = columns.ChoiceFieldColumn(
+ verbose_name=_('Status')
+ )
+ ipsec_profile = tables.Column(
+ verbose_name=_('IPSec profile'),
+ linkify=True
+ )
+ terminations_count = columns.LinkedCountColumn(
+ accessor=Accessor('count_terminations'),
+ viewname='vpn:tunneltermination_list',
+ url_params={'tunnel_id': 'pk'},
+ verbose_name=_('Terminations')
+ )
+ comments = columns.MarkdownColumn(
+ verbose_name=_('Comments'),
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:tunnel_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = Tunnel
+ fields = (
+ 'pk', 'id', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'tenant_group', 'tunnel_id',
+ 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated',
+ )
+ default_columns = ('pk', 'name', 'status', 'encapsulation', 'tenant', 'terminations_count')
+
+
+class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
+ tunnel = tables.Column(
+ verbose_name=_('Tunnel'),
+ linkify=True
+ )
+ role = columns.ChoiceFieldColumn(
+ verbose_name=_('Role')
+ )
+ interface_parent = tables.Column(
+ accessor='interface__parent_object',
+ linkify=True,
+ orderable=False,
+ verbose_name=_('Host')
+ )
+ interface = tables.Column(
+ verbose_name=_('Interface'),
+ linkify=True
+ )
+ ip_addresses = tables.ManyToManyColumn(
+ accessor=tables.A('interface__ip_addresses'),
+ orderable=False,
+ linkify_item=True,
+ verbose_name=_('IP Addresses')
+ )
+ outside_ip = tables.Column(
+ verbose_name=_('Outside IP'),
+ linkify=True
+ )
+ tags = columns.TagColumn(
+ url_name='vpn:tunneltermination_list'
+ )
+
+ class Meta(NetBoxTable.Meta):
+ model = TunnelTermination
+ fields = (
+ 'pk', 'id', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip', 'tags',
+ 'created', 'last_updated',
+ )
+ default_columns = ('pk', 'id', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip')
diff --git a/netbox/vpn/tests/test_api.py b/netbox/vpn/tests/test_api.py
index 9bfa297ab45..2714bd4fcf4 100644
--- a/netbox/vpn/tests/test_api.py
+++ b/netbox/vpn/tests/test_api.py
@@ -2,6 +2,7 @@
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
+from ipam.models import VLAN
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
from vpn.choices import *
from vpn.models import *
@@ -471,3 +472,96 @@ def setUpTestData(cls):
'ipsec_policy': ipsec_policies[1].pk,
'description': 'New description',
}
+
+
+class L2VPNTest(APIViewTestCases.APIViewTestCase):
+ model = L2VPN
+ brief_fields = ['display', 'id', 'identifier', 'name', 'slug', 'type', 'url']
+ create_data = [
+ {
+ 'name': 'L2VPN 4',
+ 'slug': 'l2vpn-4',
+ 'type': 'vxlan',
+ 'identifier': 33343344
+ },
+ {
+ 'name': 'L2VPN 5',
+ 'slug': 'l2vpn-5',
+ 'type': 'vxlan',
+ 'identifier': 33343345
+ },
+ {
+ 'name': 'L2VPN 6',
+ 'slug': 'l2vpn-6',
+ 'type': 'vpws',
+ 'identifier': 33343346
+ },
+ ]
+ bulk_update_data = {
+ 'description': 'New description',
+ }
+
+ @classmethod
+ def setUpTestData(cls):
+
+ l2vpns = (
+ L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
+ L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
+ L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
+ )
+ L2VPN.objects.bulk_create(l2vpns)
+
+
+class L2VPNTerminationTest(APIViewTestCases.APIViewTestCase):
+ model = L2VPNTermination
+ brief_fields = ['display', 'id', 'l2vpn', 'url']
+
+ @classmethod
+ def setUpTestData(cls):
+
+ 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),
+ VLAN(name='VLAN 6', vid=656),
+ VLAN(name='VLAN 7', vid=657)
+ )
+ VLAN.objects.bulk_create(vlans)
+
+ l2vpns = (
+ L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
+ L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
+ L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
+ )
+ L2VPN.objects.bulk_create(l2vpns)
+
+ l2vpnterminations = (
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
+ )
+ L2VPNTermination.objects.bulk_create(l2vpnterminations)
+
+ cls.create_data = [
+ {
+ 'l2vpn': l2vpns[0].pk,
+ 'assigned_object_type': 'ipam.vlan',
+ 'assigned_object_id': vlans[3].pk,
+ },
+ {
+ 'l2vpn': l2vpns[0].pk,
+ 'assigned_object_type': 'ipam.vlan',
+ 'assigned_object_id': vlans[4].pk,
+ },
+ {
+ 'l2vpn': l2vpns[0].pk,
+ 'assigned_object_type': 'ipam.vlan',
+ 'assigned_object_id': vlans[5].pk,
+ },
+ ]
+
+ cls.bulk_update_data = {
+ 'l2vpn': l2vpns[2].pk
+ }
diff --git a/netbox/vpn/tests/test_filtersets.py b/netbox/vpn/tests/test_filtersets.py
index 966717f4a99..a9eeb120338 100644
--- a/netbox/vpn/tests/test_filtersets.py
+++ b/netbox/vpn/tests/test_filtersets.py
@@ -1,13 +1,14 @@
+from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from dcim.choices import InterfaceTypeChoices
-from dcim.models import Interface
-from ipam.models import IPAddress
-from virtualization.models import VMInterface
+from dcim.models import Device, Interface, Site
+from ipam.models import IPAddress, VLAN, RouteTarget
+from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
+from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import *
from vpn.filtersets import *
from vpn.models import *
-from utilities.testing import ChangeLoggedFilterSetTests, create_test_device, create_test_virtualmachine
class TunnelTestCase(TestCase, ChangeLoggedFilterSetTests):
@@ -590,3 +591,163 @@ def test_ipsec_policy(self):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'ipsec_policy': [ipsec_policies[0].name, ipsec_policies[1].name]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+
+class L2VPNTestCase(TestCase, ChangeLoggedFilterSetTests):
+ queryset = L2VPN.objects.all()
+ filterset = L2VPNFilterSet
+
+ @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', slug='l2vpn-1', type=L2VPNTypeChoices.TYPE_VXLAN, identifier=65001),
+ L2VPN(name='L2VPN 2', slug='l2vpn-2', type=L2VPNTypeChoices.TYPE_VPWS, identifier=65002),
+ L2VPN(name='L2VPN 3', slug='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_slug(self):
+ params = {'slug': ['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):
+ queryset = L2VPNTermination.objects.all()
+ filterset = L2VPNTerminationFilterSet
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interfaces = (
+ 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)
+
+ 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', 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)
+
+ l2vpnterminations = (
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
+ L2VPNTermination(l2vpn=l2vpns[1], assigned_object=vlans[1]),
+ L2VPNTermination(l2vpn=l2vpns[2], assigned_object=vlans[2]),
+ 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_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(), 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_interface(self):
+ interfaces = Interface.objects.all()[:2]
+ params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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_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)
+ params = {'vlan': ['VLAN 1', 'VLAN 2']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
+ def test_site(self):
+ site = Site.objects.all().first()
+ params = {'site_id': [site.pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'site': ['site-1']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+ def test_device(self):
+ device = Device.objects.all().first()
+ params = {'device_id': [device.pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'device': ['Device 1']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+
+ def test_virtual_machine(self):
+ virtual_machine = VirtualMachine.objects.all().first()
+ params = {'virtual_machine_id': [virtual_machine.pk]}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
+ params = {'virtual_machine': ['Virtual Machine 1']}
+ self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
diff --git a/netbox/vpn/tests/test_models.py b/netbox/vpn/tests/test_models.py
new file mode 100644
index 00000000000..e464dccd926
--- /dev/null
+++ b/netbox/vpn/tests/test_models.py
@@ -0,0 +1,79 @@
+from django.core.exceptions import ValidationError
+from django.test import TestCase
+
+from dcim.models import Interface, Device, DeviceRole, DeviceType, Manufacturer, Site
+from ipam.models import VLAN
+from vpn.models import *
+
+
+class TestL2VPNTermination(TestCase):
+
+ @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)
+ role = DeviceRole.objects.create(name='Switch')
+ device = Device.objects.create(
+ name='Device 1',
+ site=site,
+ device_type=device_type,
+ role=role,
+ status='active'
+ )
+
+ 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.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),
+ VLAN(name='VLAN 6', vid=656),
+ VLAN(name='VLAN 7', vid=657)
+ )
+
+ VLAN.objects.bulk_create(vlans)
+
+ l2vpns = (
+ L2VPN(name='L2VPN 1', slug='l2vpn-1', type='vxlan', identifier=650001),
+ L2VPN(name='L2VPN 2', slug='l2vpn-2', type='vpws', identifier=650002),
+ L2VPN(name='L2VPN 3', slug='l2vpn-3', type='vpls'), # No RD
+ )
+ L2VPN.objects.bulk_create(l2vpns)
+
+ l2vpnterminations = (
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
+ )
+
+ L2VPNTermination.objects.bulk_create(l2vpnterminations)
+
+ def test_duplicate_interface_terminations(self):
+ device = Device.objects.first()
+ interface = Interface.objects.filter(device=device).first()
+ l2vpn = L2VPN.objects.first()
+
+ L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=interface)
+ duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=interface)
+
+ self.assertRaises(ValidationError, duplicate.clean)
+
+ def test_duplicate_vlan_terminations(self):
+ vlan = Interface.objects.first()
+ l2vpn = L2VPN.objects.first()
+
+ L2VPNTermination.objects.create(l2vpn=l2vpn, assigned_object=vlan)
+ duplicate = L2VPNTermination(l2vpn=l2vpn, assigned_object=vlan)
+ self.assertRaises(ValidationError, duplicate.clean)
diff --git a/netbox/vpn/tests/test_views.py b/netbox/vpn/tests/test_views.py
index 433eca4679e..4d908042201 100644
--- a/netbox/vpn/tests/test_views.py
+++ b/netbox/vpn/tests/test_views.py
@@ -1,8 +1,9 @@
from dcim.choices import InterfaceTypeChoices
from dcim.models import Interface
+from ipam.models import RouteTarget, VLAN
+from utilities.testing import ViewTestCases, create_tags, create_test_device
from vpn.choices import *
from vpn.models import *
-from utilities.testing import ViewTestCases, create_tags, create_test_device
class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
@@ -506,3 +507,142 @@ def setUpTestData(cls):
'ike_policy': ike_policies[1].pk,
'ipsec_policy': ipsec_policies[1].pk,
}
+
+
+class L2VPNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = L2VPN
+
+ @classmethod
+ def setUpTestData(cls):
+ rts = (
+ RouteTarget(name='64534:123'),
+ RouteTarget(name='64534:321')
+ )
+ RouteTarget.objects.bulk_create(rts)
+
+ l2vpns = (
+ 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)
+
+ cls.csv_data = (
+ 'name,slug,type,identifier',
+ 'L2VPN 5,l2vpn-5,vxlan,456',
+ 'L2VPN 6,l2vpn-6,vxlan,444',
+ )
+
+ cls.csv_update_data = (
+ 'id,name,description',
+ f'{l2vpns[0].pk},L2VPN 7,New description 7',
+ f'{l2vpns[1].pk},L2VPN 8,New description 8',
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New Description',
+ }
+
+ cls.form_data = {
+ 'name': 'L2VPN 8',
+ 'slug': 'l2vpn-8',
+ 'type': L2VPNTypeChoices.TYPE_VXLAN,
+ 'identifier': 123,
+ 'description': 'Description',
+ 'import_targets': [rts[0].pk],
+ 'export_targets': [rts[1].pk]
+ }
+
+
+class L2VPNTerminationTestCase(
+ ViewTestCases.GetObjectViewTestCase,
+ ViewTestCases.GetObjectChangelogViewTestCase,
+ ViewTestCases.CreateObjectViewTestCase,
+ ViewTestCases.EditObjectViewTestCase,
+ ViewTestCases.DeleteObjectViewTestCase,
+ ViewTestCases.ListObjectsViewTestCase,
+ ViewTestCases.BulkImportObjectsViewTestCase,
+ ViewTestCases.BulkDeleteObjectsViewTestCase,
+):
+
+ model = L2VPNTermination
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interface = Interface.objects.create(name='Interface 1', device=device, type='1000baset')
+ l2vpns = (
+ 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.objects.bulk_create(l2vpns)
+
+ vlans = (
+ VLAN(name='Vlan 1', vid=1001),
+ VLAN(name='Vlan 2', vid=1002),
+ VLAN(name='Vlan 3', vid=1003),
+ VLAN(name='Vlan 4', vid=1004),
+ VLAN(name='Vlan 5', vid=1005),
+ VLAN(name='Vlan 6', vid=1006)
+ )
+ VLAN.objects.bulk_create(vlans)
+
+ terminations = (
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[0]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[1]),
+ L2VPNTermination(l2vpn=l2vpns[0], assigned_object=vlans[2])
+ )
+ L2VPNTermination.objects.bulk_create(terminations)
+
+ cls.form_data = {
+ 'l2vpn': l2vpns[0].pk,
+ 'device': device.pk,
+ 'interface': interface.pk,
+ }
+
+ cls.csv_data = (
+ "l2vpn,vlan",
+ "L2VPN 1,Vlan 4",
+ "L2VPN 1,Vlan 5",
+ "L2VPN 1,Vlan 6",
+ )
+
+ cls.csv_update_data = (
+ f"id,l2vpn",
+ f"{terminations[0].pk},{l2vpns[0].name}",
+ f"{terminations[1].pk},{l2vpns[0].name}",
+ f"{terminations[2].pk},{l2vpns[0].name}",
+ )
+
+ cls.bulk_edit_data = {}
+
+ # TODO: Fix L2VPNTerminationImportForm validation to support bulk updates
+ def test_bulk_update_objects_with_permission(self):
+ pass
+
+ #
+ # Custom assertions
+ #
+
+ # TODO: Remove this
+ def assertInstanceEqual(self, instance, data, exclude=None, api=False):
+ """
+ Override parent
+ """
+ if exclude is None:
+ exclude = []
+
+ fields = [k for k in data.keys() if k not in exclude]
+ model_dict = self.model_to_dict(instance, fields=fields, api=api)
+
+ # Omit any dictionary keys which are not instance attributes or have been excluded
+ relevant_data = {
+ k: v for k, v in data.items() if hasattr(instance, k) and k not in exclude
+ }
+
+ # Handle relations on the model
+ for k, v in model_dict.items():
+ if isinstance(v, object) and hasattr(v, 'first'):
+ model_dict[k] = v.first().pk
+
+ self.assertDictEqual(model_dict, relevant_data)
diff --git a/netbox/vpn/urls.py b/netbox/vpn/urls.py
index 7fe54824548..0e1b1664e53 100644
--- a/netbox/vpn/urls.py
+++ b/netbox/vpn/urls.py
@@ -62,4 +62,20 @@
path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'),
path('ipsec-profiles//', include(get_model_urls('vpn', 'ipsecprofile'))),
+ # L2VPN
+ path('l2vpns/', views.L2VPNListView.as_view(), name='l2vpn_list'),
+ path('l2vpns/add/', views.L2VPNEditView.as_view(), name='l2vpn_add'),
+ path('l2vpns/import/', views.L2VPNBulkImportView.as_view(), name='l2vpn_import'),
+ path('l2vpns/edit/', views.L2VPNBulkEditView.as_view(), name='l2vpn_bulk_edit'),
+ path('l2vpns/delete/', views.L2VPNBulkDeleteView.as_view(), name='l2vpn_bulk_delete'),
+ path('l2vpns//', include(get_model_urls('vpn', 'l2vpn'))),
+
+ # L2VPN terminations
+ path('l2vpn-terminations/', views.L2VPNTerminationListView.as_view(), name='l2vpntermination_list'),
+ path('l2vpn-terminations/add/', views.L2VPNTerminationEditView.as_view(), name='l2vpntermination_add'),
+ path('l2vpn-terminations/import/', views.L2VPNTerminationBulkImportView.as_view(), name='l2vpntermination_import'),
+ path('l2vpn-terminations/edit/', views.L2VPNTerminationBulkEditView.as_view(), name='l2vpntermination_bulk_edit'),
+ path('l2vpn-terminations/delete/', views.L2VPNTerminationBulkDeleteView.as_view(), name='l2vpntermination_bulk_delete'),
+ path('l2vpn-terminations//', include(get_model_urls('vpn', 'l2vpntermination'))),
+
]
diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py
index 56eadc07715..f230e48284a 100644
--- a/netbox/vpn/views.py
+++ b/netbox/vpn/views.py
@@ -1,4 +1,6 @@
+from ipam.tables import RouteTargetTable
from netbox.views import generic
+from tenancy.views import ObjectContactsView
from utilities.utils import count_related
from utilities.views import register_model_view
from . import filtersets, forms, tables
@@ -332,3 +334,112 @@ class IPSecProfileBulkDeleteView(generic.BulkDeleteView):
queryset = IPSecProfile.objects.all()
filterset = filtersets.IPSecProfileFilterSet
table = tables.IPSecProfileTable
+
+
+# L2VPN
+
+class L2VPNListView(generic.ObjectListView):
+ queryset = L2VPN.objects.all()
+ table = tables.L2VPNTable
+ filterset = filtersets.L2VPNFilterSet
+ filterset_form = forms.L2VPNFilterForm
+
+
+@register_model_view(L2VPN)
+class L2VPNView(generic.ObjectView):
+ queryset = L2VPN.objects.all()
+
+ def get_extra_context(self, request, instance):
+ import_targets_table = RouteTargetTable(
+ instance.import_targets.prefetch_related('tenant'),
+ orderable=False
+ )
+ export_targets_table = RouteTargetTable(
+ instance.export_targets.prefetch_related('tenant'),
+ orderable=False
+ )
+
+ return {
+ 'import_targets_table': import_targets_table,
+ 'export_targets_table': export_targets_table,
+ }
+
+
+@register_model_view(L2VPN, 'edit')
+class L2VPNEditView(generic.ObjectEditView):
+ queryset = L2VPN.objects.all()
+ form = forms.L2VPNForm
+
+
+@register_model_view(L2VPN, 'delete')
+class L2VPNDeleteView(generic.ObjectDeleteView):
+ queryset = L2VPN.objects.all()
+
+
+class L2VPNBulkImportView(generic.BulkImportView):
+ queryset = L2VPN.objects.all()
+ model_form = forms.L2VPNImportForm
+
+
+class L2VPNBulkEditView(generic.BulkEditView):
+ queryset = L2VPN.objects.all()
+ filterset = filtersets.L2VPNFilterSet
+ table = tables.L2VPNTable
+ form = forms.L2VPNBulkEditForm
+
+
+class L2VPNBulkDeleteView(generic.BulkDeleteView):
+ queryset = L2VPN.objects.all()
+ filterset = filtersets.L2VPNFilterSet
+ table = tables.L2VPNTable
+
+
+@register_model_view(L2VPN, 'contacts')
+class L2VPNContactsView(ObjectContactsView):
+ queryset = L2VPN.objects.all()
+
+
+#
+# L2VPN terminations
+#
+
+class L2VPNTerminationListView(generic.ObjectListView):
+ queryset = L2VPNTermination.objects.all()
+ table = tables.L2VPNTerminationTable
+ filterset = filtersets.L2VPNTerminationFilterSet
+ filterset_form = forms.L2VPNTerminationFilterForm
+
+
+@register_model_view(L2VPNTermination)
+class L2VPNTerminationView(generic.ObjectView):
+ queryset = L2VPNTermination.objects.all()
+
+
+@register_model_view(L2VPNTermination, 'edit')
+class L2VPNTerminationEditView(generic.ObjectEditView):
+ queryset = L2VPNTermination.objects.all()
+ form = forms.L2VPNTerminationForm
+ template_name = 'vpn/l2vpntermination_edit.html'
+
+
+@register_model_view(L2VPNTermination, 'delete')
+class L2VPNTerminationDeleteView(generic.ObjectDeleteView):
+ queryset = L2VPNTermination.objects.all()
+
+
+class L2VPNTerminationBulkImportView(generic.BulkImportView):
+ queryset = L2VPNTermination.objects.all()
+ model_form = forms.L2VPNTerminationImportForm
+
+
+class L2VPNTerminationBulkEditView(generic.BulkEditView):
+ queryset = L2VPNTermination.objects.all()
+ filterset = filtersets.L2VPNTerminationFilterSet
+ table = tables.L2VPNTerminationTable
+ form = forms.L2VPNTerminationBulkEditForm
+
+
+class L2VPNTerminationBulkDeleteView(generic.BulkDeleteView):
+ queryset = L2VPNTermination.objects.all()
+ filterset = filtersets.L2VPNTerminationFilterSet
+ table = tables.L2VPNTerminationTable