diff --git a/netbox/circuits/forms.py b/netbox/circuits/forms.py index b6a8f0efc..b5883e6ec 100644 --- a/netbox/circuits/forms.py +++ b/netbox/circuits/forms.py @@ -237,9 +237,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm ) ) interface = ChainedModelChoiceField( - queryset=Interface.objects.connectable().select_related( - 'circuit_termination', 'connected_as_a', 'connected_as_b' - ), + queryset=Interface.objects.connectable().select_related('circuit_termination'), chains=( ('device', 'device'), ), diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index b2b77b78c..6e113d394 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -6,10 +6,9 @@ from circuits.models import Circuit, CircuitTermination from dcim.constants import * from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceType, DeviceRole, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + DeviceBayTemplate, DeviceType, DeviceRole, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, + Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) from extras.api.customfields import CustomFieldModelSerializer from ipam.models import IPAddress, VLAN @@ -614,7 +613,7 @@ class IsConnectedMixin(object): """ Return True if the interface has a connected interface or circuit. """ - if obj.connection: + if obj.connected_endpoint: return True if hasattr(obj, 'circuit_termination') and obj.circuit_termination is not None: return True @@ -662,8 +661,8 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri device = NestedDeviceSerializer() form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False) lag = NestedInterfaceSerializer(required=False, allow_null=True) + connected_endpoint = NestedInterfaceSerializer(required=False, allow_null=True) is_connected = serializers.SerializerMethodField(read_only=True) - interface_connection = serializers.SerializerMethodField(read_only=True) circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) @@ -679,7 +678,7 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri model = Interface fields = [ 'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', - 'is_connected', 'interface_connection', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans', + 'is_connected', 'connected_endpoint', 'circuit_termination', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', ] @@ -702,15 +701,6 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri return super(InterfaceSerializer, self).validate(data) - def get_interface_connection(self, obj): - if obj.connection: - context = { - 'request': self.context['request'], - 'interface': obj.connected_interface, - } - return ContextualInterfaceConnectionSerializer(obj.connection, context=context).data - return None - # # Rear panel ports @@ -804,36 +794,17 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer): # class InterfaceConnectionSerializer(ValidatedModelSerializer): - interface_a = NestedInterfaceSerializer() - interface_b = NestedInterfaceSerializer() + interface_a = serializers.SerializerMethodField() + interface_b = NestedInterfaceSerializer(source='connected_endpoint') connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) class Meta: - model = InterfaceConnection - fields = ['id', 'interface_a', 'interface_b', 'connection_status'] + model = Interface + fields = ['interface_a', 'interface_b', 'connection_status'] - -class NestedInterfaceConnectionSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') - - class Meta: - model = InterfaceConnection - fields = ['id', 'url', 'connection_status'] - - -class ContextualInterfaceConnectionSerializer(serializers.ModelSerializer): - """ - A read-only representation of an InterfaceConnection from the perspective of either of its two connected Interfaces. - """ - interface = serializers.SerializerMethodField(read_only=True) - connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) - - class Meta: - model = InterfaceConnection - fields = ['id', 'interface', 'connection_status'] - - def get_interface(self, obj): - return NestedInterfaceSerializer(self.context['interface'], context=self.context).data + def get_interface_a(self, obj): + context = {'request': self.context['request']} + return NestedInterfaceSerializer(instance=obj, context=context).data # diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 0e6a5d344..4ddbaf9a8 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -60,7 +60,7 @@ router.register(r'inventory-items', views.InventoryItemViewSet) # Connections router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections') router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections') -router.register(r'interface-connections', views.InterfaceConnectionViewSet) +router.register(r'interface-connections', views.InterfaceConnectionViewSet, base_name='interfaceconnections') # Virtual chassis router.register(r'virtual-chassis', views.VirtualChassisViewSet) diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index db7149d18..6128d3411 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,6 +1,7 @@ from collections import OrderedDict from django.conf import settings +from django.db.models import F from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from drf_yasg import openapi @@ -14,10 +15,9 @@ from rest_framework.viewsets import GenericViewSet, ViewSet from dcim import filters from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, + Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) from extras.api.serializers import RenderedGraphSerializer from extras.api.views import CustomFieldModelViewSet @@ -35,8 +35,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet): fields = ( (Device, ['face', 'status']), (ConsolePort, ['connection_status']), - (Interface, ['form_factor', 'mode']), - (InterfaceConnection, ['connection_status']), + (Interface, ['connection_status', 'form_factor', 'mode']), (InterfaceTemplate, ['form_factor']), (PowerPort, ['connection_status']), (Rack, ['type', 'width']), @@ -419,7 +418,12 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ModelViewSet): - queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device') + queryset = Interface.objects.select_related( + 'device', 'connected_endpoint__device' + ).filter( + connected_endpoint__isnull=False, + pk__lt=F('connected_endpoint') + ) serializer_class = serializers.InterfaceConnectionSerializer filter_class = filters.InterfaceConnectionFilter diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index da2055506..93d46bf01 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -14,10 +14,9 @@ from .constants import ( ) from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, + InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) @@ -853,21 +852,21 @@ class InterfaceConnectionFilter(django_filters.FilterSet): ) class Meta: - model = InterfaceConnection + model = Interface fields = ['connection_status'] def filter_site(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( - Q(interface_a__device__site__slug=value) | - Q(interface_b__device__site__slug=value) + Q(device__site__slug=value) | + Q(connected_endpoint__device__site__slug=value) ) def filter_device(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( - Q(interface_a__device__name__icontains=value) | - Q(interface_b__device__name__icontains=value) + Q(device__name__icontains=value) | + Q(connected_endpoint__device__name__icontains=value) ) diff --git a/netbox/dcim/fixtures/dcim.json b/netbox/dcim/fixtures/dcim.json index 6c3e4b7df..9ebe9d4c7 100644 --- a/netbox/dcim/fixtures/dcim.json +++ b/netbox/dcim/fixtures/dcim.json @@ -5746,158 +5746,5 @@ "mgmt_only": true, "description": "" } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 3, - "fields": { - "interface_a": 99, - "interface_b": 15, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 4, - "fields": { - "interface_a": 100, - "interface_b": 153, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 5, - "fields": { - "interface_a": 46, - "interface_b": 14, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 6, - "fields": { - "interface_a": 47, - "interface_b": 152, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 7, - "fields": { - "interface_a": 91, - "interface_b": 144, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 8, - "fields": { - "interface_a": 92, - "interface_b": 145, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 16, - "fields": { - "interface_a": 189, - "interface_b": 37, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 17, - "fields": { - "interface_a": 192, - "interface_b": 175, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 18, - "fields": { - "interface_a": 195, - "interface_b": 41, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 19, - "fields": { - "interface_a": 198, - "interface_b": 179, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 20, - "fields": { - "interface_a": 191, - "interface_b": 197, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 21, - "fields": { - "interface_a": 194, - "interface_b": 200, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 22, - "fields": { - "interface_a": 9, - "interface_b": 218, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 23, - "fields": { - "interface_a": 8, - "interface_b": 206, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 24, - "fields": { - "interface_a": 7, - "interface_b": 212, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 25, - "fields": { - "interface_a": 217, - "interface_b": 205, - "connection_status": true - } -}, -{ - "model": "dcim.interfaceconnection", - "pk": 26, - "fields": { - "interface_a": 216, - "interface_b": 211, - "connection_status": true - } } ] diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 762fe7c4c..fd31bb1bf 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -26,10 +26,9 @@ from virtualization.models import Cluster from .constants import * from .models import ( Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, - Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, Manufacturer, + InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, + RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) DEVICE_BY_PK_RE = r'{\d+\}' @@ -2017,173 +2016,6 @@ class InterfaceBulkDisconnectForm(ConfirmationForm): pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) -# -# Interface connections -# - -class InterfaceConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): - interface_a = forms.ChoiceField( - choices=[], - widget=SelectWithDisabled, - label='Interface' - ) - site_b = forms.ModelChoiceField( - queryset=Site.objects.all(), - label='Site', - required=False, - widget=forms.Select( - attrs={'filter-for': 'rack_b'} - ) - ) - rack_b = ChainedModelChoiceField( - queryset=Rack.objects.all(), - chains=( - ('site', 'site_b'), - ), - label='Rack', - required=False, - widget=APISelect( - api_url='/api/dcim/racks/?site_id={{site_b}}', - attrs={'filter-for': 'device_b', 'nullable': 'true'} - ) - ) - device_b = ChainedModelChoiceField( - queryset=Device.objects.all(), - chains=( - ('site', 'site_b'), - ('rack', 'rack_b'), - ), - label='Device', - required=False, - widget=APISelect( - api_url='/api/dcim/devices/?site_id={{site_b}}&rack_id={{rack_b}}', - display_field='display_name', - attrs={'filter-for': 'interface_b'} - ) - ) - livesearch = forms.CharField( - required=False, - label='Device', - widget=Livesearch( - query_key='q', - query_url='dcim-api:device-list', - field_to_update='device_b' - ) - ) - interface_b = ChainedModelChoiceField( - queryset=Interface.objects.connectable().select_related( - 'circuit_termination', 'connected_as_a', 'connected_as_b' - ), - chains=( - ('device', 'device_b'), - ), - label='Interface', - widget=APISelect( - api_url='/api/dcim/interfaces/?device_id={{device_b}}&type=physical', - disabled_indicator='is_connected' - ) - ) - - class Meta: - model = InterfaceConnection - fields = ['interface_a', 'site_b', 'rack_b', 'device_b', 'interface_b', 'livesearch', 'connection_status'] - - def __init__(self, device_a, *args, **kwargs): - - super(InterfaceConnectionForm, self).__init__(*args, **kwargs) - - # Initialize interface A choices - device_a_interfaces = device_a.vc_interfaces.connectable().order_naturally().select_related( - 'circuit_termination', 'connected_as_a', 'connected_as_b' - ) - self.fields['interface_a'].choices = [ - (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) for iface in device_a_interfaces - ] - - # Mark connected interfaces as disabled - if self.data.get('device_b'): - self.fields['interface_b'].choices = [] - for iface in self.fields['interface_b'].queryset: - self.fields['interface_b'].choices.append( - (iface.id, {'label': iface.name, 'disabled': iface.is_connected}) - ) - - -class InterfaceConnectionCSVForm(forms.ModelForm): - device_a = FlexibleModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device A', - error_messages={'invalid_choice': 'Device A not found.'} - ) - interface_a = forms.CharField( - help_text='Name of interface A' - ) - device_b = FlexibleModelChoiceField( - queryset=Device.objects.all(), - to_field_name='name', - help_text='Name or ID of device B', - error_messages={'invalid_choice': 'Device B not found.'} - ) - interface_b = forms.CharField( - help_text='Name of interface B' - ) - connection_status = CSVChoiceField( - choices=CONNECTION_STATUS_CHOICES, - help_text='Connection status' - ) - - class Meta: - model = InterfaceConnection - fields = InterfaceConnection.csv_headers - - def clean_interface_a(self): - - interface_name = self.cleaned_data.get('interface_a') - if not interface_name: - return None - - try: - # Retrieve interface by name - interface = Interface.objects.get( - device=self.cleaned_data['device_a'], name=interface_name - ) - # Check for an existing connection to this interface - if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count(): - raise forms.ValidationError("{} {} is already connected".format( - self.cleaned_data['device_a'], interface_name - )) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface ({} {})".format( - self.cleaned_data['device_a'], interface_name - )) - - return interface - - def clean_interface_b(self): - - interface_name = self.cleaned_data.get('interface_b') - if not interface_name: - return None - - try: - # Retrieve interface by name - interface = Interface.objects.get( - device=self.cleaned_data['device_b'], name=interface_name - ) - # Check for an existing connection to this interface - if InterfaceConnection.objects.filter(Q(interface_a=interface) | Q(interface_b=interface)).count(): - raise forms.ValidationError("{} {} is already connected".format( - self.cleaned_data['device_b'], interface_name - )) - except Interface.DoesNotExist: - raise forms.ValidationError("Invalid interface ({} {})".format( - self.cleaned_data['device_b'], interface_name - )) - - return interface - - # # Front panel ports # diff --git a/netbox/dcim/migrations/0066_cables.py b/netbox/dcim/migrations/0066_cables.py index 2413fa3a5..ce16c4a93 100644 --- a/netbox/dcim/migrations/0066_cables.py +++ b/netbox/dcim/migrations/0066_cables.py @@ -17,6 +17,7 @@ def console_connections_to_cables(apps, schema_editor): consoleserverport_type = ContentType.objects.get_for_model(ConsoleServerPort) # Create a new Cable instance from each console connection + print("\n Adding console connections... ", end='', flush=True) for consoleport in ConsolePort.objects.filter(connected_endpoint__isnull=False): c = Cable() # We have to assign GFK fields manually because we're inside a migration. @@ -27,6 +28,9 @@ def console_connections_to_cables(apps, schema_editor): c.connection_status = consoleport.connection_status c.save() + cable_count = Cable.objects.filter(endpoint_a_type=consoleport_type).count() + print("{} cables created".format(cable_count)) + def power_connections_to_cables(apps, schema_editor): """ @@ -42,6 +46,7 @@ def power_connections_to_cables(apps, schema_editor): poweroutlet_type = ContentType.objects.get_for_model(PowerOutlet) # Create a new Cable instance from each power connection + print(" Adding power connections... ", end='', flush=True) for powerport in PowerPort.objects.filter(connected_endpoint__isnull=False): c = Cable() # We have to assign GFK fields manually because we're inside a migration. @@ -52,6 +57,9 @@ def power_connections_to_cables(apps, schema_editor): c.connection_status = powerport.connection_status c.save() + cable_count = Cable.objects.filter(endpoint_a_type=powerport_type).count() + print("{} cables created".format(cable_count)) + def interface_connections_to_cables(apps, schema_editor): """ @@ -66,6 +74,7 @@ def interface_connections_to_cables(apps, schema_editor): interface_type = ContentType.objects.get_for_model(Interface) # Create a new Cable instance from each InterfaceConnection + print(" Adding interface connections... ", end='', flush=True) for conn in InterfaceConnection.objects.all(): c = Cable() # We have to assign GFK fields manually because we're inside a migration. @@ -76,8 +85,23 @@ def interface_connections_to_cables(apps, schema_editor): 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 + ) + Interface.objects.filter(pk=conn.interface_b_id).update( + connected_endpoint=conn.interface_a_id, + connection_status=conn.connection_status + ) + + cable_count = Cable.objects.filter(endpoint_a_type=interface_type).count() + print("{} cables created".format(cable_count)) + class Migration(migrations.Migration): + atomic = False dependencies = [ ('contenttypes', '0002_remove_content_type_name'), @@ -142,9 +166,34 @@ class Migration(migrations.Migration): field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='poweroutlets', to='dcim.Device'), ), + # Alter the Interface model + migrations.AddField( + model_name='interface', + 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='interface', + name='connection_status', + field=models.NullBooleanField(default=True), + ), + # Copy console/power/interface connections as Cables migrations.RunPython(console_connections_to_cables), migrations.RunPython(power_connections_to_cables), migrations.RunPython(interface_connections_to_cables), + # Delete the InterfaceConnection model + migrations.RemoveField( + model_name='interfaceconnection', + name='interface_a', + ), + migrations.RemoveField( + model_name='interfaceconnection', + name='interface_b', + ), + migrations.DeleteModel( + name='InterfaceConnection', + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 53459143c..d69718013 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1826,7 +1826,7 @@ class PowerOutlet(ComponentModel): class Interface(ComponentModel): """ A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other - Interface via the creation of an InterfaceConnection. + Interface. """ device = models.ForeignKey( to='Device', @@ -1842,6 +1842,20 @@ class Interface(ComponentModel): null=True, blank=True ) + name = models.CharField( + max_length=64 + ) + connected_endpoint = models.OneToOneField( + to='self', + on_delete=models.SET_NULL, + related_name='+', + blank=True, + null=True + ) + connection_status = models.NullBooleanField( + choices=CONNECTION_STATUS_CHOICES, + default=CONNECTION_STATUS_CONNECTED + ) lag = models.ForeignKey( to='self', on_delete=models.SET_NULL, @@ -1850,9 +1864,6 @@ class Interface(ComponentModel): blank=True, verbose_name='Parent LAG' ) - name = models.CharField( - max_length=64 - ) form_factor = models.PositiveSmallIntegerField( choices=IFACE_FF_CHOICES, default=IFACE_FF_10GE_SFP_PLUS @@ -2002,10 +2013,7 @@ class Interface(ComponentModel): changed_object=self, related_object=parent_obj, action=action, - object_data=serialize_object(self, extra={ - 'connected_interface': self.connected_interface.pk if self.connection else None, - 'connection_status': self.connection.connection_status if self.connection else None, - }) + object_data=serialize_object(self) ).save() @property @@ -2034,140 +2042,7 @@ class Interface(ComponentModel): return bool(self.circuit_termination) except ObjectDoesNotExist: pass - return bool(self.connection) - - @property - def connection(self): - try: - return self.connected_as_a - except ObjectDoesNotExist: - pass - try: - return self.connected_as_b - except ObjectDoesNotExist: - pass - return None - - @property - def connected_interface(self): - try: - if self.connected_as_a: - return self.connected_as_a.interface_b - except ObjectDoesNotExist: - pass - try: - if self.connected_as_b: - return self.connected_as_b.interface_a - except ObjectDoesNotExist: - pass - return None - - -class InterfaceConnection(models.Model): - """ - An InterfaceConnection represents a symmetrical, one-to-one connection between two Interfaces. There is no - significant difference between the interface_a and interface_b fields. - """ - interface_a = models.OneToOneField( - to='dcim.Interface', - on_delete=models.CASCADE, - related_name='connected_as_a' - ) - interface_b = models.OneToOneField( - to='dcim.Interface', - on_delete=models.CASCADE, - related_name='connected_as_b' - ) - connection_status = models.BooleanField( - choices=CONNECTION_STATUS_CHOICES, - default=CONNECTION_STATUS_CONNECTED, - verbose_name='Status' - ) - - csv_headers = ['device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status'] - - def clean(self): - - # An interface cannot be connected to itself - if self.interface_a == self.interface_b: - raise ValidationError({ - 'interface_b': "Cannot connect an interface to itself." - }) - - # Only connectable interface types are permitted - if self.interface_a.form_factor in NONCONNECTABLE_IFACE_TYPES: - raise ValidationError({ - 'interface_a': '{} is not a connectable interface type.'.format( - self.interface_a.get_form_factor_display() - ) - }) - if self.interface_b.form_factor in NONCONNECTABLE_IFACE_TYPES: - raise ValidationError({ - 'interface_b': '{} is not a connectable interface type.'.format( - self.interface_b.get_form_factor_display() - ) - }) - - # Prevent the A side of one connection from being the B side of another - interface_a_connections = InterfaceConnection.objects.filter( - Q(interface_a=self.interface_a) | - Q(interface_b=self.interface_a) - ).exclude(pk=self.pk) - if interface_a_connections.exists(): - raise ValidationError({ - 'interface_a': "This interface is already connected." - }) - interface_b_connections = InterfaceConnection.objects.filter( - Q(interface_a=self.interface_b) | - Q(interface_b=self.interface_b) - ).exclude(pk=self.pk) - if interface_b_connections.exists(): - raise ValidationError({ - 'interface_b': "This interface is already connected." - }) - - def to_csv(self): - return ( - self.interface_a.device.identifier, - self.interface_a.name, - self.interface_b.device.identifier, - self.interface_b.name, - self.get_connection_status_display(), - ) - - def log_change(self, user, request_id, action): - """ - Create a new ObjectChange for each of the two affected Interfaces. - """ - interfaces = ( - (self.interface_a, self.interface_b), - (self.interface_b, self.interface_a), - ) - - for interface, peer_interface in interfaces: - if action == OBJECTCHANGE_ACTION_DELETE: - connection_data = { - 'connected_interface': None, - } - else: - connection_data = { - 'connected_interface': peer_interface.pk, - 'connection_status': self.connection_status - } - - try: - parent_obj = interface.parent - except ObjectDoesNotExist: - parent_obj = None - - ObjectChange( - user=user, - request_id=request_id, - changed_object=interface, - related_object=parent_obj, - action=OBJECTCHANGE_ACTION_UPDATE, - object_data=serialize_object(interface, extra=connection_data) - ).save() + return bool(self.connected_endpoint) # diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 6174593af..d72497ecd 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -5,10 +5,9 @@ from tenancy.tables import COL_TENANT from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from .models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, + InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackReservation, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) REGION_LINK = """ @@ -654,17 +653,33 @@ class PowerConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable): - device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'), - args=[Accessor('interface_a.device.pk')], verbose_name='Device A') - interface_a = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_a'), - args=[Accessor('interface_a.pk')], verbose_name='Interface A') - device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'), - args=[Accessor('interface_b.device.pk')], verbose_name='Device B') - interface_b = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_b'), - args=[Accessor('interface_b.pk')], verbose_name='Interface B') + device_a = tables.LinkColumn( + viewname='dcim:device', + accessor=Accessor('device'), + args=[Accessor('device.pk')], + verbose_name='Device A' + ) + interface_a = tables.LinkColumn( + viewname='dcim:interface', + accessor=Accessor('name'), + args=[Accessor('pk')], + verbose_name='Interface A' + ) + device_b = tables.LinkColumn( + viewname='dcim:device', + accessor=Accessor('connected_endpoint.device'), + args=[Accessor('connected_endpoint.device.pk')], + verbose_name='Device B' + ) + interface_b = tables.LinkColumn( + viewname='dcim:interface', + accessor=Accessor('connected_endpoint.name'), + args=[Accessor('connected_endpoint.pk')], + verbose_name='Interface B' + ) class Meta(BaseTable.Meta): - model = InterfaceConnection + model = Interface fields = ('device_a', 'interface_a', 'device_b', 'interface_b') diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index e6cc02537..774725725 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -8,7 +8,7 @@ from dcim.constants import ( ) from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, + DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup, RackReservation, RackRole, Region, Site, VirtualChassis, ) @@ -2393,6 +2393,7 @@ class InterfaceTest(APITestCase): url = reverse('dcim-api:interface-detail', kwargs={'pk': self.interface1.pk}) response = self.client.get(url, **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) self.assertEqual(response.data['name'], self.interface1.name) def test_get_interface_graphs(self): @@ -2882,179 +2883,44 @@ class PowerConnectionTest(APITestCase): self.assertEqual(response.data['count'], 3) -class InterfaceConnectionTest(APITestCase): - - def setUp(self): - - super(InterfaceConnectionTest, self).setUp() - - site = Site.objects.create(name='Test Site 1', slug='test-site-1') - manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') - devicetype = DeviceType.objects.create( - manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' - ) - devicerole = DeviceRole.objects.create( - name='Test Device Role 1', slug='test-device-role-1', color='ff0000' - ) - self.device = Device.objects.create( - device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site - ) - self.interface1 = Interface.objects.create(device=self.device, name='Test Interface 1') - self.interface2 = Interface.objects.create(device=self.device, name='Test Interface 2') - self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3') - self.interface4 = Interface.objects.create(device=self.device, name='Test Interface 4') - self.interface5 = Interface.objects.create(device=self.device, name='Test Interface 5') - self.interface6 = Interface.objects.create(device=self.device, name='Test Interface 6') - self.interface7 = Interface.objects.create(device=self.device, name='Test Interface 7') - self.interface8 = Interface.objects.create(device=self.device, name='Test Interface 8') - self.interface9 = Interface.objects.create(device=self.device, name='Test Interface 9') - self.interface10 = Interface.objects.create(device=self.device, name='Test Interface 10') - self.interface11 = Interface.objects.create(device=self.device, name='Test Interface 11') - self.interface12 = Interface.objects.create(device=self.device, name='Test Interface 12') - self.interfaceconnection1 = InterfaceConnection.objects.create( - interface_a=self.interface1, interface_b=self.interface2 - ) - self.interfaceconnection2 = InterfaceConnection.objects.create( - interface_a=self.interface3, interface_b=self.interface4 - ) - self.interfaceconnection3 = InterfaceConnection.objects.create( - interface_a=self.interface5, interface_b=self.interface6 - ) - - def test_get_interfaceconnection(self): - - url = reverse('dcim-api:interfaceconnection-detail', kwargs={'pk': self.interfaceconnection1.pk}) - response = self.client.get(url, **self.header) - - self.assertEqual(response.data['interface_a']['id'], self.interfaceconnection1.interface_a_id) - self.assertEqual(response.data['interface_b']['id'], self.interfaceconnection1.interface_b_id) - - def test_list_interfaceconnections(self): - - url = reverse('dcim-api:interfaceconnection-list') - response = self.client.get(url, **self.header) - - self.assertEqual(response.data['count'], 3) - - def test_list_interfaceconnections_brief(self): - - url = reverse('dcim-api:interfaceconnection-list') - response = self.client.get('{}?brief=1'.format(url), **self.header) - - self.assertEqual( - sorted(response.data['results'][0]), - ['connection_status', 'id', 'url'] - ) - - def test_create_interfaceconnection(self): - - data = { - 'interface_a': self.interface7.pk, - 'interface_b': self.interface8.pk, - } - - url = reverse('dcim-api:interfaceconnection-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(InterfaceConnection.objects.count(), 4) - interfaceconnection4 = InterfaceConnection.objects.get(pk=response.data['id']) - self.assertEqual(interfaceconnection4.interface_a_id, data['interface_a']) - self.assertEqual(interfaceconnection4.interface_b_id, data['interface_b']) - - def test_create_interfaceconnection_bulk(self): - - data = [ - { - 'interface_a': self.interface7.pk, - 'interface_b': self.interface8.pk, - }, - { - 'interface_a': self.interface9.pk, - 'interface_b': self.interface10.pk, - }, - { - 'interface_a': self.interface11.pk, - 'interface_b': self.interface12.pk, - }, - ] - - url = reverse('dcim-api:interfaceconnection-list') - response = self.client.post(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_201_CREATED) - self.assertEqual(InterfaceConnection.objects.count(), 6) - for i in range(0, 3): - self.assertEqual(response.data[i]['interface_a']['id'], data[i]['interface_a']) - self.assertEqual(response.data[i]['interface_b']['id'], data[i]['interface_b']) - - def test_update_interfaceconnection(self): - - new_connection_status = not self.interfaceconnection1.connection_status - - data = { - 'interface_a': self.interface7.pk, - 'interface_b': self.interface8.pk, - 'connection_status': new_connection_status, - } - - url = reverse('dcim-api:interfaceconnection-detail', kwargs={'pk': self.interfaceconnection1.pk}) - response = self.client.put(url, data, format='json', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(InterfaceConnection.objects.count(), 3) - interfaceconnection1 = InterfaceConnection.objects.get(pk=response.data['id']) - self.assertEqual(interfaceconnection1.interface_a_id, data['interface_a']) - self.assertEqual(interfaceconnection1.interface_b_id, data['interface_b']) - self.assertEqual(interfaceconnection1.connection_status, data['connection_status']) - - def test_delete_interfaceconnection(self): - - url = reverse('dcim-api:interfaceconnection-detail', kwargs={'pk': self.interfaceconnection1.pk}) - response = self.client.delete(url, **self.header) - - self.assertHttpStatus(response, status.HTTP_204_NO_CONTENT) - self.assertEqual(InterfaceConnection.objects.count(), 2) - - -class ConnectedDeviceTest(APITestCase): - - def setUp(self): - - super(ConnectedDeviceTest, self).setUp() - - self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') - self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2') - manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') - self.devicetype1 = DeviceType.objects.create( - manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' - ) - self.devicetype2 = DeviceType.objects.create( - manufacturer=manufacturer, model='Test Device Type 2', slug='test-device-type-2' - ) - self.devicerole1 = DeviceRole.objects.create( - name='Test Device Role 1', slug='test-device-role-1', color='ff0000' - ) - self.devicerole2 = DeviceRole.objects.create( - name='Test Device Role 2', slug='test-device-role-2', color='00ff00' - ) - self.device1 = Device.objects.create( - device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice1', site=self.site1 - ) - self.device2 = Device.objects.create( - device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice2', site=self.site1 - ) - self.interface1 = Interface.objects.create(device=self.device1, name='eth0') - self.interface2 = Interface.objects.create(device=self.device2, name='eth0') - InterfaceConnection.objects.create(interface_a=self.interface1, interface_b=self.interface2) - - def test_get_connected_device(self): - - url = reverse('dcim-api:connected-device-list') - response = self.client.get(url + '?peer-device=TestDevice2&peer-interface=eth0', **self.header) - - self.assertHttpStatus(response, status.HTTP_200_OK) - self.assertEqual(response.data['name'], self.device1.name) +# class ConnectedDeviceTest(APITestCase): +# +# def setUp(self): +# +# super(ConnectedDeviceTest, self).setUp() +# +# self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1') +# self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2') +# manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') +# self.devicetype1 = DeviceType.objects.create( +# manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' +# ) +# self.devicetype2 = DeviceType.objects.create( +# manufacturer=manufacturer, model='Test Device Type 2', slug='test-device-type-2' +# ) +# self.devicerole1 = DeviceRole.objects.create( +# name='Test Device Role 1', slug='test-device-role-1', color='ff0000' +# ) +# self.devicerole2 = DeviceRole.objects.create( +# name='Test Device Role 2', slug='test-device-role-2', color='00ff00' +# ) +# self.device1 = Device.objects.create( +# device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice1', site=self.site1 +# ) +# self.device2 = Device.objects.create( +# device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice2', site=self.site1 +# ) +# self.interface1 = Interface.objects.create(device=self.device1, name='eth0') +# self.interface2 = Interface.objects.create(device=self.device2, name='eth0') +# InterfaceConnection.objects.create(interface_a=self.interface1, interface_b=self.interface2) +# +# def test_get_connected_device(self): +# +# url = reverse('dcim-api:connected-device-list') +# response = self.client.get(url + '?peer-device=TestDevice2&peer-interface=eth0', **self.header) +# +# self.assertHttpStatus(response, status.HTTP_200_OK) +# self.assertEqual(response.data['name'], self.device1.name) class VirtualChassisTest(APITestCase): diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 3e011b4c3..a7f52b80c 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -207,8 +207,9 @@ urlpatterns = [ url(r'^devices/(?P\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), url(r'^devices/(?P\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), url(r'^devices/(?P\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), - url(r'^devices/(?P\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'), - url(r'^interface-connections/(?P\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'), + # url(r'^devices/(?P\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'), + url(r'^interfaces/(?P\d+)/connect/$', views.CableConnectView.as_view(), name='interface_connect', kwargs={'endpoint_a_type': Interface}), + # url(r'^interface-connections/(?P\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'), url(r'^interfaces/(?P\d+)/$', views.InterfaceView.as_view(), name='interface'), url(r'^interfaces/(?P\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), url(r'^interfaces/(?P\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'), @@ -253,11 +254,11 @@ urlpatterns = [ # Console/power/interface connections url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), - url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'), + # url(r'^console-connections/import/$', views.ConsoleConnectionsBulkImportView.as_view(), name='console_connections_import'), url(r'^power-connections/$', views.PowerConnectionsListView.as_view(), name='power_connections_list'), - url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'), + # url(r'^power-connections/import/$', views.PowerConnectionsBulkImportView.as_view(), name='power_connections_import'), url(r'^interface-connections/$', views.InterfaceConnectionsListView.as_view(), name='interface_connections_list'), - url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'), + # url(r'^interface-connections/import/$', views.InterfaceConnectionsBulkImportView.as_view(), name='interface_connections_import'), # Virtual chassis url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 5dcb70c0e..991ec2ac0 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -6,7 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin from django.core.exceptions import ObjectDoesNotExist from django.core.paginator import EmptyPage, PageNotAnInteger from django.db import transaction -from django.db.models import Count, Q +from django.db.models import Count, F, Q from django.forms import modelformset_factory from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render @@ -33,10 +33,9 @@ from . import filters, forms, tables from .constants import CONNECTION_STATUS_CONNECTED from .models import ( Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, - DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, - InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, - PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, - VirtualChassis, + DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, + Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, + RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis, ) @@ -905,8 +904,7 @@ class DeviceView(View): interfaces = device.vc_interfaces.order_naturally( device.device_type.interface_ordering ).select_related( - 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', - 'circuit_termination__circuit' + 'connected_endpoint__device', 'circuit_termination__circuit' ).prefetch_related('ip_addresses') # Front panel ports @@ -999,7 +997,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View): interfaces = device.vc_interfaces.order_naturally( device.device_type.interface_ordering ).connectable().select_related( - 'connected_as_a', 'connected_as_b' + 'connected_endpoint__device' ) return render(request, 'dcim/device_lldp_neighbors.html', { @@ -1736,10 +1734,9 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView): form = forms.InterfaceBulkDisconnectForm def disconnect_objects(self, interfaces): - count, _ = InterfaceConnection.objects.filter( - Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces) - ).delete() - return count + return Interface.objects.filter(connected_endpoint__in=interfaces).update( + connected_endpoint=None, connection_status=None + ) class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): @@ -2016,115 +2013,6 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie default_return_url = 'dcim:device_list' -# -# Interface connections -# - -class InterfaceConnectionAddView(PermissionRequiredMixin, GetReturnURLMixin, View): - permission_required = 'dcim.add_interfaceconnection' - default_return_url = 'dcim:device_list' - - def get(self, request, pk): - - device = get_object_or_404(Device, pk=pk) - form = forms.InterfaceConnectionForm(device, initial={ - 'interface_a': request.GET.get('interface_a'), - 'site_b': request.GET.get('site_b'), - 'rack_b': request.GET.get('rack_b'), - 'device_b': request.GET.get('device_b'), - 'interface_b': request.GET.get('interface_b'), - }) - - return render(request, 'dcim/interfaceconnection_edit.html', { - 'device': device, - 'form': form, - 'return_url': device.get_absolute_url(), - }) - - def post(self, request, pk): - - device = get_object_or_404(Device, pk=pk) - form = forms.InterfaceConnectionForm(device, request.POST) - - if form.is_valid(): - - interfaceconnection = form.save() - msg = 'Connected {} {} to {} {}'.format( - interfaceconnection.interface_a.device.get_absolute_url(), - escape(interfaceconnection.interface_a.device), - escape(interfaceconnection.interface_a.name), - interfaceconnection.interface_b.device.get_absolute_url(), - escape(interfaceconnection.interface_b.device), - escape(interfaceconnection.interface_b.name), - ) - messages.success(request, mark_safe(msg)) - - if '_addanother' in request.POST: - base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk}) - device_b = interfaceconnection.interface_b.device - params = urlencode({ - 'rack_b': device_b.rack.pk if device_b.rack else '', - 'device_b': device_b.pk, - }) - return HttpResponseRedirect('{}?{}'.format(base_url, params)) - else: - return redirect('dcim:device', pk=device.pk) - - return render(request, 'dcim/interfaceconnection_edit.html', { - 'device': device, - 'form': form, - 'return_url': device.get_absolute_url(), - }) - - -class InterfaceConnectionDeleteView(PermissionRequiredMixin, GetReturnURLMixin, View): - permission_required = 'dcim.delete_interfaceconnection' - default_return_url = 'dcim:device_list' - - def get(self, request, pk): - - interfaceconnection = get_object_or_404(InterfaceConnection, pk=pk) - form = forms.ConfirmationForm() - - return render(request, 'dcim/interfaceconnection_delete.html', { - 'interfaceconnection': interfaceconnection, - 'form': form, - 'return_url': self.get_return_url(request, interfaceconnection), - }) - - def post(self, request, pk): - - interfaceconnection = get_object_or_404(InterfaceConnection, pk=pk) - form = forms.ConfirmationForm(request.POST) - - if form.is_valid(): - interfaceconnection.delete() - msg = 'Disconnected {} {} from {} {}'.format( - interfaceconnection.interface_a.device.get_absolute_url(), - escape(interfaceconnection.interface_a.device), - escape(interfaceconnection.interface_a.name), - interfaceconnection.interface_b.device.get_absolute_url(), - escape(interfaceconnection.interface_b.device), - escape(interfaceconnection.interface_b.name), - ) - messages.success(request, mark_safe(msg)) - - return redirect(self.get_return_url(request, interfaceconnection)) - - return render(request, 'dcim/interfaceconnection_delete.html', { - 'interfaceconnection': interfaceconnection, - 'form': form, - 'return_url': self.get_return_url(request, interfaceconnection), - }) - - -class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView): - permission_required = 'dcim.change_interface' - model_form = forms.InterfaceConnectionCSVForm - table = tables.InterfaceConnectionTable - default_return_url = 'dcim:interface_connections_list' - - # # Connections # @@ -2158,10 +2046,11 @@ class PowerConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView): - queryset = InterfaceConnection.objects.select_related( - 'interface_a__device', 'interface_b__device' - ).order_by( - 'interface_a__device__name', 'interface_a__name' + queryset = Interface.objects.select_related( + 'connected_endpoint__device', + ).filter( + connected_endpoint__isnull=False, + pk__lt=F('connected_endpoint'), ) filter = filters.InterfaceConnectionFilter filter_form = forms.InterfaceConnectionFilterForm diff --git a/netbox/extras/constants.py b/netbox/extras/constants.py index 2397ece7b..5c7939e68 100644 --- a/netbox/extras/constants.py +++ b/netbox/extras/constants.py @@ -49,7 +49,7 @@ GRAPH_TYPE_CHOICES = ( EXPORTTEMPLATE_MODELS = [ 'provider', 'circuit', # Circuits 'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM - 'consoleport', 'powerport', 'interfaceconnection', 'virtualchassis', # DCIM + 'consoleport', 'powerport', 'interface', 'virtualchassis', # DCIM 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM 'secret', # Secrets 'tenant', # Tenancy diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 8bc0a8726..abd9cf49d 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -504,15 +504,18 @@ class TopologyMap(models.Model): def add_network_connections(self, devices): from circuits.models import CircuitTermination - from dcim.models import InterfaceConnection + from dcim.models import Interface # Add all interface connections to the graph - connections = InterfaceConnection.objects.filter( - interface_a__device__in=devices, interface_b__device__in=devices + connected_interfaces = Interface.objects.select_related( + 'connected_endpoint__device' + ).filter( + Q(device__in=devices) | Q(connected_endpoint__device__in=devices), + connected_endpoint__isnull=False, ) - for c in connections: - style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed' - self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style) + 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): diff --git a/netbox/netbox/views.py b/netbox/netbox/views.py index e13a3df5a..b75debefe 100644 --- a/netbox/netbox/views.py +++ b/netbox/netbox/views.py @@ -1,6 +1,6 @@ from collections import OrderedDict -from django.db.models import Count +from django.db.models import Count, F from django.shortcuts import render from django.views.generic import View from rest_framework.response import Response @@ -14,8 +14,7 @@ from dcim.filters import ( DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter ) from dcim.models import ( - ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site, - VirtualChassis + ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis ) from dcim.tables import ( DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable @@ -157,6 +156,17 @@ class HomeView(View): def get(self, request): + connected_consoleports = ConsolePort.objects.filter( + connected_endpoint__isnull=False + ) + connected_powerports = PowerPort.objects.filter( + connected_endpoint__isnull=False + ) + connected_interfaces = Interface.objects.filter( + connected_endpoint__isnull=False, + pk__lt=F('connected_endpoint') + ) + stats = { # Organization @@ -166,9 +176,9 @@ class HomeView(View): # DCIM 'rack_count': Rack.objects.count(), 'device_count': Device.objects.count(), - 'interface_connections_count': InterfaceConnection.objects.count(), - 'console_connections_count': ConsolePort.objects.filter(connected_endpoint__isnull=False).count(), - 'power_connections_count': PowerPort.objects.filter(connected_endpoint__isnull=False).count(), + 'interface_connections_count': connected_interfaces.count(), + 'console_connections_count': connected_consoleports.count(), + 'power_connections_count': connected_powerports.count(), # IPAM 'vrf_count': VRF.objects.count(), diff --git a/netbox/templates/dcim/console_connections_list.html b/netbox/templates/dcim/console_connections_list.html index 89eb0822d..cf47d426c 100644 --- a/netbox/templates/dcim/console_connections_list.html +++ b/netbox/templates/dcim/console_connections_list.html @@ -3,9 +3,6 @@ {% block content %}
- {% if perms.dcim.change_consoleport %} - {% import_button 'dcim:console_connections_import' %} - {% endif %} {% export_button content_type %}

