diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 2ec7921fc..8b6923121 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -3,7 +3,7 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie from circuits.constants import CIRCUIT_STATUS_CHOICES from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer +from dcim.api.serializers import NestedSiteSerializer from extras.api.customfields import CustomFieldModelSerializer from tenancy.api.serializers import NestedTenantSerializer from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer @@ -85,10 +85,18 @@ class NestedCircuitSerializer(WritableNestedSerializer): class CircuitTerminationSerializer(ValidatedModelSerializer): circuit = NestedCircuitSerializer() site = NestedSiteSerializer() - interface = InterfaceSerializer(required=False, allow_null=True) class Meta: model = CircuitTermination fields = [ - 'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', + 'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', ] + + +class NestedCircuitTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + circuit = NestedCircuitSerializer() + + class Meta: + model = CircuitTermination + fields = ['id', 'url', 'circuit', 'term_side'] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 882805ec1..95229fe9e 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -67,6 +67,6 @@ class CircuitViewSet(CustomFieldModelViewSet): # class CircuitTerminationViewSet(ModelViewSet): - queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device') + queryset = CircuitTermination.objects.select_related('circuit', 'site') serializer_class = serializers.CircuitTerminationSerializer filter_class = filters.CircuitTerminationFilter diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index 2f10598f2..8d276b3f3 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -2,7 +2,7 @@ from django import forms from django.db.models import Count from taggit.forms import TagField -from dcim.models import Site, Device, Interface, Rack +from dcim.models import Site, Device, Rack from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from tenancy.forms import TenancyForm from tenancy.models import Tenant @@ -203,57 +203,12 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm): # Circuit terminations # -class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - site = forms.ModelChoiceField( - queryset=Site.objects.all(), - widget=forms.Select( - attrs={'filter-for': 'rack'} - ) - ) - rack = ChainedModelChoiceField( - queryset=Rack.objects.all(), - chains=( - ('site', 'site'), - ), - required=False, - label='Rack', - widget=APISelect( - api_url='/api/dcim/racks/?site_id={{site}}', - attrs={'filter-for': 'device', 'nullable': 'true'} - ) - ) - device = ChainedModelChoiceField( - queryset=Device.objects.all(), - chains=( - ('site', 'site'), - ('rack', 'rack'), - ), - required=False, - label='Device', - widget=APISelect( - api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}', - display_field='display_name', - attrs={'filter-for': 'interface'} - ) - ) - interface = ChainedModelChoiceField( - queryset=Interface.objects.connectable().select_related('circuit_termination'), - chains=( - ('device', 'device'), - ), - required=False, - label='Interface', - widget=APISelect( - api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical', - disabled_indicator='cable' - ) - ) +class CircuitTerminationForm(BootstrapMixin, forms.ModelForm): class Meta: model = CircuitTermination fields = [ - 'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', - 'pp_info', + 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', ] help_texts = { 'port_speed': "Physical circuit speed", @@ -263,25 +218,3 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm widgets = { 'term_side': forms.HiddenInput(), } - - def __init__(self, *args, **kwargs): - - # Initialize helper selectors - instance = kwargs.get('instance') - if instance and instance.interface is not None: - initial = kwargs.get('initial', {}).copy() - initial['rack'] = instance.interface.device.rack - initial['device'] = instance.interface.device - kwargs['initial'] = initial - - super(CircuitTerminationForm, self).__init__(*args, **kwargs) - - # Mark occupied interfaces as disabled - self.fields['interface'].choices = [] - for iface in self.fields['interface'].queryset: - self.fields['interface'].choices.append( - (iface.id, { - 'label': iface.name, - 'disabled': bool(iface.cable) and iface.pk != self.initial.get('interface'), - }) - ) diff --git a/netbox/circuits/migrations/0013_cables.py b/netbox/circuits/migrations/0013_cables.py new file mode 100644 index 000000000..dfdfedb96 --- /dev/null +++ b/netbox/circuits/migrations/0013_cables.py @@ -0,0 +1,80 @@ +from django.db import migrations, models +import django.db.models.deletion + +from dcim.constants import CONNECTION_STATUS_CONNECTED + + +def circuit_terminations_to_cables(apps, schema_editor): + """ + Copy all existing CircuitTermination Interface associations as Cables + """ + ContentType = apps.get_model('contenttypes', 'ContentType') + CircuitTermination = apps.get_model('circuits', 'CircuitTermination') + Interface = apps.get_model('dcim', 'Interface') + Cable = apps.get_model('dcim', 'Cable') + + # Load content types + circuittermination_type = ContentType.objects.get_for_model(CircuitTermination) + interface_type = ContentType.objects.get_for_model(Interface) + + # Create a new Cable instance from each console connection + print("\n Adding circuit terminations... ", end='', flush=True) + for circuittermination in CircuitTermination.objects.filter(interface__isnull=False): + c = Cable() + + # We have to assign all fields manually because we're inside a migration. + c.termination_a_type = circuittermination_type + c.termination_a_id = circuittermination.id + c.termination_b_type = interface_type + c.termination_b_id = circuittermination.interface_id + c.connection_status = CONNECTION_STATUS_CONNECTED + c.save() + + # Cache the connected Cable on the CircuitTermination + circuittermination.cable = c + circuittermination.connected_endpoint = circuittermination.interface + circuittermination.connection_status = CONNECTION_STATUS_CONNECTED + circuittermination.save() + + # Cache the connected Cable on the Interface + interface = circuittermination.interface + interface.cable = c + interface._connected_circuittermination = circuittermination + interface.connection_status = CONNECTION_STATUS_CONNECTED + interface.save() + + cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count() + print("{} cables created".format(cable_count)) + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ('circuits', '0012_change_logging'), + ('dcim', '0066_cables'), + ] + + operations = [ + + # Add CircuitTermination.connected_endpoint + migrations.AddField( + model_name='circuittermination', + name='connected_endpoint', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'), + ), + migrations.AddField( + model_name='circuittermination', + name='connection_status', + field=models.NullBooleanField(default=True), + ), + + # Copy CircuitTermination connections to Interfaces as Cables + migrations.RunPython(circuit_terminations_to_cables), + + # Model changes + migrations.RemoveField( + model_name='circuittermination', + name='interface', + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index fea669d72..2b01965c6 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -3,7 +3,7 @@ from django.db import models from django.urls import reverse from taggit.managers import TaggableManager -from dcim.constants import STATUS_CLASSES +from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES from dcim.fields import ASNField from extras.models import CustomFieldModel, ObjectChange from utilities.models import ChangeLoggedModel @@ -114,8 +114,8 @@ class CircuitType(ChangeLoggedModel): class Circuit(ChangeLoggedModel, CustomFieldModel): """ A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple - circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device - interface, but this is not required. Circuit port speed and commit rate are measured in Kbps. + circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured + in Kbps. """ cid = models.CharField( max_length=50, @@ -227,13 +227,17 @@ class CircuitTermination(models.Model): on_delete=models.PROTECT, related_name='circuit_terminations' ) - interface = models.OneToOneField( + connected_endpoint = models.OneToOneField( to='dcim.Interface', - on_delete=models.PROTECT, - related_name='circuit_termination', + on_delete=models.SET_NULL, + related_name='+', blank=True, null=True ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + default=CONNECTION_STATUS_CONNECTED + ) port_speed = models.PositiveIntegerField( verbose_name='Port speed (Kbps)' ) diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index d70f36bf2..c6a215db8 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -23,12 +23,6 @@ STATUS_LABEL = """ class CircuitTerminationColumn(tables.Column): def render(self, value): - if value.interface: - return mark_safe('{}'.format( - value.interface.device.get_absolute_url(), - value.site, - value.interface.device - )) return mark_safe('{}'.format( value.site.get_absolute_url(), value.site diff --git a/netbox/circuits/urls.py b/netbox/circuits/urls.py index e40ff9f94..b0920d9cd 100644 --- a/netbox/circuits/urls.py +++ b/netbox/circuits/urls.py @@ -1,8 +1,9 @@ from django.conf.urls import url +from dcim.views import CableCreateView from extras.views import ObjectChangeLogView from . import views -from .models import Circuit, CircuitType, Provider +from .models import Circuit, CircuitTermination, CircuitType, Provider app_name = 'circuits' urlpatterns = [ @@ -42,5 +43,6 @@ urlpatterns = [ url(r'^circuits/(?P\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), url(r'^circuit-terminations/(?P\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), url(r'^circuit-terminations/(?P\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), + url(r'^circuit-terminations/(?P\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), ] diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index a38635f90..661f78e8e 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -132,7 +132,7 @@ class CircuitListView(ObjectListView): queryset = Circuit.objects.select_related( 'provider', 'type', 'tenant' ).prefetch_related( - 'terminations__site', 'terminations__interface__device' + 'terminations__site' ) filter = filters.CircuitFilter filter_form = forms.CircuitFilterForm @@ -146,12 +146,12 @@ class CircuitView(View): circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) termination_a = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' + 'site__region', 'connected_endpoint__device' ).filter( circuit=circuit, term_side=TERM_SIDE_A ).first() termination_z = CircuitTermination.objects.select_related( - 'site__region', 'interface__device' + 'site__region', 'connected_endpoint__device' ).filter( circuit=circuit, term_side=TERM_SIDE_Z ).first() diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index cd3652173..93e7092f6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -696,8 +696,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): device = NestedDeviceSerializer() form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False) lag = NestedInterfaceSerializer(required=False, allow_null=True) - connected_endpoint = NestedInterfaceSerializer(read_only=True) - circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) + connected_endpoint = serializers.SerializerMethodField(read_only=True) mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) tagged_vlans = SerializedPKRelatedField( @@ -713,7 +712,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): model = Interface fields = [ 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', - 'connected_endpoint', 'circuit_termination', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', + 'connected_endpoint', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', ] def validate(self, data): @@ -735,6 +734,19 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer): return super(InterfaceSerializer, self).validate(data) + def get_connected_endpoint(self, obj): + """ + Return the appropriate serializer for the type of connected object. + """ + if obj.connected_endpoint is None: + return None + + serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') + context = {'request': self.context['request']} + data = serializer(obj.connected_endpoint, context=context).data + + return data + # # Rear ports diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 31ec0a3cf..6c4dc6f2d 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,7 +1,7 @@ from collections import OrderedDict from django.conf import settings -from django.db.models import F +from django.db.models import F, Q from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from drf_yasg import openapi @@ -407,7 +407,7 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet): class InterfaceViewSet(CableTraceMixin, ModelViewSet): queryset = Interface.objects.select_related( - 'device', 'connected_endpoint__device', 'cable' + 'device', '_connected_interface', '_connected_circuittermination', 'cable' ).prefetch_related( 'tags' ) @@ -483,10 +483,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ModelViewSet): queryset = Interface.objects.select_related( - 'device', 'connected_endpoint__device' + 'device', '_connected_interface', '_connected_circuittermination' ).filter( - connected_endpoint__isnull=False, - pk__lt=F('connected_endpoint') + # Avoid duplicate connections by only selecting the lower PK in a connected pair + Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) | + Q(_connected_circuittermination__isnull=False) ) serializer_class = serializers.InterfaceConnectionSerializer filter_class = filters.InterfaceConnectionFilter diff --git a/netbox/dcim/constants.py b/netbox/dcim/constants.py index 6f7d53239..90e5afd90 100644 --- a/netbox/dcim/constants.py +++ b/netbox/dcim/constants.py @@ -340,9 +340,10 @@ COMPATIBLE_TERMINATION_TYPES = { 'consoleserverport': ['consoleport', 'frontport', 'rearport'], 'powerport': ['poweroutlet'], 'poweroutlet': ['powerport'], - 'interface': ['interface', 'frontport', 'rearport'], + 'interface': ['interface', 'circuittermination', 'frontport', 'rearport'], 'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'], + 'circuittermination': ['interface', 'frontport', 'rearport'], } LENGTH_UNIT_METER = 'm' diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 20f9cba1c..a211fd60e 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,6 @@ import django_filters from django.contrib.auth.models import User -from django.db.models import Count, Q +from django.db.models import Q from netaddr import EUI from netaddr.core import AddrFormatError @@ -876,7 +876,7 @@ class InterfaceConnectionFilter(django_filters.FilterSet): return queryset return queryset.filter( Q(device__site__slug=value) | - Q(connected_endpoint__device__site__slug=value) + Q(_connected_interface__device__site__slug=value) ) def filter_device(self, queryset, name, value): @@ -884,5 +884,5 @@ class InterfaceConnectionFilter(django_filters.FilterSet): return queryset return queryset.filter( Q(device__name__icontains=value) | - Q(connected_endpoint__device__name__icontains=value) + Q(_connected_interface__device__name__icontains=value) ) diff --git a/netbox/dcim/migrations/0066_cables.py b/netbox/dcim/migrations/0066_cables.py index 442e9d5d8..3358900ae 100644 --- a/netbox/dcim/migrations/0066_cables.py +++ b/netbox/dcim/migrations/0066_cables.py @@ -88,28 +88,26 @@ def interface_connections_to_cables(apps, schema_editor): for conn in InterfaceConnection.objects.all(): c = Cable() - # We have to assign GFK fields manually because we're inside a migration. + # We have to assign all fields manually because we're inside a migration. c.termination_a_type = interface_type c.termination_a_id = conn.interface_a_id - c.termination_a = conn.interface_a c.termination_b_type = interface_type c.termination_b_id = conn.interface_b_id - c.termination_b = conn.interface_b c.connection_status = conn.connection_status c.save() - # connected_endpoint and connection_status must be manually assigned - # since these are new fields on Interface - Interface.objects.filter(pk=conn.interface_a_id).update( - connected_endpoint=conn.interface_b_id, - connection_status=conn.connection_status, - cable=c - ) - Interface.objects.filter(pk=conn.interface_b_id).update( - connected_endpoint=conn.interface_a_id, - connection_status=conn.connection_status, - cable=c - ) + # Cache the connected Cable on each Interface + interface_a = conn.interface_a + interface_a._connected_interface = conn.interface_b + interface_a.connection_status = conn.connection_status + interface_a.cable = c + interface_a.save() + + interface_b = conn.interface_b + interface_b._connected_interface = conn.interface_a + interface_b.connection_status = conn.connection_status + interface_b.cable = c + interface_b.save() cable_count = Cable.objects.filter(termination_a_type=interface_type).count() print("{} cables created".format(cable_count)) @@ -120,6 +118,7 @@ class Migration(migrations.Migration): dependencies = [ ('contenttypes', '0002_remove_content_type_name'), + ('circuits', '0006_terminations'), ('dcim', '0065_front_rear_ports'), ] @@ -217,7 +216,12 @@ class Migration(migrations.Migration): # Alter the Interface model migrations.AddField( model_name='interface', - name='connected_endpoint', + name='_connected_circuittermination', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.CircuitTermination'), + ), + migrations.AddField( + model_name='interface', + name='_connected_interface', field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'), ), migrations.AddField( diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 1a8631e6c..9889f9466 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey from taggit.managers import TaggableManager from timezone_field import TimeZoneField -from circuits.models import Circuit +from circuits.models import Circuit, CircuitTermination from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange from utilities.fields import ColorField, NullableCharField from utilities.managers import NaturalOrderByManager @@ -1843,13 +1843,20 @@ class Interface(CableTermination, ComponentModel): name = models.CharField( max_length=64 ) - connected_endpoint = models.OneToOneField( + _connected_interface = models.OneToOneField( to='self', on_delete=models.SET_NULL, related_name='+', blank=True, null=True ) + _connected_circuittermination = models.OneToOneField( + to='circuits.CircuitTermination', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) connection_status = models.NullBooleanField( choices=CONNECTION_STATUS_CHOICES, default=CONNECTION_STATUS_CONNECTED @@ -2008,6 +2015,28 @@ class Interface(CableTermination, ComponentModel): object_data=serialize_object(self) ).save() + @property + def connected_endpoint(self): + if self._connected_interface: + return self._connected_interface + return self._connected_circuittermination + + @connected_endpoint.setter + def connected_endpoint(self, value): + if value is None: + self._connected_interface = None + self._connected_circuittermination = None + elif isinstance(value, Interface): + self._connected_interface = value + self._connected_circuittermination = None + elif isinstance(value, CircuitTermination): + self._connected_interface = None + self._connected_circuittermination = value + else: + raise ValueError( + "Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value)) + ) + @property def parent(self): return self.device or self.virtual_machine diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index c2f9a9695..81b3d28fe 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -904,7 +904,7 @@ class DeviceView(View): interfaces = device.vc_interfaces.order_naturally( device.device_type.interface_ordering ).select_related( - 'lag', 'connected_endpoint__device', 'circuit_termination__circuit', 'cable' + 'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable' ).prefetch_related( 'cable__termination_a', 'cable__termination_b', 'ip_addresses' ) @@ -999,7 +999,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): interfaces = device.vc_interfaces.order_naturally( device.device_type.interface_ordering ).connectable().select_related( - 'connected_endpoint__device' + '_connected_interface__device' ) return render(request, 'dcim/device_lldp_neighbors.html', { @@ -1667,13 +1667,6 @@ class InterfaceView(View): interface = get_object_or_404(Interface, pk=pk) - # Get connected interface - connected_interface = interface.connected_endpoint - if connected_interface is None and hasattr(interface, 'circuit_termination'): - peer_termination = interface.circuit_termination.get_peer_termination() - if peer_termination is not None: - connected_interface = peer_termination.interface - # Get assigned IP addresses ipaddress_table = InterfaceIPAddressTable( data=interface.ip_addresses.select_related('vrf', 'tenant'), @@ -1696,7 +1689,8 @@ class InterfaceView(View): return render(request, 'dcim/interface.html', { 'interface': interface, - 'connected_interface': connected_interface, + # TODO: Also handle connected CircuitTerminations + 'connected_interface': interface._connected_interface, 'ipaddress_table': ipaddress_table, 'vlan_table': vlan_table, }) @@ -1736,8 +1730,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): form = forms.InterfaceBulkDisconnectForm def disconnect_objects(self, interfaces): - return Interface.objects.filter(connected_endpoint__in=interfaces).update( - connected_endpoint=None, connection_status=None + return Interface.objects.filter(_connected_interface__in=interfaces).update( + _connected_interface=None, connection_status=None ) @@ -2103,10 +2097,11 @@ class PowerConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView): queryset = Interface.objects.select_related( - 'connected_endpoint__device', + '_connected_interface', '_connected_circuittermination' ).filter( - connected_endpoint__isnull=False, - pk__lt=F('connected_endpoint'), + # Avoid duplicate connections by only selecting the lower PK in a connected pair + _connected_interface__isnull=False, + pk__lt=F('_connected_interface') ) filter = filters.InterfaceConnectionFilter filter_form = forms.InterfaceConnectionFilterForm diff --git a/netbox/extras/models.py b/netbox/extras/models.py index abd9cf49d..7bf28d225 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -508,17 +508,17 @@ class TopologyMap(models.Model): # Add all interface connections to the graph connected_interfaces = Interface.objects.select_related( - 'connected_endpoint__device' + '_connected_interface__device' ).filter( - Q(device__in=devices) | Q(connected_endpoint__device__in=devices), - connected_endpoint__isnull=False, + Q(device__in=devices) | Q(_connected_interface__device__in=devices), + _connected_interface__isnull=False, ) for interface in connected_interfaces: style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed' self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style) # Add all circuits to the graph - for termination in CircuitTermination.objects.filter(term_side='A', interface__device__in=devices): + for termination in CircuitTermination.objects.filter(term_side='A', connected_endpoint__device__in=devices): peer_termination = termination.get_peer_termination() if (peer_termination is not None and peer_termination.interface is not None and peer_termination.interface.device in devices): diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index b75debefe..3ec3c7e42 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -163,8 +163,8 @@ class HomeView(View): connected_endpoint__isnull=False ) connected_interfaces = Interface.objects.filter( - connected_endpoint__isnull=False, - pk__lt=F('connected_endpoint') + _connected_interface__isnull=False, + pk__lt=F('_connected_interface') ) stats = { diff --git a/netbox/templates/circuits/circuittermination_edit.html b/netbox/templates/circuits/circuittermination_edit.html index a2c7d966f..792ff99b8 100644 --- a/netbox/templates/circuits/circuittermination_edit.html +++ b/netbox/templates/circuits/circuittermination_edit.html @@ -41,9 +41,6 @@ {% render_field form.site %} - {% render_field form.rack %} - {% render_field form.device %} - {% render_field form.interface %}
diff --git a/netbox/templates/circuits/inc/circuit_termination.html b/netbox/templates/circuits/inc/circuit_termination.html index a1b236dcc..bdfd6faae 100644 --- a/netbox/templates/circuits/inc/circuit_termination.html +++ b/netbox/templates/circuits/inc/circuit_termination.html @@ -39,10 +39,17 @@ Termination - {% if termination.interface %} - {{ termination.interface.device }} - {{ termination.interface }} + {% if termination.connected_endpoint %} + {{ termination.connected_endpoint.device }} + {{ termination.connected_endpoint }} {% else %} + {% if perms.circuits.change_circuittermination %} + + {% endif %} Not defined {% endif %} @@ -61,8 +68,8 @@ IP Addressing - {% if termination.interface %} - {% for ip in termination.interface.ip_addresses.all %} + {% if termination.connected_endpoint %} + {% for ip in termination.connected_endpoint.ip_addresses.all %} {% if not forloop.first %}
{% endif %} {{ ip }} ({{ ip.vrf|default:"Global" }}) {% empty %} diff --git a/netbox/templates/dcim/cable_connect.html b/netbox/templates/dcim/cable_connect.html index f067bd8d5..ecce56118 100644 --- a/netbox/templates/dcim/cable_connect.html +++ b/netbox/templates/dcim/cable_connect.html @@ -29,30 +29,59 @@ A Side
-
- -
-

