Deprecated the InterfaceConnection model

This commit is contained in:
Jeremy Stretch 2018-10-24 13:59:44 -04:00
parent 2ec8dc8319
commit f30367e094
22 changed files with 219 additions and 884 deletions

View File

@ -237,9 +237,7 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
) )
) )
interface = ChainedModelChoiceField( interface = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related( queryset=Interface.objects.connectable().select_related('circuit_termination'),
'circuit_termination', 'connected_as_a', 'connected_as_b'
),
chains=( chains=(
('device', 'device'), ('device', 'device'),
), ),

View File

@ -6,10 +6,9 @@ from circuits.models import Circuit, CircuitTermination
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, DeviceBayTemplate, DeviceType, DeviceRole, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from ipam.models import IPAddress, VLAN from ipam.models import IPAddress, VLAN
@ -614,7 +613,7 @@ class IsConnectedMixin(object):
""" """
Return True if the interface has a connected interface or circuit. Return True if the interface has a connected interface or circuit.
""" """
if obj.connection: if obj.connected_endpoint:
return True return True
if hasattr(obj, 'circuit_termination') and obj.circuit_termination is not None: if hasattr(obj, 'circuit_termination') and obj.circuit_termination is not None:
return True return True
@ -662,8 +661,8 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False) form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True) lag = NestedInterfaceSerializer(required=False, allow_null=True)
connected_endpoint = NestedInterfaceSerializer(required=False, allow_null=True)
is_connected = serializers.SerializerMethodField(read_only=True) is_connected = serializers.SerializerMethodField(read_only=True)
interface_connection = serializers.SerializerMethodField(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True) circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True) mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True) untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
@ -679,7 +678,7 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri
model = Interface model = Interface
fields = [ fields = [
'id', 'device', 'name', 'form_factor', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', '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', 'tags',
] ]
@ -702,15 +701,6 @@ class InterfaceSerializer(TaggitSerializer, IsConnectedMixin, ValidatedModelSeri
return super(InterfaceSerializer, self).validate(data) 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 # Rear panel ports
@ -804,36 +794,17 @@ class InventoryItemSerializer(TaggitSerializer, ValidatedModelSerializer):
# #
class InterfaceConnectionSerializer(ValidatedModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = NestedInterfaceSerializer() interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer() interface_b = NestedInterfaceSerializer(source='connected_endpoint')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False)
class Meta: class Meta:
model = InterfaceConnection model = Interface
fields = ['id', 'interface_a', 'interface_b', 'connection_status'] fields = ['interface_a', 'interface_b', 'connection_status']
def get_interface_a(self, obj):
class NestedInterfaceConnectionSerializer(WritableNestedSerializer): context = {'request': self.context['request']}
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') return NestedInterfaceSerializer(instance=obj, context=context).data
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
# #

View File

@ -60,7 +60,7 @@ router.register(r'inventory-items', views.InventoryItemViewSet)
# Connections # Connections
router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections') router.register(r'console-connections', views.ConsoleConnectionViewSet, base_name='consoleconnections')
router.register(r'power-connections', views.PowerConnectionViewSet, base_name='powerconnections') 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 # Virtual chassis
router.register(r'virtual-chassis', views.VirtualChassisViewSet) router.register(r'virtual-chassis', views.VirtualChassisViewSet)

View File

@ -1,6 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.db.models import F
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from drf_yasg import openapi from drf_yasg import openapi
@ -14,10 +15,9 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
from dcim import filters from dcim import filters
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
from extras.api.serializers import RenderedGraphSerializer from extras.api.serializers import RenderedGraphSerializer
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
@ -35,8 +35,7 @@ class DCIMFieldChoicesViewSet(FieldChoicesViewSet):
fields = ( fields = (
(Device, ['face', 'status']), (Device, ['face', 'status']),
(ConsolePort, ['connection_status']), (ConsolePort, ['connection_status']),
(Interface, ['form_factor', 'mode']), (Interface, ['connection_status', 'form_factor', 'mode']),
(InterfaceConnection, ['connection_status']),
(InterfaceTemplate, ['form_factor']), (InterfaceTemplate, ['form_factor']),
(PowerPort, ['connection_status']), (PowerPort, ['connection_status']),
(Rack, ['type', 'width']), (Rack, ['type', 'width']),
@ -419,7 +418,12 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ModelViewSet): 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 serializer_class = serializers.InterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter filter_class = filters.InterfaceConnectionFilter

View File

@ -14,10 +14,9 @@ from .constants import (
) )
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate,
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
@ -853,21 +852,21 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
) )
class Meta: class Meta:
model = InterfaceConnection model = Interface
fields = ['connection_status'] fields = ['connection_status']
def filter_site(self, queryset, name, value): def filter_site(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(interface_a__device__site__slug=value) | Q(device__site__slug=value) |
Q(interface_b__device__site__slug=value) Q(connected_endpoint__device__site__slug=value)
) )
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
if not value.strip(): if not value.strip():
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(interface_a__device__name__icontains=value) | Q(device__name__icontains=value) |
Q(interface_b__device__name__icontains=value) Q(connected_endpoint__device__name__icontains=value)
) )

