diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index e0f38afef..a24f9ea6d 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -359,6 +359,16 @@ INTERFACE_BUTTONS = """
{% endif %}
+{% elif record.type == 'virtual' %}
+ {% if perms.vpn.add_tunnel and not record.tunnel_termination %}
+
+
+
+ {% elif perms.vpn.delete_tunneltermination and record.tunnel_termination %}
+
+
+
+ {% endif %}
{% elif record.is_wired and perms.dcim.add_cable %}
diff --git a/netbox/vpn/forms/model_forms.py b/netbox/vpn/forms/model_forms.py
index 34976a62a..cc8cb4b1f 100644
--- a/netbox/vpn/forms/model_forms.py
+++ b/netbox/vpn/forms/model_forms.py
@@ -257,6 +257,12 @@ class TunnelTerminationCreateForm(NetBoxModelForm):
'virtual_machine_id': '$parent',
})
+ def clean(self):
+ super().clean()
+
+ # Assign the interface
+ self.instance.interface = self.cleaned_data['interface']
+
class IPSecProfileForm(NetBoxModelForm):
comments = CommentField()
diff --git a/netbox/vpn/models/tunnels.py b/netbox/vpn/models/tunnels.py
index b07ac3ab4..4f3bd4110 100644
--- a/netbox/vpn/models/tunnels.py
+++ b/netbox/vpn/models/tunnels.py
@@ -1,4 +1,5 @@
-from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.contenttypes.fields import GenericForeignKey
+from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -127,6 +128,18 @@ class TunnelTermination(CustomFieldsMixin, CustomLinksMixin, TagsMixin, ChangeLo
def get_role_color(self):
return TunnelTerminationRoleChoices.colors.get(self.role)
+ def clean(self):
+ super().clean()
+
+ # Check that the selected Interface is not already attached to a Tunnel
+ if self.interface.tunnel_termination:
+ raise ValidationError({
+ 'interface': _("Interface {name} is already attached to a tunnel ({tunnel}).").format(
+ name=self.interface.name,
+ tunnel=self.interface.tunnel_termination.tunnel
+ )
+ })
+
def to_objectchange(self, action):
objectchange = super().to_objectchange(action)
objectchange.related_object = self.tunnel