diff --git a/netbox/circuits/models/virtual_circuits.py b/netbox/circuits/models/virtual_circuits.py
index d9f8c88f3..ab43839d6 100644
--- a/netbox/circuits/models/virtual_circuits.py
+++ b/netbox/circuits/models/virtual_circuits.py
@@ -1,3 +1,5 @@
+from functools import cached_property
+
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
@@ -139,3 +141,18 @@ class VirtualCircuitTermination(
@property
def parent_object(self):
return self.virtual_circuit
+
+ @cached_property
+ def peer_terminations(self):
+ if self.role == VirtualCircuitTerminationRoleChoices.ROLE_PEER:
+ return self.virtual_circuit.terminations.exclude(pk=self.pk).filter(
+ role=VirtualCircuitTerminationRoleChoices.ROLE_PEER
+ )
+ if self.role == VirtualCircuitTerminationRoleChoices.ROLE_HUB:
+ return self.virtual_circuit.terminations.filter(
+ role=VirtualCircuitTerminationRoleChoices.ROLE_SPOKE
+ )
+ if self.role == VirtualCircuitTerminationRoleChoices.ROLE_SPOKE:
+ return self.virtual_circuit.terminations.filter(
+ role=VirtualCircuitTerminationRoleChoices.ROLE_HUB
+ )
diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py
index 36fd02add..ce5974d17 100644
--- a/netbox/dcim/models/device_components.py
+++ b/netbox/dcim/models/device_components.py
@@ -980,6 +980,14 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
def l2vpn_termination(self):
return self.l2vpn_terminations.first()
+ @cached_property
+ def connected_endpoints(self):
+ # If this is a virtual interface, return the remote endpoint of the connected
+ # virtual circuit, if any.
+ if self.is_virtual and hasattr(self, 'virtual_circuit_termination'):
+ return self.virtual_circuit_termination.peer_terminations
+ return super().connected_endpoints
+
#
# Pass-through ports
diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py
index fed33401c..4a672dc8d 100644
--- a/netbox/dcim/tables/devices.py
+++ b/netbox/dcim/tables/devices.py
@@ -635,6 +635,14 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
url_name='dcim:interface_list'
)
+ # Override PathEndpointTable.connection to accommodate virtual circuits
+ connection = columns.TemplateColumn(
+ accessor='_path__destinations',
+ template_code=INTERFACE_LINKTERMINATION,
+ verbose_name=_('Connection'),
+ orderable=False
+ )
+
class Meta(DeviceComponentTable.Meta):
model = models.Interface
fields = (
diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py
index 96ab803e6..1e6a21c14 100644
--- a/netbox/dcim/tables/template_code.py
+++ b/netbox/dcim/tables/template_code.py
@@ -10,6 +10,20 @@ LINKTERMINATION = """
{% endfor %}
"""
+INTERFACE_LINKTERMINATION = """
+{% load i18n %}
+{% if record.is_virtual and record.virtual_circuit_termination %}
+ {% for termination in record.connected_endpoints %}
+ {{ termination.interface.parent_object }}
+
+ {{ termination.interface }}
+ {% trans "via" %}
+ {{ termination.parent_object }}
+ {% if not forloop.last %}
{% endif %}
+ {% endfor %}
+{% else %}""" + LINKTERMINATION + """{% endif %}
+"""
+
CABLE_LENGTH = """
{% load helpers %}
{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %}
diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html
index b18a6380b..842af37c1 100644
--- a/netbox/templates/dcim/interface.html
+++ b/netbox/templates/dcim/interface.html
@@ -139,7 +139,41 @@
- {% if not object.is_virtual %}
+ {% if object.is_virtual and object.virtual_circuit_termination %}
+
{% trans "Provider" %} | +{{ object.virtual_circuit_termination.virtual_circuit.provider|linkify }} | +
---|---|
{% trans "Provider Network" %} | +{{ object.virtual_circuit_termination.virtual_circuit.provider_network|linkify }} | +
{% trans "Circuit ID" %} | +{{ object.virtual_circuit_termination.virtual_circuit|linkify }} | +
{% trans "Role" %} | +{{ object.virtual_circuit_termination.get_role_display }} | +
{% trans "Connections" %} | +
+ {% for termination in object.virtual_circuit_termination.peer_terminations %}
+ {{ termination.interface.parent_object }}
+
+ {{ termination.interface }}
+ ({{ termination.get_role_display }})
+ {% if not forloop.last %} {% endif %} + {% endfor %} + |
+