View File

@ -5746,158 +5746,5 @@
"mgmt_only": true, "mgmt_only": true,
"description": "" "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
}
} }
] ]

View File

@ -26,10 +26,9 @@ from virtualization.models import Cluster
from .constants import * from .constants import *
from .models import ( from .models import (
Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Cable, DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate,
Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, Device, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate, Manufacturer,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
DEVICE_BY_PK_RE = r'{\d+\}' DEVICE_BY_PK_RE = r'{\d+\}'
@ -2017,173 +2016,6 @@ class InterfaceBulkDisconnectForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Interface.objects.all(), widget=forms.MultipleHiddenInput) 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 # Front panel ports
# #

View File

@ -17,6 +17,7 @@ def console_connections_to_cables(apps, schema_editor):
consoleserverport_type = ContentType.objects.get_for_model(ConsoleServerPort) consoleserverport_type = ContentType.objects.get_for_model(ConsoleServerPort)
# Create a new Cable instance from each console connection # 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): for consoleport in ConsolePort.objects.filter(connected_endpoint__isnull=False):
c = Cable() c = Cable()
# We have to assign GFK fields manually because we're inside a migration. # 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.connection_status = consoleport.connection_status
c.save() 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): 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) poweroutlet_type = ContentType.objects.get_for_model(PowerOutlet)
# Create a new Cable instance from each power connection # 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): for powerport in PowerPort.objects.filter(connected_endpoint__isnull=False):
c = Cable() c = Cable()
# We have to assign GFK fields manually because we're inside a migration. # 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.connection_status = powerport.connection_status
c.save() 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): 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) interface_type = ContentType.objects.get_for_model(Interface)
# Create a new Cable instance from each InterfaceConnection # Create a new Cable instance from each InterfaceConnection
print(" Adding interface connections... ", end='', flush=True)
for conn in InterfaceConnection.objects.all(): for conn in InterfaceConnection.objects.all():
c = Cable() c = Cable()
# We have to assign GFK fields manually because we're inside a migration. # 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.connection_status = conn.connection_status
c.save() 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): class Migration(migrations.Migration):
atomic = False
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ('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'), 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 # Copy console/power/interface connections as Cables
migrations.RunPython(console_connections_to_cables), migrations.RunPython(console_connections_to_cables),
migrations.RunPython(power_connections_to_cables), migrations.RunPython(power_connections_to_cables),
migrations.RunPython(interface_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',
),
] ]

View File

