diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index cd73a614d..1c8ad69e4 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -46,9 +46,7 @@ class CircuitTypeViewSet(ModelViewSet): class CircuitViewSet(CustomFieldModelViewSet): queryset = Circuit.objects.prefetch_related( - Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related( - 'site', 'connected_endpoint__device' - )), + Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')), 'type', 'tenant', 'provider', ).prefetch_related('tags') serializer_class = serializers.CircuitSerializer @@ -61,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet): class CircuitTerminationViewSet(ModelViewSet): queryset = CircuitTermination.objects.prefetch_related( - 'circuit', 'site', 'connected_endpoint__device', 'cable' + 'circuit', 'site', 'cable' ) serializer_class = serializers.CircuitTerminationSerializer filterset_class = filters.CircuitTerminationFilterSet diff --git a/netbox/circuits/migrations/0022_drop_connected_endpoint.py b/netbox/circuits/migrations/0022_drop_connected_endpoint.py new file mode 100644 index 000000000..59647dd2b --- /dev/null +++ b/netbox/circuits/migrations/0022_drop_connected_endpoint.py @@ -0,0 +1,17 @@ +# Generated by Django 3.1 on 2020-10-05 13:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0021_cablepath'), + ] + + operations = [ + migrations.RemoveField( + model_name='circuittermination', + name='connected_endpoint', + ), + ] diff --git a/netbox/circuits/models.py b/netbox/circuits/models.py index 686ab9219..746a71b3c 100644 --- a/netbox/circuits/models.py +++ b/netbox/circuits/models.py @@ -248,13 +248,6 @@ class CircuitTermination(PathEndpoint, CableTermination): on_delete=models.PROTECT, related_name='circuit_terminations' ) - connected_endpoint = models.OneToOneField( - to='dcim.Interface', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, diff --git a/netbox/circuits/views.py b/netbox/circuits/views.py index e5da5100f..968c88777 100644 --- a/netbox/circuits/views.py +++ b/netbox/circuits/views.py @@ -131,7 +131,7 @@ class CircuitView(ObjectView): circuit = get_object_or_404(self.queryset, pk=pk) termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region', 'connected_endpoint__device' + 'site__region' ).filter( circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A ).first() @@ -139,7 +139,7 @@ class CircuitView(ObjectView): termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view') termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( - 'site__region', 'connected_endpoint__device' + 'site__region' ).filter( circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z ).first() diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 8078f8819..0cf78fafc 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -34,8 +34,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): def get_connected_endpoint_type(self, obj): if obj.path is not None: - destination = obj.path.destination - return f'{destination._meta.app_label}.{destination._meta.model_name}' + return f'{obj.connected_endpoint._meta.app_label}.{obj.connected_endpoint._meta.model_name}' return None @swagger_serializer_method(serializer_or_field=serializers.DictField) @@ -44,7 +43,7 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer): Return the appropriate serializer for the type of connected object. """ if obj.path is not None: - serializer = get_serializer_for_model(obj.path.destination, prefix='Nested') + serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') context = {'request': self.context['request']} return serializer(obj.path.destination, context=context).data return None diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index edbdfb7be..6ed50324e 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -470,37 +470,31 @@ class DeviceViewSet(CustomFieldModelViewSet): # class ConsolePortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = ConsolePort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsolePortFilterSet class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = ConsoleServerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.ConsoleServerPortSerializer filterset_class = filters.ConsoleServerPortFilterSet class PowerPortViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerPort.objects.prefetch_related( - 'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags' - ) + queryset = PowerPort.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerPortFilterSet class PowerOutletViewSet(PathEndpointMixin, ModelViewSet): - queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') + queryset = PowerOutlet.objects.prefetch_related('device', '_path', 'cable', 'tags') serializer_class = serializers.PowerOutletSerializer filterset_class = filters.PowerOutletFilterSet class InterfaceViewSet(PathEndpointMixin, ModelViewSet): - queryset = Interface.objects.prefetch_related( - 'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags' - ).filter( - device__isnull=False - ) + queryset = Interface.objects.prefetch_related('device', '_path', 'cable', 'ip_addresses', 'tags') serializer_class = serializers.InterfaceSerializer filterset_class = filters.InterfaceFilterSet @@ -534,32 +528,26 @@ class InventoryItemViewSet(ModelViewSet): # class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = ConsolePort.objects.prefetch_related( - 'device', 'connected_endpoint__device' - ).filter( - connected_endpoint__isnull=False + queryset = ConsolePort.objects.prefetch_related('device', '_path').filter( + _path__destination_id__isnull=False ) serializer_class = serializers.ConsolePortSerializer filterset_class = filters.ConsoleConnectionFilterSet class PowerConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = PowerPort.objects.prefetch_related( - 'device', 'connected_endpoint__device' - ).filter( - _connected_poweroutlet__isnull=False + queryset = PowerPort.objects.prefetch_related('device', '_path').filter( + _path__destination_id__isnull=False ) serializer_class = serializers.PowerPortSerializer filterset_class = filters.PowerConnectionFilterSet class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): - queryset = Interface.objects.prefetch_related( - 'device', '_connected_interface__device' - ).filter( + queryset = Interface.objects.prefetch_related('device', '_path').filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair - _connected_interface__isnull=False, - pk__lt=F('_connected_interface') + _path__destination_id__isnull=False, + pk__lt=F('_path__destination_id') ) serializer_class = serializers.InterfaceConnectionSerializer filterset_class = filters.InterfaceConnectionFilterSet @@ -664,7 +652,7 @@ class ConnectedDeviceViewSet(ViewSet): device__name=peer_device_name, name=peer_interface_name ) - local_interface = peer_interface._connected_interface + local_interface = peer_interface.connected_endpoint if local_interface is None: return Response() diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index c76bd3b87..aa5c62552 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1171,18 +1171,19 @@ class ConsoleConnectionFilterSet(BaseFilterSet): model = ConsolePort fields = ['name', 'connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter(connected_endpoint__device__site__slug=value) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'connected_endpoint__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter(connected_endpoint__device__site__slug=value) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'connected_endpoint__{}__in'.format(name): value}) + # ) class PowerConnectionFilterSet(BaseFilterSet): @@ -1202,18 +1203,19 @@ class PowerConnectionFilterSet(BaseFilterSet): model = PowerPort fields = ['name', 'connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter(_connected_poweroutlet__device__site__slug=value) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter(_connected_poweroutlet__device__site__slug=value) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'_connected_poweroutlet__{}__in'.format(name): value}) + # ) class InterfaceConnectionFilterSet(BaseFilterSet): @@ -1233,21 +1235,22 @@ class InterfaceConnectionFilterSet(BaseFilterSet): model = Interface fields = ['connection_status'] - def filter_site(self, queryset, name, value): - if not value.strip(): - return queryset - return queryset.filter( - Q(device__site__slug=value) | - Q(_connected_interface__device__site__slug=value) - ) - - def filter_device(self, queryset, name, value): - if not value: - return queryset - return queryset.filter( - Q(**{'{}__in'.format(name): value}) | - Q(**{'_connected_interface__{}__in'.format(name): value}) - ) + # TODO: Fix filters + # def filter_site(self, queryset, name, value): + # if not value.strip(): + # return queryset + # return queryset.filter( + # Q(device__site__slug=value) | + # Q(_connected_interface__device__site__slug=value) + # ) + # + # def filter_device(self, queryset, name, value): + # if not value: + # return queryset + # return queryset.filter( + # Q(**{'{}__in'.format(name): value}) | + # Q(**{'_connected_interface__{}__in'.format(name): value}) + # ) class PowerPanelFilterSet(BaseFilterSet): diff --git a/netbox/dcim/migrations/0121_drop_connected_endpoint.py b/netbox/dcim/migrations/0121_drop_connected_endpoint.py new file mode 100644 index 000000000..f05cfdd1a --- /dev/null +++ b/netbox/dcim/migrations/0121_drop_connected_endpoint.py @@ -0,0 +1,37 @@ +# Generated by Django 3.1 on 2020-10-05 13:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0120_cablepath'), + ] + + operations = [ + migrations.RemoveField( + model_name='consoleport', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_circuittermination', + ), + migrations.RemoveField( + model_name='interface', + name='_connected_interface', + ), + migrations.RemoveField( + model_name='powerfeed', + name='connected_endpoint', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_powerfeed', + ), + migrations.RemoveField( + model_name='powerport', + name='_connected_poweroutlet', + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 65032f529..72edfc46e 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -150,6 +150,15 @@ class PathEndpoint(models.Model): # Return the path as a list of three-tuples (A termination, cable, B termination) return list(zip(*[iter(path)] * 3)) + @property + def connected_endpoint(self): + """ + Caching accessor for the attached CablePath's destination (if any) + """ + if not hasattr(self, '_connected_endpoint'): + self._connected_endpoint = self._path.destination if self._path else None + return self._connected_endpoint + # # Console ports @@ -166,13 +175,6 @@ class ConsolePort(CableTermination, PathEndpoint, ComponentModel): blank=True, help_text='Physical port type' ) - connected_endpoint = models.OneToOneField( - to='dcim.ConsoleServerPort', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -267,20 +269,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): validators=[MinValueValidator(1)], help_text="Allocated power draw (watts)" ) - _connected_poweroutlet = models.OneToOneField( - to='dcim.PowerOutlet', - on_delete=models.SET_NULL, - related_name='connected_endpoint', - blank=True, - null=True - ) - _connected_powerfeed = models.OneToOneField( - to='dcim.PowerFeed', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -308,43 +296,6 @@ class PowerPort(CableTermination, PathEndpoint, ComponentModel): self.description, ) - @property - def connected_endpoint(self): - """ - Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for - ObjectDoesNotExist in case the referenced object has been deleted from the database. - """ - try: - if self._connected_poweroutlet: - return self._connected_poweroutlet - except ObjectDoesNotExist: - pass - try: - if self._connected_powerfeed: - return self._connected_powerfeed - except ObjectDoesNotExist: - pass - return None - - @connected_endpoint.setter - def connected_endpoint(self, value): - # TODO: Fix circular import - from . import PowerFeed - - if value is None: - self._connected_poweroutlet = None - self._connected_powerfeed = None - elif isinstance(value, PowerOutlet): - self._connected_poweroutlet = value - self._connected_powerfeed = None - elif isinstance(value, PowerFeed): - self._connected_poweroutlet = None - self._connected_powerfeed = value - else: - raise ValueError( - "Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value)) - ) - def get_power_draw(self): """ Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. @@ -497,20 +448,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): max_length=100, blank=True ) - _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.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, @@ -631,42 +568,6 @@ class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface): return super().save(*args, **kwargs) - @property - def connected_endpoint(self): - """ - Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to - check for ObjectDoesNotExist in case the referenced object has been deleted from the database. - """ - try: - if self._connected_interface: - return self._connected_interface - except ObjectDoesNotExist: - pass - try: - if self._connected_circuittermination: - return self._connected_circuittermination - except ObjectDoesNotExist: - pass - return None - - @connected_endpoint.setter - def connected_endpoint(self, value): - from circuits.models import CircuitTermination - - 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 diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index caa22e74a..ec1480a7e 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -88,13 +88,6 @@ class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldMo blank=True, null=True ) - connected_endpoint = models.OneToOneField( - to='dcim.PowerPort', - on_delete=models.SET_NULL, - related_name='+', - blank=True, - null=True - ) connection_status = models.BooleanField( choices=CONNECTION_STATUS_CHOICES, blank=True, diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 87bd38309..7a1028dec 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1122,10 +1122,8 @@ class DeviceLLDPNeighborsView(ObjectView): def get(self, request, pk): device = get_object_or_404(self.queryset, pk=pk) - interfaces = device.vc_interfaces.restrict(request.user, 'view').exclude( + interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related('_path').exclude( type__in=NONCONNECTABLE_IFACE_TYPES - ).prefetch_related( - '_connected_interface__device' ) return render(request, 'dcim/device_lldp_neighbors.html', { @@ -1483,8 +1481,6 @@ class InterfaceView(ObjectView): return render(request, 'dcim/interface.html', { 'instance': interface, - 'connected_interface': interface._connected_interface, - 'connected_circuittermination': interface._connected_circuittermination, 'ipaddress_table': ipaddress_table, 'vlan_table': vlan_table, }) @@ -2137,7 +2133,7 @@ class InterfaceConnectionsListView(ObjectListView): ).filter( # Avoid duplicate connections by only selecting the lower PK in a connected pair _path__isnull=False, - pk__lt=F('_connected_interface') + pk__lt=F('_path__destination_id') ).order_by('device') filterset = filters.InterfaceConnectionFilterSet filterset_form = forms.InterfaceConnectionFilterForm diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index b2bee0f96..161bfda74 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -190,15 +190,15 @@ class HomeView(View): def get(self, request): - connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').filter( - connected_endpoint__isnull=False + connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False ) - connected_powerports = PowerPort.objects.restrict(request.user, 'view').filter( - _connected_poweroutlet__isnull=False + connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False ) - connected_interfaces = Interface.objects.restrict(request.user, 'view').filter( - _connected_interface__isnull=False, - pk__lt=F('_connected_interface') + connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter( + _path__destination_id__isnull=False, + pk__lt=F('_path__destination_id') ) # Report Results diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 7fcf6ab0a..12b9918e9 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -77,61 +77,63 @@ {% if instance.cable %} - {% if connected_interface %} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {% elif connected_circuittermination %} - {% with ct=connected_circuittermination %} + {% if instance.connected_endpoint.device %} + {% with iface=instance.connected_endpoint %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endwith %} + {% elif instance.connected_endpoint.circuit %} + {% with ct=instance.connected_endpoint %}
Device - {{ connected_interface.device }} -
Name - {{ connected_interface.name }} -
Type{{ connected_interface.get_type_display }}
Enabled - {% if connected_interface.enabled %} - - {% else %} - - {% endif %} -
LAG - {% if connected_interface.lag%} - {{ connected_interface.lag }} - {% else %} - None - {% endif %} -
Description{{ connected_interface.description|placeholder }}
MTU{{ connected_interface.mtu|placeholder }}
MAC Address{{ connected_interface.mac_address|placeholder }}
802.1Q Mode{{ connected_interface.get_mode_display }}
Device + {{ iface.device }} +
Name + {{ iface.name }} +
Type{{ iface.get_type_display }}
Enabled + {% if iface.enabled %} + + {% else %} + + {% endif %} +
LAG + {% if iface.lag%} + {{ iface.lag }} + {% else %} + None + {% endif %} +
Description{{ iface.description|placeholder }}
MTU{{ iface.mtu|placeholder }}
MAC Address{{ iface.mac_address|placeholder }}
802.1Q Mode{{ iface.get_mode_display }}
Provider {{ ct.circuit.provider }}