Extended Cables to connect CircuitTerminations

This commit is contained in:
Jeremy Stretch 2018-10-30 12:16:22 -04:00
parent cbfb25f003
commit 4df74780b8
22 changed files with 305 additions and 199 deletions

View File

@ -3,7 +3,7 @@ from taggit_serializer.serializers import TaggitSerializer, TagListSerializerFie
from circuits.constants import CIRCUIT_STATUS_CHOICES from circuits.constants import CIRCUIT_STATUS_CHOICES
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from dcim.api.serializers import NestedSiteSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from tenancy.api.serializers import NestedTenantSerializer from tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
@ -85,10 +85,18 @@ class NestedCircuitSerializer(WritableNestedSerializer):
class CircuitTerminationSerializer(ValidatedModelSerializer): class CircuitTerminationSerializer(ValidatedModelSerializer):
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
site = NestedSiteSerializer() site = NestedSiteSerializer()
interface = InterfaceSerializer(required=False, allow_null=True)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'id', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
] ]
class NestedCircuitTerminationSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer()
class Meta:
model = CircuitTermination
fields = ['id', 'url', 'circuit', 'term_side']

View File

@ -67,6 +67,6 @@ class CircuitViewSet(CustomFieldModelViewSet):
# #
class CircuitTerminationViewSet(ModelViewSet): class CircuitTerminationViewSet(ModelViewSet):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device') queryset = CircuitTermination.objects.select_related('circuit', 'site')
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter filter_class = filters.CircuitTerminationFilter

View File