@ -1826,7 +1826,7 @@ class PowerOutlet(ComponentModel):
class Interface(ComponentModel): class Interface(ComponentModel):
""" """
A network interface within a Device or VirtualMachine. A physical Interface can connect to exactly one other 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( device = models.ForeignKey(
to='Device', to='Device',
@ -1842,6 +1842,20 @@ class Interface(ComponentModel):
null=True, null=True,
blank=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( lag = models.ForeignKey(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -1850,9 +1864,6 @@ class Interface(ComponentModel):
blank=True, blank=True,
verbose_name='Parent LAG' verbose_name='Parent LAG'
) )
name = models.CharField(
max_length=64
)
form_factor = models.PositiveSmallIntegerField( form_factor = models.PositiveSmallIntegerField(
choices=IFACE_FF_CHOICES, choices=IFACE_FF_CHOICES,
default=IFACE_FF_10GE_SFP_PLUS default=IFACE_FF_10GE_SFP_PLUS
@ -2002,10 +2013,7 @@ class Interface(ComponentModel):
changed_object=self, changed_object=self,
related_object=parent_obj, related_object=parent_obj,
action=action, action=action,
object_data=serialize_object(self, extra={ object_data=serialize_object(self)
'connected_interface': self.connected_interface.pk if self.connection else None,
'connection_status': self.connection.connection_status if self.connection else None,
})
).save() ).save()
@property @property
@ -2034,140 +2042,7 @@ class Interface(ComponentModel):
return bool(self.circuit_termination) return bool(self.circuit_termination)
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
return bool(self.connection) return bool(self.connected_endpoint)
@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()
# #

View File

@ -5,10 +5,9 @@ from tenancy.tables import COL_TENANT
from utilities.tables import BaseTable, BooleanColumn, ToggleColumn from utilities.tables import BaseTable, BooleanColumn, ToggleColumn
from .models import ( from .models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate,
InterfaceTemplate, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, InventoryItem, Manufacturer, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerPortTemplate, Rack, RackGroup, RackReservation, RearPanelPort, RearPanelPortTemplate, Region, Site, RackGroup, RackReservation, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
REGION_LINK = """ REGION_LINK = """
@ -654,17 +653,33 @@ class PowerConnectionTable(BaseTable):
class InterfaceConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable):
device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'), device_a = tables.LinkColumn(
args=[Accessor('interface_a.device.pk')], verbose_name='Device A') viewname='dcim:device',
interface_a = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_a'), accessor=Accessor('device'),
args=[Accessor('interface_a.pk')], verbose_name='Interface A') args=[Accessor('device.pk')],
device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'), verbose_name='Device A'
args=[Accessor('interface_b.device.pk')], verbose_name='Device B') )
interface_b = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_b'), interface_a = tables.LinkColumn(
args=[Accessor('interface_b.pk')], verbose_name='Interface B') 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): class Meta(BaseTable.Meta):
model = InterfaceConnection model = Interface
fields = ('device_a', 'interface_a', 'device_b', 'interface_b') fields = ('device_a', 'interface_a', 'device_b', 'interface_b')

View File

