diff --git a/netbox/templates/vpn/tunneltermination.html b/netbox/templates/vpn/tunneltermination.html
new file mode 100644
index 000000000..178b97ef5
--- /dev/null
+++ b/netbox/templates/vpn/tunneltermination.html
@@ -0,0 +1,55 @@
+{% extends 'generic/object.html' %}
+{% load helpers %}
+{% load plugins %}
+{% load i18n %}
+
+{% block content %}
+
+
+
+
+
+
+
+ {% trans "Tunnel" %} |
+ {{ object.tunnel|linkify }} |
+
+
+ {% trans "Role" %} |
+ {{ object.get_role_display }} |
+
+
+
+ {% if object.interface.device %}
+ {% trans "Device" %}
+ {% elif object.interface.virtual_machine %}
+ {% trans "Virtual Machine" %}
+ {% endif %}
+ |
+ {{ object.interface.parent_object|linkify }} |
+
+
+ {% trans "Interface" %} |
+ {{ object.interface|linkify }} |
+
+
+ {% trans "Outside IP" %} |
+ {{ object.outside_ip|linkify|placeholder }} |
+
+
+
+
+ {% plugin_left_page object %}
+
+
+ {% include 'inc/panels/custom_fields.html' %}
+ {% include 'inc/panels/tags.html' %}
+ {% plugin_right_page object %}
+
+
+
+
+ {% plugin_full_width_page object %}
+
+
+{% endblock %}
diff --git a/netbox/vpn/forms/bulk_import.py b/netbox/vpn/forms/bulk_import.py
index d961b156c..a815aa10b 100644
--- a/netbox/vpn/forms/bulk_import.py
+++ b/netbox/vpn/forms/bulk_import.py
@@ -4,7 +4,7 @@ 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 utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, CSVModelMultipleChoiceField
from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import *
from vpn.models import *
@@ -34,6 +34,7 @@ class TunnelImportForm(NetBoxModelImportForm):
ipsec_profile = CSVModelChoiceField(
label=_('IPSec profile'),
queryset=IPSecProfile.objects.all(),
+ required=False,
to_field_name='name'
)
tenant = CSVModelChoiceField(
@@ -87,6 +88,7 @@ class TunnelTerminationImportForm(NetBoxModelImportForm):
outside_ip = CSVModelChoiceField(
label=_('Outside IP'),
queryset=IPAddress.objects.all(),
+ required=False,
to_field_name='name'
)
@@ -111,6 +113,14 @@ class TunnelTerminationImportForm(NetBoxModelImportForm):
**{f"virtual_machine__{self.fields['virtual_machine'].to_field_name}": data['virtual_machine']}
)
+ def save(self, *args, **kwargs):
+
+ # Set interface assignment
+ if self.cleaned_data.get('interface'):
+ self.instance.interface = self.cleaned_data['interface']
+
+ return super().save(*args, **kwargs)
+
class IKEProposalImportForm(NetBoxModelImportForm):
authentication_method = CSVChoiceField(
@@ -121,7 +131,7 @@ class IKEProposalImportForm(NetBoxModelImportForm):
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices
)
- authentication_algorithmn = CSVChoiceField(
+ authentication_algorithm = CSVChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices
)
@@ -133,7 +143,7 @@ class IKEProposalImportForm(NetBoxModelImportForm):
class Meta:
model = IKEProposal
fields = (
- 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithmn',
+ 'name', 'description', 'authentication_method', 'encryption_algorithm', 'authentication_algorithm',
'group', 'sa_lifetime', 'tags',
)
@@ -147,7 +157,11 @@ class IKEPolicyImportForm(NetBoxModelImportForm):
label=_('Mode'),
choices=IKEModeChoices
)
- # TODO: M2M field for proposals
+ proposals = CSVModelMultipleChoiceField(
+ queryset=IKEProposal.objects.all(),
+ to_field_name='name',
+ help_text=_('IKE proposal(s)'),
+ )
class Meta:
model = IKEPolicy
@@ -161,7 +175,7 @@ class IPSecProposalImportForm(NetBoxModelImportForm):
label=_('Encryption algorithm'),
choices=EncryptionAlgorithmChoices
)
- authentication_algorithmn = CSVChoiceField(
+ authentication_algorithm = CSVChoiceField(
label=_('Authentication algorithm'),
choices=AuthenticationAlgorithmChoices
)
@@ -169,7 +183,7 @@ class IPSecProposalImportForm(NetBoxModelImportForm):
class Meta:
model = IPSecProposal
fields = (
- 'name', 'description', 'encryption_algorithm', 'authentication_algorithmn', 'sa_lifetime_seconds',
+ 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', 'sa_lifetime_seconds',
'sa_lifetime_data', 'tags',
)
@@ -179,7 +193,11 @@ class IPSecPolicyImportForm(NetBoxModelImportForm):
label=_('PFS group'),
choices=DHGroupChoices
)
- # TODO: M2M field for proposals
+ proposals = CSVModelMultipleChoiceField(
+ queryset=IPSecProposal.objects.all(),
+ to_field_name='name',
+ help_text=_('IPSec proposal(s)'),
+ )
class Meta:
model = IPSecPolicy
diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py
index 49b9dab24..79d029172 100644
--- a/netbox/vpn/forms/model_forms.py
+++ b/netbox/vpn/forms/model_forms.py
@@ -6,6 +6,7 @@ from ipam.models import IPAddress
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
+from utilities.forms.utils import add_blank_choice
from utilities.forms.widgets import HTMXSelect
from virtualization.models import VirtualMachine, VMInterface
from vpn.choices import *
@@ -20,7 +21,6 @@ __all__ = (
'TunnelCreateForm',
'TunnelForm',
'TunnelTerminationForm',
- 'TunnelTerminationCreateForm',
)
@@ -49,21 +49,25 @@ class TunnelForm(TenancyForm, NetBoxModelForm):
class TunnelCreateForm(TunnelForm):
# First termination
termination1_role = forms.ChoiceField(
- choices=TunnelTerminationRoleChoices,
+ choices=add_blank_choice(TunnelTerminationRoleChoices),
+ required=False,
label=_('Role')
)
termination1_type = forms.ChoiceField(
choices=TunnelTerminationTypeChoices,
+ required=False,
widget=HTMXSelect(),
label=_('Type')
)
termination1_parent = DynamicModelChoiceField(
queryset=Device.objects.all(),
+ required=False,
selector=True,
label=_('Device')
)
termination1_interface = DynamicModelChoiceField(
queryset=Interface.objects.all(),
+ required=False,
label=_('Interface'),
query_params={
'device_id': '$termination1_parent',
@@ -80,7 +84,7 @@ class TunnelCreateForm(TunnelForm):
# Second termination
termination2_role = forms.ChoiceField(
- choices=TunnelTerminationRoleChoices,
+ choices=add_blank_choice(TunnelTerminationRoleChoices),
required=False,
label=_('Role')
)
@@ -155,34 +159,36 @@ class TunnelCreateForm(TunnelForm):
def clean(self):
super().clean()
- # Check that all required parameters have been set for the second termination (if any)
- termination2_required_parameters = (
- 'termination2_role', 'termination2_type', 'termination2_parent', 'termination2_interface',
- )
- termination2_parameters = (
- *termination2_required_parameters,
- 'termination2_outside_ip',
- )
- if any([self.cleaned_data[param] for param in termination2_parameters]):
- for param in termination2_required_parameters:
+ # Validate attributes for each termination (if any)
+ for term in ('termination1', 'termination2'):
+ required_parameters = (
+ f'{term}_role', f'{term}_parent', f'{term}_interface',
+ )
+ parameters = (
+ *required_parameters,
+ f'{term}_outside_ip',
+ )
+ if any([self.cleaned_data[param] for param in parameters]):
+ for param in required_parameters:
if not self.cleaned_data[param]:
raise forms.ValidationError({
- param: _("This parameter is required when defining a second termination.")
+ param: _("This parameter is required when defining a termination.")
})
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Create first termination
- TunnelTermination.objects.create(
- tunnel=instance,
- role=self.cleaned_data['termination1_role'],
- interface=self.cleaned_data['termination1_interface'],
- outside_ip=self.cleaned_data['termination1_outside_ip'],
- )
+ if self.cleaned_data['termination1_interface']:
+ TunnelTermination.objects.create(
+ tunnel=instance,
+ role=self.cleaned_data['termination1_role'],
+ interface=self.cleaned_data['termination1_interface'],
+ outside_ip=self.cleaned_data['termination1_outside_ip'],
+ )
# Create second termination, if defined
- if self.cleaned_data['termination2_role']:
+ if self.cleaned_data['termination2_interface']:
TunnelTermination.objects.create(
tunnel=instance,
role=self.cleaned_data['termination2_role'],
@@ -194,20 +200,6 @@ class TunnelCreateForm(TunnelForm):
class TunnelTerminationForm(NetBoxModelForm):
- outside_ip = DynamicModelChoiceField(
- queryset=IPAddress.objects.all(),
- required=False,
- label=_('Outside IP')
- )
-
- class Meta:
- model = TunnelTermination
- fields = [
- 'role', 'outside_ip', 'tags',
- ]
-
-
-class TunnelTerminationCreateForm(NetBoxModelForm):
tunnel = DynamicModelChoiceField(
queryset=Tunnel.objects.all()
)
@@ -261,11 +253,15 @@ class TunnelTerminationCreateForm(NetBoxModelForm):
'virtual_machine_id': '$parent',
})
+ if self.instance.pk:
+ self.fields['parent'].initial = self.instance.interface.parent_object
+ self.fields['interface'].initial = self.instance.interface
+
def clean(self):
super().clean()
# Assign the interface
- self.instance.interface = self.cleaned_data['interface']
+ self.instance.interface = self.cleaned_data.get('interface')
class IKEProposalForm(NetBoxModelForm):
diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py
index 9f696dbbd..6603de87c 100644
--- a/netbox/vpn/models/tunnels.py
+++ b/netbox/vpn/models/tunnels.py
@@ -119,7 +119,7 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
return f'{self.tunnel}: Termination {self.pk}'
def get_absolute_url(self):
- return self.tunnel.get_absolute_url()
+ return reverse('vpn:tunneltermination', args=[self.pk])
def get_role_color(self):
return TunnelTerminationRoleChoices.colors.get(self.role)
@@ -128,7 +128,7 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
super().clean()
# Check that the selected Interface is not already attached to a Tunnel
- if self.interface.tunnel_termination and self.interface.tunnel_termination.pk != self.pk:
+ if getattr(self.interface, 'tunnel_termination', None) and self.interface.tunnel_termination.pk != self.pk:
raise ValidationError({
'interface': _("Interface {name} is already attached to a tunnel ({tunnel}).").format(
name=self.interface.name,
diff --git a/netbox/vpn/tables.py b/netbox/vpn/tables.py
index ab1642969..b375674c9 100644
--- a/netbox/vpn/tables.py
+++ b/netbox/vpn/tables.py
@@ -89,7 +89,7 @@ class TunnelTerminationTable(TenancyColumnsMixin, NetBoxTable):
'pk', 'id', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip', 'tags',
'created', 'last_updated',
)
- default_columns = ('pk', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip')
+ default_columns = ('pk', 'id', 'tunnel', 'role', 'interface_parent', 'interface', 'ip_addresses', 'outside_ip')
class IKEProposalTable(NetBoxTable):
diff --git a/netbox/vpn/tests/test_views.py b/netbox/vpn/tests/test_views.py
new file mode 100644
index 000000000..88803d921
--- /dev/null
+++ b/netbox/vpn/tests/test_views.py
@@ -0,0 +1,509 @@
+from django.contrib.contenttypes.models import ContentType
+
+from dcim.choices import InterfaceTypeChoices
+from dcim.models import Interface
+from vpn.choices import *
+from vpn.models import *
+from utilities.testing import ViewTestCases, create_tags, create_test_device
+
+
+class TunnelTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = Tunnel
+
+ @classmethod
+ def setUpTestData(cls):
+
+ tunnels = (
+ Tunnel(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 2',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ Tunnel(
+ name='Tunnel 3',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ ),
+ )
+ Tunnel.objects.bulk_create(tunnels)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'Tunnel X',
+ 'description': 'New tunnel',
+ 'status': TunnelStatusChoices.STATUS_PLANNED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,status,encapsulation",
+ "Tunnel 4,planned,gre",
+ "Tunnel 5,planned,gre",
+ "Tunnel 6,planned,gre",
+ )
+
+ cls.csv_update_data = (
+ "id,status,encapsulation",
+ f"{tunnels[0].pk},active,ip-ip",
+ f"{tunnels[1].pk},active,ip-ip",
+ f"{tunnels[2].pk},active,ip-ip",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'status': TunnelStatusChoices.STATUS_DISABLED,
+ 'encapsulation': TunnelEncapsulationChoices.ENCAP_GRE,
+ }
+
+
+class TunnelTerminationTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = TunnelTermination
+
+ @classmethod
+ def setUpTestData(cls):
+ device = create_test_device('Device 1')
+ interfaces = (
+ Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 5', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 6', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ Interface(device=device, name='Interface 7', type=InterfaceTypeChoices.TYPE_VIRTUAL),
+ )
+ Interface.objects.bulk_create(interfaces)
+
+ tunnel = Tunnel.objects.create(
+ name='Tunnel 1',
+ status=TunnelStatusChoices.STATUS_ACTIVE,
+ encapsulation=TunnelEncapsulationChoices.ENCAP_IP_IP
+ )
+
+ tunnel_terminations = (
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_HUB,
+ interface=interfaces[0]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ interface=interfaces[1]
+ ),
+ TunnelTermination(
+ tunnel=tunnel,
+ role=TunnelTerminationRoleChoices.ROLE_SPOKE,
+ interface=interfaces[2]
+ ),
+ )
+ TunnelTermination.objects.bulk_create(tunnel_terminations)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'tunnel': tunnel.pk,
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ 'type': TunnelTerminationTypeChoices.TYPE_DEVICE,
+ 'parent': device.pk,
+ # TODO: Solve for GFK validation
+ 'interface': interfaces[6].pk,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "tunnel,role,device,interface",
+ "Tunnel 1,peer,Device 1,Interface 4",
+ "Tunnel 1,peer,Device 1,Interface 5",
+ "Tunnel 1,peer,Device 1,Interface 6",
+ )
+
+ cls.csv_update_data = (
+ "id,role",
+ f"{tunnel_terminations[0].pk},peer",
+ f"{tunnel_terminations[1].pk},peer",
+ f"{tunnel_terminations[2].pk},peer",
+ )
+
+ cls.bulk_edit_data = {
+ 'role': TunnelTerminationRoleChoices.ROLE_PEER,
+ }
+
+
+class IKEProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IKEProposal
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 3',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IKE Proposal X',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,authentication_method,encryption_algorithm,authentication_algorithm,group",
+ "IKE Proposal 4,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ "IKE Proposal 5,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ "IKE Proposal 6,preshared-keys,aes-128-cbc,hmac-sha1,14",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ike_proposals[0].pk},New description",
+ f"{ike_proposals[1].pk},New description",
+ f"{ike_proposals[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'authentication_method': AuthenticationMethodChoices.CERTIFICATES,
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'group': DHGroupChoices.GROUP_19
+ }
+
+
+class IKEPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IKEPolicy
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposals = (
+ IKEProposal(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ IKEProposal(
+ name='IKE Proposal 2',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IKEProposal.objects.bulk_create(ike_proposals)
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 3',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.set(ike_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IKE Policy X',
+ 'version': IKEVersionChoices.VERSION_2,
+ 'mode': IKEModeChoices.AGGRESSIVE,
+ 'proposals': [p.pk for p in ike_proposals],
+ 'tags': [t.pk for t in tags],
+ }
+
+ ike_proposal_names = ','.join([p.name for p in ike_proposals])
+ cls.csv_data = (
+ "name,version,mode,proposals",
+ f"IKE Proposal 4,2,aggressive,\"{ike_proposal_names}\"",
+ f"IKE Proposal 5,2,aggressive,\"{ike_proposal_names}\"",
+ f"IKE Proposal 6,2,aggressive,\"{ike_proposal_names}\"",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ike_policies[0].pk},New description",
+ f"{ike_policies[1].pk},New description",
+ f"{ike_policies[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'version': IKEVersionChoices.VERSION_2,
+ 'mode': IKEModeChoices.AGGRESSIVE,
+ }
+
+
+class IPSecProposalTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecProposal
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 3',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Proposal X',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'sa_lifetime_seconds': 3600,
+ 'sa_lifetime_data': 1000000,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,encryption_algorithm,authentication_algorithm,sa_lifetime_seconds,sa_lifetime_data",
+ "IKE Proposal 4,aes-128-cbc,hmac-sha1,3600,1000000",
+ "IKE Proposal 5,aes-128-cbc,hmac-sha1,3600,1000000",
+ "IKE Proposal 6,aes-128-cbc,hmac-sha1,3600,1000000",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_proposals[0].pk},New description",
+ f"{ipsec_proposals[1].pk},New description",
+ f"{ipsec_proposals[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'encryption_algorithm': EncryptionAlgorithmChoices.ENCRYPTION_AES192_CBC,
+ 'authentication_algorithm': AuthenticationAlgorithmChoices.AUTH_HMAC_SHA256,
+ 'sa_lifetime_seconds': 3600,
+ 'sa_lifetime_data': 1000000,
+ }
+
+
+class IPSecPolicyTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecPolicy
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ipsec_proposals = (
+ IPSecProposal(
+ name='IPSec Policy 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ IPSecProposal(
+ name='IPSec Proposal 2',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ ),
+ )
+ IPSecProposal.objects.bulk_create(ipsec_proposals)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 3',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.set(ipsec_proposals)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Policy X',
+ 'pfs_group': DHGroupChoices.GROUP_5,
+ 'proposals': [p.pk for p in ipsec_proposals],
+ 'tags': [t.pk for t in tags],
+ }
+
+ ipsec_proposal_names = ','.join([p.name for p in ipsec_proposals])
+ cls.csv_data = (
+ "name,pfs_group,proposals",
+ f"IKE Proposal 4,19,\"{ipsec_proposal_names}\"",
+ f"IKE Proposal 5,19,\"{ipsec_proposal_names}\"",
+ f"IKE Proposal 6,19,\"{ipsec_proposal_names}\"",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_policies[0].pk},New description",
+ f"{ipsec_policies[1].pk},New description",
+ f"{ipsec_policies[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'pfs_group': DHGroupChoices.GROUP_5,
+ }
+
+
+class IPSecProfileTestCase(ViewTestCases.PrimaryObjectViewTestCase):
+ model = IPSecProfile
+
+ @classmethod
+ def setUpTestData(cls):
+
+ ike_proposal = IKEProposal.objects.create(
+ name='IKE Proposal 1',
+ authentication_method=AuthenticationMethodChoices.PRESHARED_KEYS,
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1,
+ group=DHGroupChoices.GROUP_14
+ )
+
+ ipsec_proposal = IPSecProposal.objects.create(
+ name='IPSec Proposal 1',
+ encryption_algorithm=EncryptionAlgorithmChoices.ENCRYPTION_AES128_CBC,
+ authentication_algorithm=AuthenticationAlgorithmChoices.AUTH_HMAC_SHA1
+ )
+
+ ike_policies = (
+ IKEPolicy(
+ name='IKE Policy 1',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ IKEPolicy(
+ name='IKE Policy 2',
+ version=IKEVersionChoices.VERSION_1,
+ mode=IKEModeChoices.MAIN,
+ ),
+ )
+ IKEPolicy.objects.bulk_create(ike_policies)
+ for ike_policy in ike_policies:
+ ike_policy.proposals.add(ike_proposal)
+
+ ipsec_policies = (
+ IPSecPolicy(
+ name='IPSec Policy 1',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ IPSecPolicy(
+ name='IPSec Policy 2',
+ pfs_group=DHGroupChoices.GROUP_14
+ ),
+ )
+ IPSecPolicy.objects.bulk_create(ipsec_policies)
+ for ipsec_policy in ipsec_policies:
+ ipsec_policy.proposals.add(ipsec_proposal)
+
+ ipsec_profiles = (
+ IPSecProfile(
+ name='IPSec Profile 1',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 2',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ IPSecProfile(
+ name='IPSec Profile 3',
+ mode=IPSecModeChoices.ESP,
+ ike_policy=ike_policies[0],
+ ipsec_policy=ipsec_policies[0]
+ ),
+ )
+ IPSecProfile.objects.bulk_create(ipsec_profiles)
+
+ tags = create_tags('Alpha', 'Bravo', 'Charlie')
+
+ cls.form_data = {
+ 'name': 'IPSec Profile X',
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ 'tags': [t.pk for t in tags],
+ }
+
+ cls.csv_data = (
+ "name,mode,ike_policy,ipsec_policy",
+ f"IKE Proposal 4,ah,IKE Policy 2,IPSec Policy 2",
+ f"IKE Proposal 5,ah,IKE Policy 2,IPSec Policy 2",
+ f"IKE Proposal 6,ah,IKE Policy 2,IPSec Policy 2",
+ )
+
+ cls.csv_update_data = (
+ "id,description",
+ f"{ipsec_profiles[0].pk},New description",
+ f"{ipsec_profiles[1].pk},New description",
+ f"{ipsec_profiles[2].pk},New description",
+ )
+
+ cls.bulk_edit_data = {
+ 'description': 'New description',
+ 'mode': IPSecModeChoices.AH,
+ 'ike_policy': ike_policies[1].pk,
+ 'ipsec_policy': ipsec_policies[1].pk,
+ }
diff --git a/netbox/vpn/views.py b/netbox/vpn/views.py
index fe6acb46a..56eadc077 100644
--- a/netbox/vpn/views.py
+++ b/netbox/vpn/views.py
@@ -75,19 +75,16 @@ class TunnelTerminationListView(generic.ObjectListView):
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
- def dispatch(self, request, *args, **kwargs):
-
- # If creating a new Tunnel, use the creation form
- if 'pk' not in kwargs:
- self.form = forms.TunnelTerminationCreateForm
-
- return super().dispatch(request, *args, **kwargs)
-
@register_model_view(TunnelTermination, 'delete')
class TunnelTerminationDeleteView(generic.ObjectDeleteView):