From c814e763a1e76d72598b556c5ab5ca5cdfc948e1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 21 Nov 2023 16:04:33 -0500 Subject: [PATCH] Add UI view tests --- netbox/templates/vpn/tunneltermination.html | 55 +++ netbox/vpn/forms/bulk_import.py | 32 +- netbox/vpn/forms/model_forms.py | 68 ++- netbox/vpn/models/tunnels.py | 4 +- netbox/vpn/tables.py | 2 +- netbox/vpn/tests/test_views.py | 509 ++++++++++++++++++++ netbox/vpn/views.py | 13 +- 7 files changed, 629 insertions(+), 54 deletions(-) create mode 100644 netbox/templates/vpn/tunneltermination.html create mode 100644 netbox/vpn/tests/test_views.py 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 Termination" %}
+
+ + + + + + + + + + + + + + + + + + + + + +
{% 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):