@ -8,7 +8,7 @@ from dcim.constants import (
) )
from dcim.models import ( from dcim.models import (
ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, 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, InventoryItem, Platform, PowerPort, PowerPortTemplate, PowerOutlet, PowerOutletTemplate, Rack, RackGroup,
RackReservation, RackRole, Region, Site, VirtualChassis, RackReservation, RackRole, Region, Site, VirtualChassis,
) )
@ -2393,6 +2393,7 @@ class InterfaceTest(APITestCase):
url = reverse('dcim-api:interface-detail', kwargs={'pk': self.interface1.pk}) url = reverse('dcim-api:interface-detail', kwargs={'pk': self.interface1.pk})
response = self.client.get(url, **self.header) response = self.client.get(url, **self.header)
self.assertHttpStatus(response, status.HTTP_200_OK)
self.assertEqual(response.data['name'], self.interface1.name) self.assertEqual(response.data['name'], self.interface1.name)
def test_get_interface_graphs(self): def test_get_interface_graphs(self):
@ -2882,179 +2883,44 @@ class PowerConnectionTest(APITestCase):
self.assertEqual(response.data['count'], 3) self.assertEqual(response.data['count'], 3)
class InterfaceConnectionTest(APITestCase): # class ConnectedDeviceTest(APITestCase):
#
def setUp(self): # def setUp(self):
#
super(InterfaceConnectionTest, self).setUp() # super(ConnectedDeviceTest, self).setUp()
#
site = Site.objects.create(name='Test Site 1', slug='test-site-1') # self.site1 = Site.objects.create(name='Test Site 1', slug='test-site-1')
manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1') # self.site2 = Site.objects.create(name='Test Site 2', slug='test-site-2')
devicetype = DeviceType.objects.create( # manufacturer = Manufacturer.objects.create(name='Test Manufacturer 1', slug='test-manufacturer-1')
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1' # self.devicetype1 = 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.devicetype2 = DeviceType.objects.create(
) # manufacturer=manufacturer, model='Test Device Type 2', slug='test-device-type-2'
self.device = Device.objects.create( # )
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=site # self.devicerole1 = DeviceRole.objects.create(
) # name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
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.devicerole2 = DeviceRole.objects.create(
self.interface3 = Interface.objects.create(device=self.device, name='Test Interface 3') # name='Test Device Role 2', slug='test-device-role-2', color='00ff00'
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.device1 = Device.objects.create(
self.interface6 = Interface.objects.create(device=self.device, name='Test Interface 6') # device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice1', site=self.site1
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.device2 = Device.objects.create(
self.interface9 = Interface.objects.create(device=self.device, name='Test Interface 9') # device_type=self.devicetype1, device_role=self.devicerole1, name='TestDevice2', site=self.site1
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.interface1 = Interface.objects.create(device=self.device1, name='eth0')
self.interface12 = Interface.objects.create(device=self.device, name='Test Interface 12') # self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
self.interfaceconnection1 = InterfaceConnection.objects.create( # InterfaceConnection.objects.create(interface_a=self.interface1, interface_b=self.interface2)
interface_a=self.interface1, interface_b=self.interface2 #
) # def test_get_connected_device(self):
self.interfaceconnection2 = InterfaceConnection.objects.create( #
interface_a=self.interface3, interface_b=self.interface4 # url = reverse('dcim-api:connected-device-list')
) # response = self.client.get(url + '?peer-device=TestDevice2&peer-interface=eth0', **self.header)
self.interfaceconnection3 = InterfaceConnection.objects.create( #
interface_a=self.interface5, interface_b=self.interface6 # self.assertHttpStatus(response, status.HTTP_200_OK)
) # self.assertEqual(response.data['name'], self.device1.name)
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 VirtualChassisTest(APITestCase): class VirtualChassisTest(APITestCase):

View File

@ -207,8 +207,9 @@ urlpatterns = [
url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'), url(r'^devices/(?P<pk>\d+)/interfaces/edit/$', views.InterfaceBulkEditView.as_view(), name='interface_bulk_edit'),
url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'), url(r'^devices/(?P<pk>\d+)/interfaces/disconnect/$', views.InterfaceBulkDisconnectView.as_view(), name='interface_bulk_disconnect'),
url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'), url(r'^devices/(?P<pk>\d+)/interfaces/delete/$', views.InterfaceBulkDeleteView.as_view(), name='interface_bulk_delete'),
url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'), # url(r'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'), url(r'^interfaces/(?P<endpoint_a_id>\d+)/connect/$', views.CableConnectView.as_view(), name='interface_connect', kwargs={'endpoint_a_type': Interface}),
# url(r'^interface-connections/(?P<pk>\d+)/delete/$', views.InterfaceConnectionDeleteView.as_view(), name='interfaceconnection_delete'),
url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'), url(r'^interfaces/(?P<pk>\d+)/$', views.InterfaceView.as_view(), name='interface'),
url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'), url(r'^interfaces/(?P<pk>\d+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'), url(r'^interfaces/(?P<pk>\d+)/assign-vlans/$', views.InterfaceAssignVLANsView.as_view(), name='interface_assign_vlans'),
@ -253,11 +254,11 @@ urlpatterns = [
# Console/power/interface connections # Console/power/interface connections
url(r'^console-connections/$', views.ConsoleConnectionsListView.as_view(), name='console_connections_list'), 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/$', 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/$', 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 # Virtual chassis
url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'), url(r'^virtual-chassis/$', views.VirtualChassisListView.as_view(), name='virtualchassis_list'),

View File

@ -6,7 +6,7 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.paginator import EmptyPage, PageNotAnInteger from django.core.paginator import EmptyPage, PageNotAnInteger
from django.db import transaction 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.forms import modelformset_factory
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render 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 .constants import CONNECTION_STATUS_CONNECTED
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceConnection, DeviceBayTemplate, DeviceRole, DeviceType, FrontPanelPort, FrontPanelPortTemplate, Interface, InterfaceTemplate,
InterfaceTemplate, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, RackGroup, RackReservation, RackRole, RearPanelPort, RearPanelPortTemplate, Region, Site, VirtualChassis,
VirtualChassis,
) )
@ -905,8 +904,7 @@ class DeviceView(View):
interfaces = device.vc_interfaces.order_naturally( interfaces = device.vc_interfaces.order_naturally(
device.device_type.interface_ordering device.device_type.interface_ordering
).select_related( ).select_related(
'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device', 'connected_endpoint__device', 'circuit_termination__circuit'
'circuit_termination__circuit'
).prefetch_related('ip_addresses') ).prefetch_related('ip_addresses')
# Front panel ports # Front panel ports
@ -999,7 +997,7 @@ class DeviceLLDPNeighborsView(PermissionRequiredMixin, View):
interfaces = device.vc_interfaces.order_naturally( interfaces = device.vc_interfaces.order_naturally(
device.device_type.interface_ordering device.device_type.interface_ordering
).connectable().select_related( ).connectable().select_related(
'connected_as_a', 'connected_as_b' 'connected_endpoint__device'
) )
return render(request, 'dcim/device_lldp_neighbors.html', { return render(request, 'dcim/device_lldp_neighbors.html', {
@ -1736,10 +1734,9 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
form = forms.InterfaceBulkDisconnectForm form = forms.InterfaceBulkDisconnectForm
def disconnect_objects(self, interfaces): def disconnect_objects(self, interfaces):
count, _ = InterfaceConnection.objects.filter( return Interface.objects.filter(connected_endpoint__in=interfaces).update(
Q(interface_a__in=interfaces) | Q(interface_b__in=interfaces) connected_endpoint=None, connection_status=None
).delete() )
return count
class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView): class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
@ -2016,115 +2013,6 @@ class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, BulkComponentCreateVie
default_return_url = 'dcim:device_list' 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 <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.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 <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.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 # Connections
# #
@ -2158,10 +2046,11 @@ class PowerConnectionsListView(ObjectListView):
class InterfaceConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView):
queryset = InterfaceConnection.objects.select_related( queryset = Interface.objects.select_related(
'interface_a__device', 'interface_b__device' 'connected_endpoint__device',
).order_by( ).filter(
'interface_a__device__name', 'interface_a__name' connected_endpoint__isnull=False,
pk__lt=F('connected_endpoint'),
) )
filter = filters.InterfaceConnectionFilter filter = filters.InterfaceConnectionFilter
filter_form = forms.InterfaceConnectionFilterForm filter_form = forms.InterfaceConnectionFilterForm

View File

@ -49,7 +49,7 @@ GRAPH_TYPE_CHOICES = (
EXPORTTEMPLATE_MODELS = [ EXPORTTEMPLATE_MODELS = [
'provider', 'circuit', # Circuits 'provider', 'circuit', # Circuits
'site', 'region', 'rack', 'rackgroup', 'manufacturer', 'devicetype', 'device', # DCIM '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 'aggregate', 'prefix', 'ipaddress', 'vlan', 'vrf', 'service', # IPAM
'secret', # Secrets 'secret', # Secrets
'tenant', # Tenancy 'tenant', # Tenancy

View File

@ -504,15 +504,18 @@ class TopologyMap(models.Model):
def add_network_connections(self, devices): def add_network_connections(self, devices):
from circuits.models import CircuitTermination from circuits.models import CircuitTermination
from dcim.models import InterfaceConnection from dcim.models import Interface
# Add all interface connections to the graph # Add all interface connections to the graph
connections = InterfaceConnection.objects.filter( connected_interfaces = Interface.objects.select_related(
interface_a__device__in=devices, interface_b__device__in=devices 'connected_endpoint__device'
).filter(
Q(device__in=devices) | Q(connected_endpoint__device__in=devices),
connected_endpoint__isnull=False,
) )
for c in connections: for interface in connected_interfaces:
style = 'solid' if c.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed' style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(c.interface_a.device.name, c.interface_b.device.name, style=style) self.graph.edge(interface.device.name, interface.connected_endpoint.device.name, style=style)
# Add all circuits to the graph # 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', interface__device__in=devices):

View File

@ -1,6 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from django.db.models import Count from django.db.models import Count, F
from django.shortcuts import render from django.shortcuts import render
from django.views.generic import View from django.views.generic import View
from rest_framework.response import Response from rest_framework.response import Response
@ -14,8 +14,7 @@ from dcim.filters import (
DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter DeviceFilter, DeviceTypeFilter, RackFilter, RackGroupFilter, SiteFilter, VirtualChassisFilter
) )
from dcim.models import ( from dcim.models import (
ConsolePort, Device, DeviceType, InterfaceConnection, PowerPort, Rack, RackGroup, Site, ConsolePort, Device, DeviceType, Interface, PowerPort, Rack, RackGroup, Site, VirtualChassis
VirtualChassis
) )
from dcim.tables import ( from dcim.tables import (
DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable DeviceDetailTable, DeviceTypeTable, RackTable, RackGroupTable, SiteTable, VirtualChassisTable
@ -157,6 +156,17 @@ class HomeView(View):
def get(self, request): 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 = { stats = {
# Organization # Organization
@ -166,9 +176,9 @@ class HomeView(View):
# DCIM # DCIM
'rack_count': Rack.objects.count(), 'rack_count': Rack.objects.count(),
'device_count': Device.objects.count(), 'device_count': Device.objects.count(),
'interface_connections_count': InterfaceConnection.objects.count(), 'interface_connections_count': connected_interfaces.count(),
'console_connections_count': ConsolePort.objects.filter(connected_endpoint__isnull=False).count(), 'console_connections_count': connected_consoleports.count(),
'power_connections_count': PowerPort.objects.filter(connected_endpoint__isnull=False).count(), 'power_connections_count': connected_powerports.count(),
# IPAM # IPAM
'vrf_count': VRF.objects.count(), 'vrf_count': VRF.objects.count(),

View File

@ -3,9 +3,6 @@
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_consoleport %}
{% import_button 'dcim:console_connections_import' %}
{% endif %}
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Console Connections{% endblock %}</h1> <h1>{% block title %}Console Connections{% endblock %}</h1>

View File

@ -549,7 +549,7 @@
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button> </button>
{% endif %} {% endif %}
{% if interfaces and perms.dcim.delete_interfaceconnection %} {% if interfaces and perms.dcim.change_interface %}
<button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs"> <button type="submit" name="_disconnect" formaction="{% url 'dcim:interface_bulk_disconnect' pk=device.pk %}" class="btn btn-danger btn-xs">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect <span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button> </button>

View File

@ -106,7 +106,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% else %} {% else %}
<a href="{% url 'dcim:interfaceconnection_add' pk=device.pk %}?interface_a={{ iface.pk }}&return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect"> <a href="{% url 'dcim:interface_connect' endpoint_a_id=device.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i>
</a> </a>
{% endif %} {% endif %}

View File

@ -3,9 +3,6 @@
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.add_interfaceconnection %}
{% import_button 'dcim:interface_connections_import' %}
{% endif %}
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Interface Connections{% endblock %}</h1> <h1>{% block title %}Interface Connections{% endblock %}</h1>

View File

@ -3,9 +3,6 @@
{% block content %} {% block content %}
<div class="pull-right"> <div class="pull-right">
{% if perms.dcim.change_powerport %}
{% import_button 'dcim:power_connections_import' %}
{% endif %}
{% export_button content_type %} {% export_button content_type %}
</div> </div>
<h1>{% block title %}Power Connections{% endblock %}</h1> <h1>{% block title %}Power Connections{% endblock %}</h1>

View File

@ -180,27 +180,12 @@
<li class="divider"></li> <li class="divider"></li>
<li class="dropdown-header">Connections</li> <li class="dropdown-header">Connections</li>
<li> <li>
{% if perms.dcim.change_consoleport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:console_connections_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:console_connections_list' %}">Console Connections</a> <a href="{% url 'dcim:console_connections_list' %}">Console Connections</a>
</li> </li>
<li> <li>
{% if perms.dcim.change_powerport %}
<div class="buttons pull-right">
<a href="{% url 'dcim:power_connections_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:power_connections_list' %}">Power Connections</a> <a href="{% url 'dcim:power_connections_list' %}">Power Connections</a>
</li> </li>
<li> <li>
{% if perms.dcim.add_interfaceconnection %}
<div class="buttons pull-right">
<a href="{% url 'dcim:interface_connections_import' %}" class="btn btn-xs btn-info" title="Import"><i class="fa fa-download"></i></a>
</div>
{% endif %}
<a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a> <a href="{% url 'dcim:interface_connections_list' %}">Interface Connections</a>
</li> </li>
</ul> </ul>