From 4426f47106f6f65c56967e0f7de4fd741df3ebd0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 7 Nov 2024 16:41:05 -0500 Subject: [PATCH] Show virtual circuit connections on interfaces --- netbox/circuits/models/virtual_circuits.py | 17 ++++++++++ netbox/dcim/models/device_components.py | 8 +++++ netbox/dcim/tables/devices.py | 8 +++++ netbox/dcim/tables/template_code.py | 14 +++++++++ netbox/templates/dcim/interface.html | 36 +++++++++++++++++++++- 5 files changed, 82 insertions(+), 1 deletion(-) 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 "Virtual Circuit" %}

+ + + + + + + + + + + + + + + + + + + + + +
{% 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 %} +
+
+ {% elif not object.is_virtual %}

{% trans "Connection" %}

{% if object.mark_connected %}