@ -2,7 +2,7 @@ from django import forms
from django.db.models import Count from django.db.models import Count
from taggit.forms import TagField from taggit.forms import TagField
from dcim.models import Site, Device, Interface, Rack from dcim.models import Site, Device, Rack
from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm from extras.forms import AddRemoveTagsForm, CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from tenancy.models import Tenant from tenancy.models import Tenant
@ -203,57 +203,12 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit terminations # Circuit terminations
# #
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm): class CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
widget=forms.Select(
attrs={'filter-for': 'rack'}
)
)
rack = ChainedModelChoiceField(
queryset=Rack.objects.all(),
chains=(
('site', 'site'),
),
required=False,
label='Rack',
widget=APISelect(
api_url='/api/dcim/racks/?site_id={{site}}',
attrs={'filter-for': 'device', 'nullable': 'true'}
)
)
device = ChainedModelChoiceField(
queryset=Device.objects.all(),
chains=(
('site', 'site'),
('rack', 'rack'),
),
required=False,
label='Device',
widget=APISelect(
api_url='/api/dcim/devices/?site_id={{site}}&rack_id={{rack}}',
display_field='display_name',
attrs={'filter-for': 'interface'}
)
)
interface = ChainedModelChoiceField(
queryset=Interface.objects.connectable().select_related('circuit_termination'),
chains=(
('device', 'device'),
),
required=False,
label='Interface',
widget=APISelect(
api_url='/api/dcim/interfaces/?device_id={{device}}&type=physical',
disabled_indicator='cable'
)
)
class Meta: class Meta:
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'pp_info',
] ]
help_texts = { help_texts = {
'port_speed': "Physical circuit speed", 'port_speed': "Physical circuit speed",
@ -263,25 +218,3 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
widgets = { widgets = {
'term_side': forms.HiddenInput(), 'term_side': forms.HiddenInput(),
} }
def __init__(self, *args, **kwargs):
# Initialize helper selectors
instance = kwargs.get('instance')
if instance and instance.interface is not None:
initial = kwargs.get('initial', {}).copy()
initial['rack'] = instance.interface.device.rack
initial['device'] = instance.interface.device
kwargs['initial'] = initial
super(CircuitTerminationForm, self).__init__(*args, **kwargs)
# Mark occupied interfaces as disabled
self.fields['interface'].choices = []
for iface in self.fields['interface'].queryset:
self.fields['interface'].choices.append(
(iface.id, {
'label': iface.name,
'disabled': bool(iface.cable) and iface.pk != self.initial.get('interface'),
})
)

View File

@ -0,0 +1,80 @@
from django.db import migrations, models
import django.db.models.deletion
from dcim.constants import CONNECTION_STATUS_CONNECTED
def circuit_terminations_to_cables(apps, schema_editor):
"""
Copy all existing CircuitTermination Interface associations as Cables
"""
ContentType = apps.get_model('contenttypes', 'ContentType')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
Interface = apps.get_model('dcim', 'Interface')
Cable = apps.get_model('dcim', 'Cable')
# Load content types
circuittermination_type = ContentType.objects.get_for_model(CircuitTermination)
interface_type = ContentType.objects.get_for_model(Interface)
# Create a new Cable instance from each console connection
print("\n Adding circuit terminations... ", end='', flush=True)
for circuittermination in CircuitTermination.objects.filter(interface__isnull=False):
c = Cable()
# We have to assign all fields manually because we're inside a migration.
c.termination_a_type = circuittermination_type
c.termination_a_id = circuittermination.id
c.termination_b_type = interface_type
c.termination_b_id = circuittermination.interface_id
c.connection_status = CONNECTION_STATUS_CONNECTED
c.save()
# Cache the connected Cable on the CircuitTermination
circuittermination.cable = c
circuittermination.connected_endpoint = circuittermination.interface
circuittermination.connection_status = CONNECTION_STATUS_CONNECTED
circuittermination.save()
# Cache the connected Cable on the Interface
interface = circuittermination.interface
interface.cable = c
interface._connected_circuittermination = circuittermination
interface.connection_status = CONNECTION_STATUS_CONNECTED
interface.save()
cable_count = Cable.objects.filter(termination_a_type=circuittermination_type).count()
print("{} cables created".format(cable_count))
class Migration(migrations.Migration):
atomic = False
dependencies = [
('circuits', '0012_change_logging'),
('dcim', '0066_cables'),
]
operations = [
# Add CircuitTermination.connected_endpoint
migrations.AddField(
model_name='circuittermination',
name='connected_endpoint',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'),
),
migrations.AddField(
model_name='circuittermination',
name='connection_status',
field=models.NullBooleanField(default=True),
),
# Copy CircuitTermination connections to Interfaces as Cables
migrations.RunPython(circuit_terminations_to_cables),
# Model changes
migrations.RemoveField(
model_name='circuittermination',
name='interface',
),
]

View File

@ -3,7 +3,7 @@ from django.db import models
from django.urls import reverse from django.urls import reverse
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.constants import STATUS_CLASSES from dcim.constants import CONNECTION_STATUS_CHOICES, CONNECTION_STATUS_CONNECTED, STATUS_CLASSES
from dcim.fields import ASNField from dcim.fields import ASNField
from extras.models import CustomFieldModel, ObjectChange from extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
@ -114,8 +114,8 @@ class CircuitType(ChangeLoggedModel):
class Circuit(ChangeLoggedModel, CustomFieldModel): class Circuit(ChangeLoggedModel, CustomFieldModel):
""" """
A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
circuits. Each circuit is also assigned a CircuitType and a Site. A Circuit may be terminated to a specific device circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps. in Kbps.
""" """
cid = models.CharField( cid = models.CharField(
max_length=50, max_length=50,
@ -227,13 +227,17 @@ class CircuitTermination(models.Model):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuit_terminations' related_name='circuit_terminations'
) )
interface = models.OneToOneField( connected_endpoint = models.OneToOneField(
to='dcim.Interface', to='dcim.Interface',
on_delete=models.PROTECT, on_delete=models.SET_NULL,
related_name='circuit_termination', related_name='+',
blank=True, blank=True,
null=True null=True
) )
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
default=CONNECTION_STATUS_CONNECTED
)
port_speed = models.PositiveIntegerField( port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)' verbose_name='Port speed (Kbps)'
) )

View File

