diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 961fd2035..904106ecf 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -195,17 +195,30 @@ IPAM_MENU = Menu( ), ) -OVERLAY_MENU = Menu( - label=_('Overlay'), +VPN_MENU = Menu( + label=_('VPN'), icon_class='mdi mdi-graph-outline', groups=( MenuGroup( - label='L2VPNs', + label=_('Tunnels'), + items=( + get_model_item('vpn', 'tunnel', _('Tunnels')), + get_model_item('vpn', 'tunneltermination', _('Tunnel Terminations')), + ), + ), + MenuGroup( + label=_('L2VPNs'), items=( get_model_item('ipam', 'l2vpn', _('L2VPNs')), get_model_item('ipam', 'l2vpntermination', _('Terminations')), ), ), + MenuGroup( + label=_('Security'), + items=( + get_model_item('vpn', 'ipsecprofile', _('IPSec Profiles')), + ), + ), ), ) @@ -443,7 +456,7 @@ MENUS = [ CONNECTIONS_MENU, WIRELESS_MENU, IPAM_MENU, - OVERLAY_MENU, + VPN_MENU, VIRTUALIZATION_MENU, CIRCUITS_MENU, POWER_MENU, diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 465389a11..ce8ab5876 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -379,6 +379,7 @@ INSTALLED_APPS = [ 'users', 'utilities', 'virtualization', + 'vpn', 'wireless', 'django_rq', # Must come after extras to allow overriding management commands 'drf_spectacular', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 6955426a8..984358911 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -33,6 +33,7 @@ _patterns = [ path('tenancy/', include('tenancy.urls')), path('users/', include('users.urls')), path('virtualization/', include('virtualization.urls')), + path('vpn/', include('vpn.urls')), path('wireless/', include('wireless.urls')), # Current user views @@ -51,6 +52,7 @@ _patterns = [ path('api/tenancy/', include('tenancy.api.urls')), path('api/users/', include('users.api.urls')), path('api/virtualization/', include('virtualization.api.urls')), + path('api/vpn/', include('vpn.api.urls')), path('api/wireless/', include('wireless.api.urls')), path('api/status/', StatusView.as_view(), name='api-status'), diff --git a/netbox/vpn/__init__.py b/netbox/vpn/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/admin.py b/netbox/vpn/admin.py new file mode 100644 index 000000000..8c38f3f3d --- /dev/null +++ b/netbox/vpn/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/netbox/vpn/api/__init__.py b/netbox/vpn/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/api/nested_serializers.py b/netbox/vpn/api/nested_serializers.py new file mode 100644 index 000000000..7ab1654b9 --- /dev/null +++ b/netbox/vpn/api/nested_serializers.py @@ -0,0 +1,40 @@ +from rest_framework import serializers + +from netbox.api.serializers import WritableNestedSerializer +from vpn import models + +__all__ = ( + 'NestedIPSecProfileSerializer', + 'NestedTunnelSerializer', + 'NestedTunnelTerminationSerializer', +) + + +class NestedTunnelSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunnel-detail' + ) + + class Meta: + model = models.Tunnel + fields = ('id', 'url', 'display', 'name') + + +class NestedTunnelTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunneltermination-detail' + ) + + class Meta: + model = models.TunnelTermination + fields = ('id', 'url', 'display') + + +class NestedIPSecProfileSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecprofile-detail' + ) + + class Meta: + model = models.IPSecProfile + fields = ('id', 'url', 'display', 'name') diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py new file mode 100644 index 000000000..803db2acd --- /dev/null +++ b/netbox/vpn/api/serializers.py @@ -0,0 +1,117 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.api.nested_serializers import NestedIPAddressSerializer +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.constants import NESTED_SERIALIZER_PREFIX +from tenancy.api.nested_serializers import NestedTenantSerializer +from utilities.api import get_serializer_for_model +from vpn.choices import * +from vpn.models import * +from .nested_serializers import * + +__all__ = ( + 'IPSecProfileSerializer', + 'TunnelSerializer', + 'TunnelTerminationSerializer', +) + + +class TunnelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunnel-detail' + ) + status = ChoiceField( + choices=TunnelStatusChoices + ) + encapsulation = ChoiceField( + choices=TunnelEncapsulationChoices + ) + ipsec_profile = NestedIPSecProfileSerializer( + required=False, + allow_null=True + ) + tenant = NestedTenantSerializer( + required=False, + allow_null=True + ) + + class Meta: + model = Tunnel + fields = ( + 'id', 'url', 'display', 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'preshared_key', + 'tunnel_id', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ) + + +class TunnelTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunneltermination-detail' + ) + tunnel = NestedTunnelSerializer() + role = ChoiceField( + choices=TunnelTerminationRoleChoices + ) + interface_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + interface = serializers.SerializerMethodField( + read_only=True + ) + outside_ip = NestedIPAddressSerializer( + required=False, + allow_null=True + ) + + class Meta: + model = TunnelTermination + fields = ( + 'id', 'url', 'display', 'tunnel', 'role', 'interface_type', 'interface_id', 'interface', 'outside_ip', + 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ) + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_interface(self, obj): + serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.assigned_object, context=context).data + + +class IPSecProfileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecprofile-detail' + ) + protocol = ChoiceField( + choices=IPSecProtocolChoices + ) + ike_version = ChoiceField( + choices=IKEVersionChoices + ) + phase1_encryption = ChoiceField( + choices=EncryptionChoices + ) + phase1_authentication = ChoiceField( + choices=AuthenticationChoices + ) + phase1_group = ChoiceField( + choices=DHGroupChoices + ) + phase2_encryption = ChoiceField( + choices=EncryptionChoices + ) + phase2_authentication = ChoiceField( + choices=AuthenticationChoices + ) + phase2_group = ChoiceField( + choices=DHGroupChoices + ) + + class Meta: + model = IPSecProfile + fields = ( + 'id', 'url', 'display', 'name', 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', + 'phase1_group', 'phase1_sa_lifetime', 'phase2_encryption', 'phase2_authentication', 'phase2_group', + 'phase2_sa_lifetime', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ) diff --git a/netbox/vpn/api/urls.py b/netbox/vpn/api/urls.py new file mode 100644 index 000000000..084514e35 --- /dev/null +++ b/netbox/vpn/api/urls.py @@ -0,0 +1,11 @@ +from netbox.api.routers import NetBoxRouter +from . import views + +router = NetBoxRouter() +router.APIRootView = views.VPNRootView +router.register('ipsec-profiles', views.IPSecProfileViewSet) +router.register('tunnels', views.TunnelViewSet) +router.register('tunnel-terminations', views.TunnelTerminationViewSet) + +app_name = 'vpn-api' +urlpatterns = router.urls diff --git a/netbox/vpn/api/views.py b/netbox/vpn/api/views.py new file mode 100644 index 000000000..6cb99f2a7 --- /dev/null +++ b/netbox/vpn/api/views.py @@ -0,0 +1,46 @@ +from rest_framework.routers import APIRootView + +from netbox.api.viewsets import NetBoxModelViewSet +from utilities.utils import count_related +from vpn import filtersets +from vpn.models import * +from . import serializers + +__all__ = ( + 'IPSecProfileViewSet', + 'TunnelTerminationViewSet', + 'TunnelViewSet', + 'VPNRootView', +) + + +class VPNRootView(APIRootView): + """ + VPN API root view + """ + def get_view_name(self): + return 'VPN' + + +# +# Viewsets +# + +class TunnelViewSet(NetBoxModelViewSet): + queryset = Tunnel.objects.prefetch_related('ipsec_profile', 'tenant').annotate( + terminations_count=count_related(TunnelTermination, 'tunnel') + ) + serializer_class = serializers.TunnelSerializer + filterset_class = filtersets.TunnelFilterSet + + +class TunnelTerminationViewSet(NetBoxModelViewSet): + queryset = Tunnel.objects.prefetch_related('tunnel') + serializer_class = serializers.TunnelTerminationSerializer + filterset_class = filtersets.TunnelTerminationFilterSet + + +class IPSecProfileViewSet(NetBoxModelViewSet): + queryset = IPSecProfile.objects.all() + serializer_class = serializers.IPSecProfileSerializer + filterset_class = filtersets.IPSecProfileFilterSet diff --git a/netbox/vpn/apps.py b/netbox/vpn/apps.py new file mode 100644 index 000000000..2254befd3 --- /dev/null +++ b/netbox/vpn/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class VPNConfig(AppConfig): + name = 'vpn' + verbose_name = 'VPN' + + def ready(self): + from . import search diff --git a/netbox/vpn/choices.py b/netbox/vpn/choices.py new file mode 100644 index 000000000..24a7b8c8d --- /dev/null +++ b/netbox/vpn/choices.py @@ -0,0 +1,108 @@ +from django.utils.translation import gettext_lazy as _ + +from utilities.choices import ChoiceSet + + +# +# Tunnels +# + +class TunnelStatusChoices(ChoiceSet): + key = 'Tunnel.status' + + STATUS_PLANNED = 'planned' + STATUS_ACTIVE = 'active' + STATUS_DISABLED = 'disabled' + + CHOICES = [ + (STATUS_PLANNED, _('Planned'), 'cyan'), + (STATUS_ACTIVE, _('Active'), 'green'), + (STATUS_DISABLED, _('Disabled'), 'red'), + ] + + +class TunnelEncapsulationChoices(ChoiceSet): + ENCAP_GRE = 'gre' + ENCAP_IP_IP = 'ip-ip' + ENCAP_IPSEC = 'ipsec' + + CHOICES = [ + (ENCAP_IPSEC, _('IPsec')), + (ENCAP_IP_IP, _('Active')), + (ENCAP_GRE, _('Disabled')), + ] + + +class TunnelTerminationRoleChoices(ChoiceSet): + ROLE_PEER = 'peer' + ROLE_HUB = 'hub' + ROLE_SPOKE = 'spoke' + + CHOICES = [ + (ROLE_PEER, _('Peer')), + (ROLE_HUB, _('Hub')), + (ROLE_SPOKE, _('Spoke')), + ] + + +# +# IKE +# + +class IPSecProtocolChoices(ChoiceSet): + PROTOCOL_ESP = 'esp' + PROTOCOL_AH = 'ah' + + CHOICES = ( + (PROTOCOL_ESP, 'ESP'), + (PROTOCOL_AH, 'AH'), + ) + + +class IKEVersionChoices(ChoiceSet): + VERSION_1 = 1 + VERSION_2 = 2 + + CHOICES = ( + (VERSION_1, 'IKEv1'), + (VERSION_2, 'IKEv2'), + ) + + +class EncryptionChoices(ChoiceSet): + ENCRYPTION_AES128 = 'aes-128' + ENCRYPTION_AES192 = 'aes-192' + ENCRYPTION_AES256 = 'aes-256' + ENCRYPTION_3DES = '3des' + + CHOICES = ( + (ENCRYPTION_AES128, 'AES (128-bit)'), + (ENCRYPTION_AES192, 'AES (192-bit)'), + (ENCRYPTION_AES256, 'AES (256-bit)'), + (ENCRYPTION_3DES, '3DES'), + ) + + +class AuthenticationChoices(ChoiceSet): + AUTH_SHA1 = 'SHA-1' + AUTH_MD5 = 'MD5' + + CHOICES = ( + (AUTH_SHA1, 'SHA-1'), + (AUTH_MD5, 'MD5'), + ) + + +class DHGroupChoices(ChoiceSet): + # TODO: Add all the groups & annotate their attributes + GROUP_1 = 1 + GROUP_2 = 2 + GROUP_5 = 5 + GROUP_7 = 7 + + CHOICES = ( + (GROUP_1, _('Group {n}').format(n=1)), + (GROUP_2, _('Group {n}').format(n=2)), + (GROUP_5, _('Group {n}').format(n=5)), + (GROUP_7, _('Group {n}').format(n=7)), + ) diff --git a/netbox/vpn/filtersets.py b/netbox/vpn/filtersets.py new file mode 100644 index 000000000..061aea418 --- /dev/null +++ b/netbox/vpn/filtersets.py @@ -0,0 +1,137 @@ +import django_filters +from django.db.models import Q +from django.utils.translation import gettext as _ + +from dcim.models import Interface +from ipam.models import IPAddress +from netbox.filtersets import NetBoxModelFilterSet +from tenancy.filtersets import TenancyFilterSet +from virtualization.models import VMInterface +from .choices import * +from .models import * + +__all__ = ( + 'IPSecProfileFilterSet', + 'TunnelFilterSet', + 'TunnelTerminationFilterSet', +) + + +class TunnelFilterSet(NetBoxModelFilterSet, TenancyFilterSet): + status = django_filters.MultipleChoiceFilter( + choices=TunnelStatusChoices + ) + encapsulation = django_filters.MultipleChoiceFilter( + choices=TunnelEncapsulationChoices + ) + ipsec_profile_id = django_filters.ModelMultipleChoiceFilter( + queryset=IPSecProfile.objects.all(), + label=_('IPSec profile (ID)'), + ) + ipsec_profile = django_filters.ModelMultipleChoiceFilter( + field_name='ipsec_profile__name', + queryset=IPSecProfile.objects.all(), + to_field_name='name', + label=_('IPSec profile (name)'), + ) + + class Meta: + model = Tunnel + fields = ['id', 'name', 'preshared_key', 'tunnel_id'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) + + +class TunnelTerminationFilterSet(NetBoxModelFilterSet): + tunnel_id = django_filters.ModelMultipleChoiceFilter( + field_name='tunnel', + queryset=Tunnel.objects.all(), + label=_('Tunnel (ID)'), + ) + tunnel = django_filters.ModelMultipleChoiceFilter( + field_name='tunnel__name', + queryset=IPSecProfile.objects.all(), + to_field_name='name', + label=_('Tunnel (name)'), + ) + role = django_filters.MultipleChoiceFilter( + choices=TunnelTerminationRoleChoices + ) + # 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='interface__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)'), + # ) + outside_ip_id = django_filters.ModelMultipleChoiceFilter( + field_name='outside_ip', + queryset=IPAddress.objects.all(), + label=_('Outside IP (ID)'), + ) + + class Meta: + model = TunnelTermination + fields = ['id'] + + +class IPSecProfileFilterSet(NetBoxModelFilterSet): + protocol = django_filters.MultipleChoiceFilter( + choices=IPSecProtocolChoices + ) + ike_version = django_filters.MultipleChoiceFilter( + choices=IKEVersionChoices + ) + phase1_encryption = django_filters.MultipleChoiceFilter( + choices=EncryptionChoices + ) + phase1_authentication = django_filters.MultipleChoiceFilter( + choices=AuthenticationChoices + ) + phase1_group = django_filters.MultipleChoiceFilter( + choices=DHGroupChoices + ) + phase2_encryption = django_filters.MultipleChoiceFilter( + choices=EncryptionChoices + ) + phase2_authentication = django_filters.MultipleChoiceFilter( + choices=AuthenticationChoices + ) + phase2_group = django_filters.MultipleChoiceFilter( + choices=DHGroupChoices + ) + + class Meta: + model = IPSecProfile + fields = ['id', 'name', 'phase1_sa_lifetime', 'phase2_sa_lifetime'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + return queryset.filter( + Q(name__icontains=value) | + Q(description__icontains=value) | + Q(comments__icontains=value) + ) diff --git a/netbox/vpn/forms/__init__.py b/netbox/vpn/forms/__init__.py new file mode 100644 index 000000000..1499f98b2 --- /dev/null +++ b/netbox/vpn/forms/__init__.py @@ -0,0 +1,4 @@ +from .bulk_edit import * +from .bulk_import import * +from .filtersets import * +from .model_forms import * diff --git a/netbox/vpn/forms/bulk_edit.py b/netbox/vpn/forms/bulk_edit.py new file mode 100644 index 000000000..db33cc95b --- /dev/null +++ b/netbox/vpn/forms/bulk_edit.py @@ -0,0 +1,140 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from netbox.forms import NetBoxModelBulkEditForm +from tenancy.models import Tenant +from utilities.forms import add_blank_choice +from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from vpn.choices import * +from vpn.models import * + +__all__ = ( + 'IPSecProfileBulkEditForm', + 'TunnelBulkEditForm', + 'TunnelTerminationBulkEditForm', +) + + +class TunnelBulkEditForm(NetBoxModelBulkEditForm): + status = forms.ChoiceField( + label=_('Status'), + choices=add_blank_choice(TunnelStatusChoices), + required=False + ) + encapsulation = forms.ChoiceField( + label=_('Encapsulation'), + choices=add_blank_choice(TunnelEncapsulationChoices), + required=False + ) + ipsec_profile = DynamicModelMultipleChoiceField( + queryset=IPSecProfile.objects.all(), + label=_('IPSec profile'), + required=False + ) + preshared_key = forms.CharField( + label=_('Pre-shared key'), + required=False + ) + tenant = DynamicModelChoiceField( + label=_('Tenant'), + queryset=Tenant.objects.all(), + required=False + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + tunnel_id = forms.IntegerField( + label=_('Tunnel ID'), + required=False + ) + comments = CommentField() + + model = Tunnel + fieldsets = ( + (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id', 'description')), + (_('Security'), ('ipsec_profile', 'preshared_key')), + (_('Tenancy'), ('tenant',)), + ) + nullable_fields = ( + 'ipsec_profile', 'preshared_key', 'tunnel_id', 'tenant', 'description', 'comments', + ) + + +class TunnelTerminationBulkEditForm(NetBoxModelBulkEditForm): + role = forms.ChoiceField( + label=_('Role'), + choices=add_blank_choice(TunnelTerminationRoleChoices), + required=False + ) + + model = TunnelTermination + fieldsets = ( + (None, ('role',)), + ) + + +class IPSecProfileBulkEditForm(NetBoxModelBulkEditForm): + protocol = forms.ChoiceField( + label=_('Protocol'), + choices=add_blank_choice(IPSecProtocolChoices), + required=False + ) + ike_version = forms.ChoiceField( + label=_('IKE version'), + choices=add_blank_choice(IKEVersionChoices), + required=False + ) + description = forms.CharField( + label=_('Description'), + max_length=200, + required=False + ) + phase1_encryption = forms.ChoiceField( + label=_('Encryption'), + choices=add_blank_choice(EncryptionChoices), + required=False + ) + phase1_authentication = forms.ChoiceField( + label=_('Authentication'), + choices=add_blank_choice(AuthenticationChoices), + required=False + ) + phase1_group = forms.ChoiceField( + label=_('Group'), + choices=add_blank_choice(DHGroupChoices), + required=False + ) + phase1_sa_lifetime = forms.IntegerField( + required=False + ) + phase2_encryption = forms.ChoiceField( + label=_('Encryption'), + choices=add_blank_choice(EncryptionChoices), + required=False + ) + phase2_authentication = forms.ChoiceField( + label=_('Authentication'), + choices=add_blank_choice(AuthenticationChoices), + required=False + ) + phase2_group = forms.ChoiceField( + label=_('Group'), + choices=add_blank_choice(DHGroupChoices), + required=False + ) + phase2_sa_lifetime = forms.IntegerField( + required=False + ) + comments = CommentField() + + model = IPSecProfile + fieldsets = ( + (_('Profile'), ('protocol', 'ike_version', 'description')), + (_('Phase 1 Parameters'), ('phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime')), + (_('Phase 2 Parameters'), ('phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime')), + ) + nullable_fields = ( + 'description', 'phase1_sa_lifetime', 'phase2_sa_lifetime', 'comments', + ) diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py new file mode 100644 index 000000000..61e9a4999 --- /dev/null +++ b/netbox/vpn/forms/bulk_import.py @@ -0,0 +1,153 @@ +from django.utils.translation import gettext_lazy as _ + +from dcim.models import Device, Interface +from ipam.models import IPAddress +from netbox.forms import NetBoxModelImportForm +from tenancy.models import Tenant +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField +from virtualization.models import VirtualMachine, VMInterface +from vpn.choices import * +from vpn.models import * + +__all__ = ( + 'IPSecProfileImportForm', + 'TunnelImportForm', + 'TunnelTerminationImportForm', +) + + +class TunnelImportForm(NetBoxModelImportForm): + status = CSVChoiceField( + label=_('Status'), + choices=TunnelStatusChoices, + help_text=_('Operational status') + ) + encapsulation = CSVChoiceField( + label=_('Encapsulation'), + choices=TunnelEncapsulationChoices, + help_text=_('Tunnel encapsulation') + ) + ipsec_profile = CSVModelChoiceField( + label=_('IPSec profile'), + queryset=IPSecProfile.objects.all(), + to_field_name='name' + ) + tenant = CSVModelChoiceField( + label=_('Tenant'), + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text=_('Assigned tenant') + ) + + class Meta: + model = Tunnel + fields = ( + 'name', 'status', 'encapsulation', 'ipsec_profile', 'tenant', 'preshared_key', 'tunnel_id', 'description', + 'comments', 'tags', + ) + + +class TunnelTerminationImportForm(NetBoxModelImportForm): + tunnel = CSVModelChoiceField( + label=_('Tunnel'), + queryset=Tunnel.objects.all(), + to_field_name='name' + ) + role = CSVChoiceField( + label=_('Role'), + choices=TunnelTerminationRoleChoices, + help_text=_('Operational role') + ) + device = CSVModelChoiceField( + label=_('Device'), + queryset=Device.objects.all(), + required=False, + to_field_name='name', + help_text=_('Parent device of assigned interface') + ) + virtual_machine = CSVModelChoiceField( + label=_('Virtual machine'), + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name='name', + help_text=_('Parent VM of assigned interface') + ) + interface = CSVModelChoiceField( + label=_('Interface'), + queryset=Interface.objects.none(), # Can also refer to VMInterface + required=False, + to_field_name='name', + help_text=_('Assigned interface') + ) + outside_ip = CSVModelChoiceField( + label=_('Outside IP'), + queryset=IPAddress.objects.all(), + to_field_name='name' + ) + + class Meta: + model = TunnelTermination + fields = ( + 'tunnel', 'role', 'outside_ip', 'tags', + ) + + def __init__(self, data=None, *args, **kwargs): + super().__init__(data, *args, **kwargs) + + if data: + + # Limit interface queryset by assigned device/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']} + ) + + +class IPSecProfileImportForm(NetBoxModelImportForm): + protocol = CSVChoiceField( + label=_('Protocol'), + choices=IPSecProtocolChoices, + help_text=_('IPSec protocol') + ) + ike_version = CSVChoiceField( + label=_('IKE version'), + choices=IKEVersionChoices, + help_text=_('IKE version') + ) + phase1_encryption = CSVChoiceField( + label=_('Phase 1 Encryption'), + choices=EncryptionChoices + ) + phase1_authentication = CSVChoiceField( + label=_('Phase 1 Authentication'), + choices=AuthenticationChoices + ) + phase1_group = CSVChoiceField( + label=_('Phase 1 Group'), + choices=DHGroupChoices + ) + phase2_encryption = CSVChoiceField( + label=_('Phase 2 Encryption'), + choices=EncryptionChoices + ) + phase2_authentication = CSVChoiceField( + label=_('Phase 2 Authentication'), + choices=AuthenticationChoices + ) + phase2_group = CSVChoiceField( + label=_('Phase 2 Group'), + choices=DHGroupChoices + ) + + class Meta: + model = IPSecProfile + fields = ( + 'name', 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', + 'phase1_sa_lifetime', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime', + 'description', 'comments', 'tags', + ) diff --git a/netbox/vpn/forms/filtersets.py b/netbox/vpn/forms/filtersets.py new file mode 100644 index 000000000..9f11de5a3 --- /dev/null +++ b/netbox/vpn/forms/filtersets.py @@ -0,0 +1,124 @@ +from django import forms +from django.utils.translation import gettext as _ + +from netbox.forms import NetBoxModelFilterSetForm +from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from vpn.choices import * +from vpn.models import * + +__all__ = ( + 'IPSecProfileFilterForm', + 'TunnelFilterForm', + 'TunnelTerminationFilterForm', +) + + +class TunnelFilterForm(NetBoxModelFilterSetForm): + model = Tunnel + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + (_('Tunnel'), ('status', 'encapsulation', 'tunnel_id')), + (_('Security'), ('ipsec_profile_id', 'preshared_key')), + (_('Tenancy'), ('tenant',)), + ) + status = forms.MultipleChoiceField( + label=_('Status'), + choices=TunnelStatusChoices, + required=False + ) + encapsulation = forms.MultipleChoiceField( + label=_('Encapsulation'), + choices=TunnelEncapsulationChoices, + required=False + ) + ipsec_profile_id = DynamicModelMultipleChoiceField( + queryset=IPSecProfile.objects.all(), + required=False, + label=_('IPSec profile') + ) + tag = TagFilterField(model) + + +class TunnelTerminationFilterForm(NetBoxModelFilterSetForm): + model = TunnelTermination + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + (_('Termination'), ('tunnel_id', 'role')), + ) + tunnel_id = DynamicModelMultipleChoiceField( + queryset=Tunnel.objects.all(), + required=False, + label=_('Tunnel') + ) + role = forms.MultipleChoiceField( + label=_('Role'), + choices=TunnelTerminationRoleChoices, + required=False + ) + tag = TagFilterField(model) + + +class IPSecProfileFilterForm(NetBoxModelFilterSetForm): + model = IPSecProfile + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + (_('Profile'), ('protocol', 'ike_version')), + (_('Phase 1 Parameters'), ('phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime')), + (_('Phase 2 Parameters'), ('phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime')), + ) + protocol = forms.MultipleChoiceField( + label=_('Protocol'), + choices=IPSecProtocolChoices, + required=False + ) + ike_version = forms.MultipleChoiceField( + label=_('IKE version'), + choices=IKEVersionChoices, + required=False + ) + ipsec_profile_id = DynamicModelMultipleChoiceField( + queryset=IPSecProfile.objects.all(), + required=False, + label=_('IPSec profile') + ) + phase1_encryption = forms.MultipleChoiceField( + label=_('Encryption'), + choices=EncryptionChoices, + required=False + ) + phase1_authentication = forms.MultipleChoiceField( + label=_('Authentication'), + choices=AuthenticationChoices, + required=False + ) + phase1_group = forms.MultipleChoiceField( + label=_('Group'), + choices=DHGroupChoices, + required=False + ) + phase1_sa_lifetime = forms.IntegerField( + required=False, + min_value=0, + label=_('SA lifetime') + ) + phase2_encryption = forms.MultipleChoiceField( + label=_('Encryption'), + choices=EncryptionChoices, + required=False + ) + phase2_authentication = forms.MultipleChoiceField( + label=_('Authentication'), + choices=AuthenticationChoices, + required=False + ) + phase2_group = forms.MultipleChoiceField( + label=_('Group'), + choices=DHGroupChoices, + required=False + ) + phase2_sa_lifetime = forms.IntegerField( + required=False, + min_value=0, + label=_('SA lifetime') + ) + tag = TagFilterField(model) diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py new file mode 100644 index 000000000..2621cdd46 --- /dev/null +++ b/netbox/vpn/forms/model_forms.py @@ -0,0 +1,96 @@ +from django import forms +from django.utils.translation import gettext_lazy as _ + +from dcim.models import Interface +from netbox.forms import NetBoxModelForm +from tenancy.forms import TenancyForm +from utilities.forms.fields import CommentField, DynamicModelChoiceField +from virtualization.models import VMInterface +from vpn.models import * + +__all__ = ( + 'IPSecProfileForm', + 'TunnelForm', + 'TunnelTerminationForm', +) + + +class TunnelForm(TenancyForm, NetBoxModelForm): + ipsec_profile = DynamicModelChoiceField( + queryset=IPSecProfile.objects.all() + ) + comments = CommentField() + + fieldsets = ( + (_('Tunnel'), ('name', 'status', 'encapsulation', 'description', 'tunnel_id', 'tags')), + (_('Security'), ('ipsec_profile', 'preshared_key')), + (_('Tenancy'), ('tenant_group', 'tenant')), + ) + + class Meta: + model = Tunnel + fields = [ + 'name', 'status', 'encapsulation', 'description', 'tunnel_id', 'ipsec_profile', 'preshared_key', + 'tenant_group', 'tenant', 'comments', 'tags', + ] + + +class TunnelTerminationForm(NetBoxModelForm): + tunnel = DynamicModelChoiceField( + queryset=Tunnel.objects.all() + ) + 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 = TunnelTermination + fields = [ + 'tunnel', 'role', 'outside_ip', 'tags', + ] + + def __init__(self, *args, **kwargs): + + # Initialize helper selectors + initial = kwargs.get('initial', {}).copy() + if instance := kwargs.get('instance'): + if type(instance.interface) is Interface: + initial['interface'] = instance.interface + elif type(instance.interface) is VMInterface: + initial['vminterface'] = instance.interface + kwargs['initial'] = initial + + super().__init__(*args, **kwargs) + + def clean(self): + super().clean() + + # Handle interface assignment + self.instance.interface = self.cleaned_data['interface'] or self.cleaned_data['interface'] or None + + +class IPSecProfileForm(NetBoxModelForm): + comments = CommentField() + + fieldsets = ( + (_('Profile'), ('name', 'protocol', 'ike_version', 'description', 'tags')), + (_('Phase 1 Parameters'), ('phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_sa_lifetime')), + (_('Phase 2 Parameters'), ('phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime')), + ) + + class Meta: + model = IPSecProfile + fields = [ + 'name', 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', + 'phase1_sa_lifetime', 'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_sa_lifetime', + 'description', 'comments', 'tags', + ] diff --git a/netbox/vpn/migrations/__init__.py b/netbox/vpn/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/models/__init__.py b/netbox/vpn/models/__init__.py new file mode 100644 index 000000000..3b70eb418 --- /dev/null +++ b/netbox/vpn/models/__init__.py @@ -0,0 +1,2 @@ +from .crypto import * +from .tunnels import * diff --git a/netbox/vpn/models/crypto.py b/netbox/vpn/models/crypto.py new file mode 100644 index 000000000..8e1ee2870 --- /dev/null +++ b/netbox/vpn/models/crypto.py @@ -0,0 +1,86 @@ +from django.db import models +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from netbox.models import PrimaryModel +from vpn.choices import * + +__all__ = ( + 'IPSecProfile', +) + + +class IPSecProfile(PrimaryModel): + name = models.CharField( + verbose_name=_('name'), + max_length=100, + unique=True + ) + protocol = models.CharField( + verbose_name=_('protocol'), + choices=IPSecProtocolChoices + ) + ike_version = models.PositiveSmallIntegerField( + verbose_name=_('IKE version'), + choices=IKEVersionChoices, + default=IKEVersionChoices.VERSION_2 + ) + + # Phase 1 parameters + phase1_encryption = models.CharField( + verbose_name=_('phase 1 encryption'), + choices=EncryptionChoices + ) + phase1_authentication = models.CharField( + verbose_name=_('phase 1 authentication'), + choices=AuthenticationChoices + ) + phase1_group = models.PositiveSmallIntegerField( + verbose_name=_('phase 1 group'), + choices=DHGroupChoices, + help_text=_('Diffie-Hellman group') + ) + phase1_sa_lifetime = models.PositiveSmallIntegerField( + verbose_name=_('phase 1 SA lifetime'), + blank=True, + null=True, + help_text=_('Security association lifetime (in seconds)') + ) + + # Phase 2 parameters + phase2_encryption = models.CharField( + verbose_name=_('phase 2 encryption'), + choices=EncryptionChoices + ) + phase2_authentication = models.CharField( + verbose_name=_('phase 2 authentication'), + choices=AuthenticationChoices + ) + phase2_group = models.PositiveSmallIntegerField( + verbose_name=_('phase 2 group'), + choices=DHGroupChoices, + help_text=_('Diffie-Hellman group') + ) + phase2_sa_lifetime = models.PositiveSmallIntegerField( + verbose_name=_('phase 2 SA lifetime'), + blank=True, + null=True, + help_text=_('Security association lifetime (in seconds)') + ) + # TODO: Add PFS group? + + clone_fields = ( + 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', 'phase1_as_lifetime', + 'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase2_as_lifetime', + ) + + class Meta: + ordering = ('name',) + verbose_name = _('tunnel') + verbose_name_plural = _('tunnels') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('vpn:ipsecprofile', args=[self.pk]) diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py new file mode 100644 index 000000000..4912ac3cd --- /dev/null +++ b/netbox/vpn/models/tunnels.py @@ -0,0 +1,115 @@ +from django.contrib.contenttypes.fields import GenericForeignKey +from django.db import models +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from netbox.models import ChangeLoggedModel, PrimaryModel +from netbox.models.features import CustomFieldsMixin, CustomLinksMixin, TagsMixin +from vpn.choices import * + +__all__ = ( + 'Tunnel', + 'TunnelTermination', +) + + +class Tunnel(PrimaryModel): + name = models.CharField( + verbose_name=_('name'), + max_length=100, + unique=True + ) + status = models.CharField( + verbose_name=_('status'), + max_length=50, + choices=TunnelStatusChoices, + default=TunnelStatusChoices.STATUS_ACTIVE + ) + encapsulation = models.CharField( + verbose_name=_('encapsulation'), + max_length=50, + choices=TunnelEncapsulationChoices + ) + ipsec_profile = models.ForeignKey( + to='vpn.IPSecProfile', + on_delete=models.PROTECT, + related_name='tunnels', + blank=True, + null=True + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='tunnels', + blank=True, + null=True + ) + preshared_key = models.TextField( + verbose_name=_('pre-shared key'), + blank=True + ) + tunnel_id = models.PositiveBigIntegerField( + verbose_name=_('tunnel ID'), + blank=True + ) + + clone_fields = ( + 'status', 'encapsulation', 'ipsec_profile', 'tenant', + ) + + class Meta: + ordering = ('name',) + verbose_name = _('tunnel') + verbose_name_plural = _('tunnels') + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('vpn:tunnel', args=[self.pk]) + + def get_status_color(self): + return TunnelStatusChoices.colors.get(self.status) + + +class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLoggedModel): + tunnel = models.ForeignKey( + to='vpn.Tunnel', + on_delete=models.CASCADE, + related_name='terminations' + ) + role = models.CharField( + verbose_name=_('role'), + max_length=50, + choices=TunnelTerminationRoleChoices, + default=TunnelTerminationRoleChoices.ROLE_PEER + ) + interface_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.PROTECT, + related_name='+' + ) + interface_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + interface = GenericForeignKey( + ct_field='interface_type', + fk_field='interface_id' + ) + outside_ip = models.OneToOneField( + to='ipam.IPAddress', + on_delete=models.PROTECT, + related_name='tunnel_termination' + ) + + class Meta: + ordering = ('tunnel', 'pk') + verbose_name = _('tunnel termination') + verbose_name_plural = _('tunnel terminations') + + def __str__(self): + return f'{self.tunnel}: Termination {self.pk}' + + def get_absolute_url(self): + return self.tunnel.get_absolute_url() diff --git a/netbox/vpn/search.py b/netbox/vpn/search.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/tables.py b/netbox/vpn/tables.py new file mode 100644 index 000000000..4f8b08066 --- /dev/null +++ b/netbox/vpn/tables.py @@ -0,0 +1,124 @@ +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 * + +__all__ = ( + 'IPSecProfileTable', + 'TunnelTable', + 'TunnelTerminationTable', +) + + +class TunnelTable(TenancyColumnsMixin, NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + status = columns.ChoiceFieldColumn( + verbose_name=_('Status') + ) + encapsulation = columns.ChoiceFieldColumn( + verbose_name=_('Encapsulation') + ) + 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', 'preshared_key', + 'tunnel_id', 'termination_count', 'description', 'comments', 'tags', 'created', 'last_updated', + ) + default_columns = ('pk', 'name', 'status', 'encapsulation', 'tenant', 'termination_count') + + +class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable): + tunnel = tables.Column( + verbose_name=_('Tunnel'), + linkify=True + ) + role = columns.ChoiceFieldColumn( + verbose_name=_('Role') + ) + interface = tables.Column( + verbose_name=_('Interface'), + linkify=True + ) + 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', 'outside_ip', 'tags', 'created', 'last_updated', + ) + default_columns = ('pk', 'tunnel', 'role', 'interface', 'outside_ip') + + +class IPSecProfileTable(TenancyColumnsMixin, NetBoxTable): + name = tables.Column( + verbose_name=_('Name'), + linkify=True + ) + protocol = columns.ChoiceFieldColumn( + verbose_name=_('Protocol') + ) + ike_version = columns.ChoiceFieldColumn( + verbose_name=_('IKE Version') + ) + phase1_encryption = columns.ChoiceFieldColumn( + verbose_name=_('Phase 1 Encryption') + ) + phase1_authentication = columns.ChoiceFieldColumn( + verbose_name=_('Phase 1 Authentication') + ) + phase1_group = columns.ChoiceFieldColumn( + verbose_name=_('Phase 1 Group') + ) + phase2_encryption = columns.ChoiceFieldColumn( + verbose_name=_('Phase 2 Encryption') + ) + phase2_authentication = columns.ChoiceFieldColumn( + verbose_name=_('Phase 2 Authentication') + ) + phase2_group = columns.ChoiceFieldColumn( + verbose_name=_('Phase 2 Group') + ) + comments = columns.MarkdownColumn( + verbose_name=_('Comments'), + ) + tags = columns.TagColumn( + url_name='vpn:tunnel_list' + ) + + class Meta(NetBoxTable.Meta): + model = IPSecProfile + fields = ( + 'pk', 'id', 'name', 'protocol', 'ike_version', 'phase1_encryption', 'phase1_authentication', 'phase1_group', + 'phase1_sa_lifetime', 'phase2_encryption', 'phase2_authentication', 'phase2_group', 'phase1_sa_lifetime', + 'description', 'comments', 'tags', 'created', 'last_updated', + ) + default_columns = ('pk', 'name', 'protocol', 'ike_version', 'description') diff --git a/netbox/vpn/urls.py b/netbox/vpn/urls.py new file mode 100644 index 000000000..bfb348a14 --- /dev/null +++ b/netbox/vpn/urls.py @@ -0,0 +1,33 @@ +from django.urls import include, path + +from utilities.urls import get_model_urls +from . import views + +app_name = 'vpn' +urlpatterns = [ + + # Tunnels + path('tunnels/', views.TunnelListView.as_view(), name='tunnel_list'), + path('tunnels/add/', views.TunnelEditView.as_view(), name='tunnel_add'), + path('tunnels/import/', views.TunnelBulkImportView.as_view(), name='tunnel_import'), + path('tunnels/edit/', views.TunnelBulkEditView.as_view(), name='tunnel_bulk_edit'), + path('tunnels/delete/', views.TunnelBulkDeleteView.as_view(), name='tunnel_bulk_delete'), + path('tunnels//', include(get_model_urls('vpn', 'tunnel'))), + + # Tunnel terminations + path('tunnel-terminations/', views.TunnelTerminationListView.as_view(), name='tunneltermination_list'), + path('tunnel-terminations/add/', views.TunnelTerminationEditView.as_view(), name='tunneltermination_add'), + path('tunnel-terminations/import/', views.TunnelTerminationBulkImportView.as_view(), name='tunneltermination_import'), + path('tunnel-terminations/edit/', views.TunnelTerminationBulkEditView.as_view(), name='tunneltermination_bulk_edit'), + path('tunnel-terminations/delete/', views.TunnelTerminationBulkDeleteView.as_view(), name='tunneltermination_bulk_delete'), + path('tunnel-terminations//', include(get_model_urls('vpn', 'tunneltermination'))), + + # IPSec profiles + path('ipsec-profiles/', views.IPSecProfileListView.as_view(), name='ipsecprofile_list'), + path('ipsec-profiles/add/', views.IPSecProfileEditView.as_view(), name='ipsecprofile_add'), + path('ipsec-profiles/import/', views.IPSecProfileBulkImportView.as_view(), name='ipsecprofile_import'), + path('ipsec-profiles/edit/', views.IPSecProfileBulkEditView.as_view(), name='ipsecprofile_bulk_edit'), + path('ipsec-profiles/delete/', views.IPSecProfileBulkDeleteView.as_view(), name='ipsecprofile_bulk_delete'), + path('ipsec-profiles//', include(get_model_urls('vpn', 'ipsecprofile'))), + +] diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py new file mode 100644 index 000000000..58034391a --- /dev/null +++ b/netbox/vpn/views.py @@ -0,0 +1,146 @@ +from netbox.views import generic +from utilities.utils import count_related +from utilities.views import register_model_view +from . import filtersets, forms, tables +from .models import * + + +# +# Tunnels +# + +class TunnelListView(generic.ObjectListView): + queryset = Tunnel.objects.annotate( + count_terminations=count_related(TunnelTermination, 'tunnel') + ) + filterset = filtersets.TunnelFilterSet + filterset_form = forms.TunnelFilterForm + table = tables.TunnelTable + + +@register_model_view(Tunnel) +class TunnelView(generic.ObjectView): + queryset = Tunnel.objects.all() + + +@register_model_view(Tunnel, 'edit') +class TunnelEditView(generic.ObjectEditView): + queryset = Tunnel.objects.all() + form = forms.TunnelForm + + +@register_model_view(Tunnel, 'delete') +class TunnelDeleteView(generic.ObjectDeleteView): + queryset = Tunnel.objects.all() + + +class TunnelBulkImportView(generic.BulkImportView): + queryset = Tunnel.objects.all() + model_form = forms.TunnelImportForm + + +class TunnelBulkEditView(generic.BulkEditView): + queryset = Tunnel.objects.annotate( + count_terminations=count_related(TunnelTermination, 'tunnel') + ) + filterset = filtersets.TunnelFilterSet + table = tables.TunnelTable + form = forms.TunnelBulkEditForm + + +class TunnelBulkDeleteView(generic.BulkDeleteView): + queryset = Tunnel.objects.annotate( + count_terminations=count_related(TunnelTermination, 'tunnel') + ) + filterset = filtersets.TunnelFilterSet + table = tables.TunnelTable + + +# +# Tunnel terminations +# + +class TunnelTerminationListView(generic.ObjectListView): + queryset = TunnelTermination.objects.all() + filterset = filtersets.TunnelTerminationFilterSet + filterset_form = forms.TunnelTerminationFilterForm + table = tables.TunnelTerminationTable + + +@register_model_view(TunnelTermination) +class TunnelTerminationView(generic.ObjectView): + queryset = TunnelTermination.objects.all() + + +@register_model_view(TunnelTermination, 'edit') +class TunnelTerminationEditView(generic.ObjectEditView): + queryset = TunnelTermination.objects.all() + form = forms.TunnelTerminationForm + + +@register_model_view(TunnelTermination, 'delete') +class TunnelTerminationDeleteView(generic.ObjectDeleteView): + queryset = TunnelTermination.objects.all() + + +class TunnelTerminationBulkImportView(generic.BulkImportView): + queryset = TunnelTermination.objects.all() + model_form = forms.TunnelTerminationImportForm + + +class TunnelTerminationBulkEditView(generic.BulkEditView): + queryset = TunnelTermination.objects.all() + filterset = filtersets.TunnelTerminationFilterSet + table = tables.TunnelTerminationTable + form = forms.TunnelTerminationBulkEditForm + + +class TunnelTerminationBulkDeleteView(generic.BulkDeleteView): + queryset = TunnelTermination.objects.all() + filterset = filtersets.TunnelTerminationFilterSet + table = tables.TunnelTerminationTable + + +# +# IPSec profiles +# + +class IPSecProfileListView(generic.ObjectListView): + queryset = IPSecProfile.objects.all() + filterset = filtersets.IPSecProfileFilterSet + filterset_form = forms.IPSecProfileFilterForm + table = tables.IPSecProfileTable + + +@register_model_view(IPSecProfile) +class IPSecProfileView(generic.ObjectView): + queryset = IPSecProfile.objects.all() + + +@register_model_view(IPSecProfile, 'edit') +class IPSecProfileEditView(generic.ObjectEditView): + queryset = IPSecProfile.objects.all() + form = forms.IPSecProfileForm + + +@register_model_view(IPSecProfile, 'delete') +class IPSecProfileDeleteView(generic.ObjectDeleteView): + queryset = IPSecProfile.objects.all() + + +class IPSecProfileBulkImportView(generic.BulkImportView): + queryset = IPSecProfile.objects.all() + model_form = forms.IPSecProfileImportForm + + +class IPSecProfileBulkEditView(generic.BulkEditView): + queryset = IPSecProfile.objects.all() + filterset = filtersets.IPSecProfileFilterSet + table = tables.IPSecProfileTable + form = forms.IPSecProfileBulkEditForm + + +class IPSecProfileBulkDeleteView(generic.BulkDeleteView): + queryset = IPSecProfile.objects.all() + filterset = filtersets.IPSecProfileFilterSet + table = tables.IPSecProfileTable