{% block title %}Console Connections{% endblock %}

diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 2d838704c..165a86595 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -549,7 +549,7 @@ Edit {% endif %} - {% if interfaces and perms.dcim.delete_interfaceconnection %} + {% if interfaces and perms.dcim.change_interface %} diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 229f6f2eb..723f14760 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -106,7 +106,7 @@ {% else %} - + {% endif %} diff --git a/netbox/templates/dcim/interface_connections_list.html b/netbox/templates/dcim/interface_connections_list.html index 950eb2f0b..e81eb55ee 100644 --- a/netbox/templates/dcim/interface_connections_list.html +++ b/netbox/templates/dcim/interface_connections_list.html @@ -3,9 +3,6 @@ {% block content %}
- {% if perms.dcim.add_interfaceconnection %} - {% import_button 'dcim:interface_connections_import' %} - {% endif %} {% export_button content_type %}

{% block title %}Interface Connections{% endblock %}

diff --git a/netbox/templates/dcim/power_connections_list.html b/netbox/templates/dcim/power_connections_list.html index 4e351eb6a..a41d571fb 100644 --- a/netbox/templates/dcim/power_connections_list.html +++ b/netbox/templates/dcim/power_connections_list.html @@ -3,9 +3,6 @@ {% block content %}
- {% if perms.dcim.change_powerport %} - {% import_button 'dcim:power_connections_import' %} - {% endif %} {% export_button content_type %}

{% block title %}Power Connections{% endblock %}

diff --git a/netbox/templates/inc/nav_menu.html b/netbox/templates/inc/nav_menu.html index aeddf1969..08ecb926d 100644 --- a/netbox/templates/inc/nav_menu.html +++ b/netbox/templates/inc/nav_menu.html @@ -180,27 +180,12 @@
  • - {% if perms.dcim.change_consoleport %} -
    - -
    - {% endif %} Console Connections
  • - {% if perms.dcim.change_powerport %} -
    - -
    - {% endif %} Power Connections
  • - {% if perms.dcim.add_interfaceconnection %} -
    - -
    - {% endif %} Interface Connections