@ -23,12 +23,6 @@ STATUS_LABEL = """
class CircuitTerminationColumn(tables.Column): class CircuitTerminationColumn(tables.Column):
def render(self, value): def render(self, value):
if value.interface:
return mark_safe('<a href="{}" title="{}">{}</a>'.format(
value.interface.device.get_absolute_url(),
value.site,
value.interface.device
))
return mark_safe('<a href="{}">{}</a>'.format( return mark_safe('<a href="{}">{}</a>'.format(
value.site.get_absolute_url(), value.site.get_absolute_url(),
value.site value.site

View File

@ -1,8 +1,9 @@
from django.conf.urls import url from django.conf.urls import url
from dcim.views import CableCreateView
from extras.views import ObjectChangeLogView from extras.views import ObjectChangeLogView
from . import views from . import views
from .models import Circuit, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
app_name = 'circuits' app_name = 'circuits'
urlpatterns = [ urlpatterns = [
@ -42,5 +43,6 @@ urlpatterns = [
url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'), url(r'^circuits/(?P<circuit>\d+)/terminations/add/$', views.CircuitTerminationCreateView.as_view(), name='circuittermination_add'),
url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), url(r'^circuit-terminations/(?P<pk>\d+)/edit/$', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), url(r'^circuit-terminations/(?P<pk>\d+)/delete/$', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
url(r'^circuit-terminations/(?P<termination_a_id>\d+)/connect/$', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
] ]

View File

@ -132,7 +132,7 @@ class CircuitListView(ObjectListView):
queryset = Circuit.objects.select_related( queryset = Circuit.objects.select_related(
'provider', 'type', 'tenant' 'provider', 'type', 'tenant'
).prefetch_related( ).prefetch_related(
'terminations__site', 'terminations__interface__device' 'terminations__site'
) )
filter = filters.CircuitFilter filter = filters.CircuitFilter
filter_form = forms.CircuitFilterForm filter_form = forms.CircuitFilterForm
@ -146,12 +146,12 @@ class CircuitView(View):
circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk) circuit = get_object_or_404(Circuit.objects.select_related('provider', 'type', 'tenant__group'), pk=pk)
termination_a = CircuitTermination.objects.select_related( termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=TERM_SIDE_A circuit=circuit, term_side=TERM_SIDE_A
).first() ).first()
termination_z = CircuitTermination.objects.select_related( termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device' 'site__region', 'connected_endpoint__device'
).filter( ).filter(
circuit=circuit, term_side=TERM_SIDE_Z circuit=circuit, term_side=TERM_SIDE_Z
).first() ).first()

View File

@ -696,8 +696,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
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(read_only=True) connected_endpoint = serializers.SerializerMethodField(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)
tagged_vlans = SerializedPKRelatedField( tagged_vlans = SerializedPKRelatedField(
@ -713,7 +712,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
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',
'connected_endpoint', 'circuit_termination', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags', 'connected_endpoint', 'cable', 'mode', 'untagged_vlan', 'tagged_vlans', 'tags',
] ]
def validate(self, data): def validate(self, data):
@ -735,6 +734,19 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
return super(InterfaceSerializer, self).validate(data) return super(InterfaceSerializer, self).validate(data)
def get_connected_endpoint(self, obj):
"""
Return the appropriate serializer for the type of connected object.
"""
if obj.connected_endpoint is None:
return None
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested')
context = {'request': self.context['request']}
data = serializer(obj.connected_endpoint, context=context).data
return data
# #
# Rear ports # Rear ports

View File

@ -1,7 +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.db.models import F, Q
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
@ -407,7 +407,7 @@ class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
class InterfaceViewSet(CableTraceMixin, ModelViewSet): class InterfaceViewSet(CableTraceMixin, ModelViewSet):
queryset = Interface.objects.select_related( queryset = Interface.objects.select_related(
'device', 'connected_endpoint__device', 'cable' 'device', '_connected_interface', '_connected_circuittermination', 'cable'
).prefetch_related( ).prefetch_related(
'tags' 'tags'
) )
@ -483,10 +483,11 @@ class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
class InterfaceConnectionViewSet(ModelViewSet): class InterfaceConnectionViewSet(ModelViewSet):
queryset = Interface.objects.select_related( queryset = Interface.objects.select_related(
'device', 'connected_endpoint__device' 'device', '_connected_interface', '_connected_circuittermination'
).filter( ).filter(
connected_endpoint__isnull=False, # Avoid duplicate connections by only selecting the lower PK in a connected pair
pk__lt=F('connected_endpoint') Q(_connected_interface__isnull=False, pk__lt=F('_connected_interface')) |
Q(_connected_circuittermination__isnull=False)
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filter_class = filters.InterfaceConnectionFilter filter_class = filters.InterfaceConnectionFilter

View File

@ -340,9 +340,10 @@ COMPATIBLE_TERMINATION_TYPES = {
'consoleserverport': ['consoleport', 'frontport', 'rearport'], 'consoleserverport': ['consoleport', 'frontport', 'rearport'],
'powerport': ['poweroutlet'], 'powerport': ['poweroutlet'],
'poweroutlet': ['powerport'], 'poweroutlet': ['powerport'],
'interface': ['interface', 'frontport', 'rearport'], 'interface': ['interface', 'circuittermination', 'frontport', 'rearport'],
'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'], 'frontport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'],
'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'], 'rearport': ['consoleport', 'consoleserverport', 'interface', 'frontport', 'rearport'],
'circuittermination': ['interface', 'frontport', 'rearport'],
} }
LENGTH_UNIT_METER = 'm' LENGTH_UNIT_METER = 'm'

View File

@ -1,6 +1,6 @@
import django_filters import django_filters
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Count, Q from django.db.models import Q
from netaddr import EUI from netaddr import EUI
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
@ -876,7 +876,7 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(device__site__slug=value) | Q(device__site__slug=value) |
Q(connected_endpoint__device__site__slug=value) Q(_connected_interface__device__site__slug=value)
) )
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
@ -884,5 +884,5 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(device__name__icontains=value) | Q(device__name__icontains=value) |
Q(connected_endpoint__device__name__icontains=value) Q(_connected_interface__device__name__icontains=value)
) )

View File

@ -88,28 +88,26 @@ def interface_connections_to_cables(apps, schema_editor):
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 all fields manually because we're inside a migration.
c.termination_a_type = interface_type c.termination_a_type = interface_type
c.termination_a_id = conn.interface_a_id c.termination_a_id = conn.interface_a_id
c.termination_a = conn.interface_a
c.termination_b_type = interface_type c.termination_b_type = interface_type
c.termination_b_id = conn.interface_b_id c.termination_b_id = conn.interface_b_id
c.termination_b = conn.interface_b
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 # Cache the connected Cable on each Interface
# since these are new fields on Interface interface_a = conn.interface_a
Interface.objects.filter(pk=conn.interface_a_id).update( interface_a._connected_interface = conn.interface_b
connected_endpoint=conn.interface_b_id, interface_a.connection_status = conn.connection_status
connection_status=conn.connection_status, interface_a.cable = c
cable=c interface_a.save()
)
Interface.objects.filter(pk=conn.interface_b_id).update( interface_b = conn.interface_b
connected_endpoint=conn.interface_a_id, interface_b._connected_interface = conn.interface_a
connection_status=conn.connection_status, interface_b.connection_status = conn.connection_status
cable=c interface_b.cable = c
) interface_b.save()
cable_count = Cable.objects.filter(termination_a_type=interface_type).count() cable_count = Cable.objects.filter(termination_a_type=interface_type).count()
print("{} cables created".format(cable_count)) print("{} cables created".format(cable_count))
@ -120,6 +118,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('contenttypes', '0002_remove_content_type_name'), ('contenttypes', '0002_remove_content_type_name'),
('circuits', '0006_terminations'),
('dcim', '0065_front_rear_ports'), ('dcim', '0065_front_rear_ports'),
] ]
@ -217,7 +216,12 @@ class Migration(migrations.Migration):
# Alter the Interface model # Alter the Interface model
migrations.AddField( migrations.AddField(
model_name='interface', model_name='interface',
name='connected_endpoint', name='_connected_circuittermination',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='circuits.CircuitTermination'),
),
migrations.AddField(
model_name='interface',
name='_connected_interface',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'), field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.Interface'),
), ),
migrations.AddField( migrations.AddField(

View File

@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from timezone_field import TimeZoneField from timezone_field import TimeZoneField
from circuits.models import Circuit from circuits.models import Circuit, CircuitTermination
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
from utilities.fields import ColorField, NullableCharField from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager from utilities.managers import NaturalOrderByManager
@ -1843,13 +1843,20 @@ class Interface(CableTermination, ComponentModel):
name = models.CharField( name = models.CharField(
max_length=64 max_length=64
) )
connected_endpoint = models.OneToOneField( _connected_interface = models.OneToOneField(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
related_name='+', related_name='+',
blank=True, blank=True,
null=True null=True
) )
_connected_circuittermination = models.OneToOneField(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField( connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES, choices=CONNECTION_STATUS_CHOICES,
default=CONNECTION_STATUS_CONNECTED default=CONNECTION_STATUS_CONNECTED
@ -2008,6 +2015,28 @@ class Interface(CableTermination, ComponentModel):
object_data=serialize_object(self) object_data=serialize_object(self)
).save() ).save()
@property
def connected_endpoint(self):
if self._connected_interface:
return self._connected_interface
return self._connected_circuittermination
@connected_endpoint.setter
def connected_endpoint(self, value):
if value is None:
self._connected_interface = None
self._connected_circuittermination = None
elif isinstance(value, Interface):
self._connected_interface = value
self._connected_circuittermination = None
elif isinstance(value, CircuitTermination):
self._connected_interface = None
self._connected_circuittermination = value
else:
raise ValueError(
"Connected endpoint must be an Interface or CircuitTermination, not {}.".format(type(value))
)
@property @property
def parent(self): def parent(self):
return self.device or self.virtual_machine return self.device or self.virtual_machine

View File

@ -904,7 +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(
'lag', 'connected_endpoint__device', 'circuit_termination__circuit', 'cable' 'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable'
).prefetch_related( ).prefetch_related(
'cable__termination_a', 'cable__termination_b', 'ip_addresses' 'cable__termination_a', 'cable__termination_b', 'ip_addresses'
) )
@ -999,7 +999,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_endpoint__device' '_connected_interface__device'
) )
return render(request, 'dcim/device_lldp_neighbors.html', { return render(request, 'dcim/device_lldp_neighbors.html', {
@ -1667,13 +1667,6 @@ class InterfaceView(View):
interface = get_object_or_404(Interface, pk=pk) interface = get_object_or_404(Interface, pk=pk)
# Get connected interface
connected_interface = interface.connected_endpoint
if connected_interface is None and hasattr(interface, 'circuit_termination'):
peer_termination = interface.circuit_termination.get_peer_termination()
if peer_termination is not None:
connected_interface = peer_termination.interface
# Get assigned IP addresses # Get assigned IP addresses
ipaddress_table = InterfaceIPAddressTable( ipaddress_table = InterfaceIPAddressTable(
data=interface.ip_addresses.select_related('vrf', 'tenant'), data=interface.ip_addresses.select_related('vrf', 'tenant'),
@ -1696,7 +1689,8 @@ class InterfaceView(View):
return render(request, 'dcim/interface.html', { return render(request, 'dcim/interface.html', {
'interface': interface, 'interface': interface,
'connected_interface': connected_interface, # TODO: Also handle connected CircuitTerminations
'connected_interface': interface._connected_interface,
'ipaddress_table': ipaddress_table, 'ipaddress_table': ipaddress_table,
'vlan_table': vlan_table, 'vlan_table': vlan_table,
}) })
@ -1736,8 +1730,8 @@ class InterfaceBulkDisconnectView(PermissionRequiredMixin, BulkDisconnectView):
form = forms.InterfaceBulkDisconnectForm form = forms.InterfaceBulkDisconnectForm
def disconnect_objects(self, interfaces): def disconnect_objects(self, interfaces):
return Interface.objects.filter(connected_endpoint__in=interfaces).update( return Interface.objects.filter(_connected_interface__in=interfaces).update(
connected_endpoint=None, connection_status=None _connected_interface=None, connection_status=None
) )
@ -2103,10 +2097,11 @@ class PowerConnectionsListView(ObjectListView):
class InterfaceConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView):
queryset = Interface.objects.select_related( queryset = Interface.objects.select_related(
'connected_endpoint__device', '_connected_interface', '_connected_circuittermination'
).filter( ).filter(
connected_endpoint__isnull=False, # Avoid duplicate connections by only selecting the lower PK in a connected pair
pk__lt=F('connected_endpoint'), _connected_interface__isnull=False,
pk__lt=F('_connected_interface')
) )
filter = filters.InterfaceConnectionFilter filter = filters.InterfaceConnectionFilter
filter_form = forms.InterfaceConnectionFilterForm filter_form = forms.InterfaceConnectionFilterForm

View File

@ -508,17 +508,17 @@ class TopologyMap(models.Model):
# Add all interface connections to the graph # Add all interface connections to the graph
connected_interfaces = Interface.objects.select_related( connected_interfaces = Interface.objects.select_related(
'connected_endpoint__device' '_connected_interface__device'
).filter( ).filter(
Q(device__in=devices) | Q(connected_endpoint__device__in=devices), Q(device__in=devices) | Q(_connected_interface__device__in=devices),
connected_endpoint__isnull=False, _connected_interface__isnull=False,
) )
for interface in connected_interfaces: for interface in connected_interfaces:
style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed' style = 'solid' if interface.connection_status == CONNECTION_STATUS_CONNECTED else 'dashed'
self.graph.edge(interface.device.name, interface.connected_endpoint.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', connected_endpoint__device__in=devices):
peer_termination = termination.get_peer_termination() peer_termination = termination.get_peer_termination()
if (peer_termination is not None and peer_termination.interface is not None and if (peer_termination is not None and peer_termination.interface is not None and
peer_termination.interface.device in devices): peer_termination.interface.device in devices):

View File

@ -163,8 +163,8 @@ class HomeView(View):
connected_endpoint__isnull=False connected_endpoint__isnull=False
) )
connected_interfaces = Interface.objects.filter( connected_interfaces = Interface.objects.filter(
connected_endpoint__isnull=False, _connected_interface__isnull=False,
pk__lt=F('connected_endpoint') pk__lt=F('_connected_interface')
) )
stats = { stats = {

View File

@ -41,9 +41,6 @@
</div> </div>
</div> </div>
{% render_field form.site %} {% render_field form.site %}
{% render_field form.rack %}
{% render_field form.device %}
{% render_field form.interface %}
</div> </div>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -39,10 +39,17 @@
<tr> <tr>
<td>Termination</td> <td>Termination</td>
<td> <td>
{% if termination.interface %} {% if termination.connected_endpoint %}
<a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a> <a href="{% url 'dcim:device' pk=termination.connected_endpoint.device.pk %}">{{ termination.connected_endpoint.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.interface }} <i class="fa fa-angle-right"></i> {{ termination.connected_endpoint }}
{% else %} {% else %}
{% if perms.circuits.change_circuittermination %}
<div class="pull-right">
<a href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk %}?return_url={{ circuit.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect">
<i class="glyphicon glyphicon-resize-small" aria-hidden="true"></i> Connect
</a>
</div>
{% endif %}
<span class="text-muted">Not defined</span> <span class="text-muted">Not defined</span>
{% endif %} {% endif %}
</td> </td>
@ -61,8 +68,8 @@
<tr> <tr>
<td>IP Addressing</td> <td>IP Addressing</td>
<td> <td>
{% if termination.interface %} {% if termination.connected_endpoint %}
{% for ip in termination.interface.ip_addresses.all %} {% for ip in termination.connected_endpoint.ip_addresses.all %}
{% if not forloop.first %}<br />{% endif %} {% if not forloop.first %}<br />{% endif %}
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }}) <a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
{% empty %} {% empty %}

View File

@ -29,30 +29,59 @@
<strong>A Side</strong> <strong>A Side</strong>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="form-group"> {% if termination_a.device %}
<label class="col-md-3 control-label required">Site</label> {# Device component #}
<div class="col-md-9"> <div class="form-group">
<p class="form-control-static">{{ termination_a.device.site }}</p> <label class="col-md-3 control-label required">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device.site }}</p>
</div>
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <label class="col-md-3 control-label required">Rack</label>
<label class="col-md-3 control-label required">Rack</label> <div class="col-md-9">
<div class="col-md-9"> <p class="form-control-static">{{ termination_a.device.rack|default:"None" }}</p>
<p class="form-control-static">{{ termination_a.device.rack|default:"None" }}</p> </div>
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <label class="col-md-3 control-label required">Device</label>
<label class="col-md-3 control-label required">Device</label> <div class="col-md-9">
<div class="col-md-9"> <p class="form-control-static">{{ termination_a.device }}</p>
<p class="form-control-static">{{ termination_a.device }}</p> </div>
</div> </div>
</div> <div class="form-group">
<div class="form-group"> <label class="col-md-3 control-label required">Name</label>
<label class="col-md-3 control-label required">Name</label> <div class="col-md-9">
<div class="col-md-9"> <p class="form-control-static">{{ termination_a }}</p>
<p class="form-control-static">{{ termination_a }}</p> </div>
</div> </div>
</div> {% else %}
{# Circuit termination #}
<div class="form-group">
<label class="col-md-3 control-label required">Site</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.site }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Provider</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.circuit.provider }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Circuit</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.circuit.cid }}</p>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Side</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.term_side }}</p>
</div>
</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,12 +1,29 @@
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr> {% if termination.device %}
<td>Device</td> {# Device component #}
<td> <tr>
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a> <td>Device</td>
</td> <td>
</tr> <a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
<tr> </td>
<td>Component</td> </tr>
<td>{{ termination }}</td> <tr>
</tr> <td>Component</td>
<td>{{ termination }}</td>
</tr>
{% else %}
{# Circuit termination #}
<tr>
<td>Provider</td>
<td>
<a href="{{ termination.circuit.provider.get_absolute_url }}">{{ termination.circuit.provider }}</a>
</td>
</tr>
<tr>
<td>Circuit</td>
<td>
<a href="{{ termination.circuit.get_absolute_url }}">{{ termination.circuit }}</a> (Side {{ termination.term_side }})
</td>
</tr>
{% endif %}
</table> </table>

View File

@ -50,17 +50,17 @@
<td colspan="2" class="text-muted">Virtual interface</td> <td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.is_wireless %} {% elif iface.is_wireless %}
<td colspan="2" class="text-muted">Wireless interface</td> <td colspan="2" class="text-muted">Wireless interface</td>
{% elif iface.connected_endpoint %} {% elif iface.connected_endpoint.name %}
{% with connected_iface=iface.connected_endpoint %} {# Connected to an Interface #}
<td> <td>
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a> <a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
</td> </td>
<td> <td>
<a href="{% url 'dcim:interface' pk=connected_iface.pk %}"><span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span></a> <a href="{% url 'dcim:interface' pk=iface.connected_endpoint.pk %}"><span title="{{ iface.connected_endpoint.get_form_factor_display }}">{{ iface.connected_endpoint }}</span></a>
</td> </td>
{% endwith %} {% elif iface.connected_endpoint.term_side %}
{% elif iface.circuit_termination %} {# Connected to a CircuitTermination #}
{% with iface.circuit_termination.get_peer_termination as peer_termination %} {% with iface.connected_endpoint.get_peer_termination as peer_termination %}
<td colspan="2"> <td colspan="2">
<i class="fa fa-fw fa-globe" title="Circuit"></i> <i class="fa fa-fw fa-globe" title="Circuit"></i>
{% if peer_termination %} {% if peer_termination %}
@ -72,7 +72,7 @@
{% endif %} {% endif %}
via via
{% endif %} {% endif %}
<a href="{% url 'circuits:circuit' pk=iface.circuit_termination.circuit_id %}">{{ iface.circuit_termination.circuit }}</a> <a href="{% url 'circuits:circuit' pk=iface.connected_endpoint.circuit_id %}">{{ iface.connected_endpoint.circuit }}</a>
</td> </td>
{% endwith %} {% endwith %}
{% else %} {% else %}
@ -84,7 +84,7 @@
{# Buttons #} {# Buttons #}
<td class="text-right text-nowrap"> <td class="text-right text-nowrap">
{% if show_graphs %} {% if show_graphs %}
{% if iface.circuit_termination or iface.connected_endpoint %} {% if iface.connected_endpoint %}
<button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs"> <button type="button" class="btn btn-primary btn-xs" data-toggle="modal" data-target="#graphs_modal" data-obj="{{ device.name }} - {{ iface.name }}" data-url="{% url 'dcim-api:interface-graphs' pk=iface.pk %}" title="Show graphs">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i> <i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</button> </button>
@ -110,13 +110,6 @@
<a href="{% url 'dcim:cable_delete' pk=iface.cable.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Remove cable"> <a href="{% url 'dcim:cable_delete' pk=iface.cable.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Remove cable">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i> <i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a> </a>
{% elif iface.circuit_termination and perms.circuits.change_circuittermination %}
<button class="btn btn-warning btn-xs interface-toggle connected" disabled="disabled" title="Circuits cannot be marked as planned or connected">
<i class="glyphicon glyphicon-ban-circle" aria-hidden="true"></i>
</button>
<a href="{% url 'circuits:circuittermination_edit' pk=iface.circuit_termination.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-danger btn-xs" title="Edit circuit termination">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</a>
{% else %} {% else %}
<a href="{% url 'dcim:interface_connect' termination_a_id=iface.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-success btn-xs" title="Connect"> <a href="{% url 'dcim:interface_connect' termination_a_id=iface.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>