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(
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'),
),

View File

@ -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
#

View File

@ -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)

View File

@ -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

View File

@ -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)
)

View File

@ -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
}
}
]

View File

@ -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
#

View File

@ -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',
),
]

View File

@ -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)
#

View File

@ -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')

View File

@ -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):

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/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+)/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'^devices/(?P<pk>\d+)/interface-connections/add/$', views.InterfaceConnectionAddView.as_view(), name='interfaceconnection_add'),
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+)/edit/$', views.InterfaceEditView.as_view(), name='interface_edit'),
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
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'),

View File

@ -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 <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
#
@ -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

View File

@ -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

View File

@ -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):

View File

@ -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(),

View File

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

View File

@ -549,7 +549,7 @@
<span class="glyphicon glyphicon-pencil" aria-hidden="true"></span> Edit
</button>
{% 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">
<span class="glyphicon glyphicon-resize-full" aria-hidden="true"></span> Disconnect
</button>

View File

@ -106,7 +106,7 @@
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% 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>
</a>
{% endif %}

View File

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

View File

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

View File

@ -180,27 +180,12 @@
<li class="divider"></li>
<li class="dropdown-header">Connections</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>
</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>
</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>
</li>
</ul>