{{ termination_a.device.site }}

+ {% if termination_a.device %} + {# Device component #} +
+ +
+

{{ termination_a.device.site }}

+
-
-
- -
-

{{ termination_a.device.rack|default:"None" }}

+
+ +
+

{{ termination_a.device.rack|default:"None" }}

+
-
-
- -
-

{{ termination_a.device }}

+
+ +
+

{{ termination_a.device }}

+
-
-
- -
-

{{ termination_a }}

+
+ +
+

{{ termination_a }}

+
-
+ {% else %} + {# Circuit termination #} +
+ +
+

{{ termination_a.site }}

+
+
+
+ +
+

{{ termination_a.circuit.provider }}

+
+
+
+ +
+

{{ termination_a.circuit.cid }}

+
+
+
+ +
+

{{ termination_a.term_side }}

+
+
+ {% endif %}
diff --git a/netbox/templates/dcim/inc/cable_termination.html b/netbox/templates/dcim/inc/cable_termination.html index 8ff74b855..e1b0959bb 100644 --- a/netbox/templates/dcim/inc/cable_termination.html +++ b/netbox/templates/dcim/inc/cable_termination.html @@ -1,12 +1,29 @@ - - - - - - - - + {% if termination.device %} + {# Device component #} + + + + + + + + + {% else %} + {# Circuit termination #} + + + + + + + + + {% endif %}
Device - {{ termination.device }} -
Component{{ termination }}
Device + {{ termination.device }} +
Component{{ termination }}
Provider + {{ termination.circuit.provider }} +
Circuit + {{ termination.circuit }} (Side {{ termination.term_side }}) +
diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 8b6583ace..a3bc3400a 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -50,17 +50,17 @@ Virtual interface {% elif iface.is_wireless %} Wireless interface - {% elif iface.connected_endpoint %} - {% with connected_iface=iface.connected_endpoint %} - - {{ connected_iface.device }} - - - {{ connected_iface }} - - {% endwith %} - {% elif iface.circuit_termination %} - {% with iface.circuit_termination.get_peer_termination as peer_termination %} + {% elif iface.connected_endpoint.name %} + {# Connected to an Interface #} + + {{ iface.connected_endpoint.device }} + + + {{ iface.connected_endpoint }} + + {% elif iface.connected_endpoint.term_side %} + {# Connected to a CircuitTermination #} + {% with iface.connected_endpoint.get_peer_termination as peer_termination %} {% if peer_termination %} @@ -72,7 +72,7 @@ {% endif %} via {% endif %} - {{ iface.circuit_termination.circuit }} + {{ iface.connected_endpoint.circuit }} {% endwith %} {% else %} @@ -84,7 +84,7 @@ {# Buttons #} {% if show_graphs %} - {% if iface.circuit_termination or iface.connected_endpoint %} + {% if iface.connected_endpoint %} @@ -110,13 +110,6 @@ - {% elif iface.circuit_termination and perms.circuits.change_circuittermination %} - - - - {% else %}