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.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 tenancy.api.serializers import NestedTenantSerializer
from utilities.api import ChoiceField, ValidatedModelSerializer, WritableNestedSerializer
@ -85,10 +85,18 @@ class NestedCircuitSerializer(WritableNestedSerializer):
class CircuitTerminationSerializer(ValidatedModelSerializer):
circuit = NestedCircuitSerializer()
site = NestedSiteSerializer()
interface = InterfaceSerializer(required=False, allow_null=True)
class Meta:
model = CircuitTermination
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):
queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device')
queryset = CircuitTermination.objects.select_related('circuit', 'site')
serializer_class = serializers.CircuitTerminationSerializer
filter_class = filters.CircuitTerminationFilter

View File

@ -2,7 +2,7 @@ from django import forms
from django.db.models import Count
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 tenancy.forms import TenancyForm
from tenancy.models import Tenant
@ -203,57 +203,12 @@ class CircuitFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Circuit terminations
#
class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, 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 CircuitTerminationForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = CircuitTermination
fields = [
'term_side', 'site', 'rack', 'device', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id',
'pp_info',
'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
]
help_texts = {
'port_speed': "Physical circuit speed",
@ -263,25 +218,3 @@ class CircuitTerminationForm(BootstrapMixin, ChainedFieldsMixin, forms.ModelForm
widgets = {
'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 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 extras.models import CustomFieldModel, ObjectChange
from utilities.models import ChangeLoggedModel
@ -114,8 +114,8 @@ class CircuitType(ChangeLoggedModel):
class Circuit(ChangeLoggedModel, CustomFieldModel):
"""
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
interface, but this is not required. Circuit port speed and commit rate are measured in Kbps.
circuits. Each circuit is also assigned a CircuitType and a Site. Circuit port speed and commit rate are measured
in Kbps.
"""
cid = models.CharField(
max_length=50,
@ -227,13 +227,17 @@ class CircuitTermination(models.Model):
on_delete=models.PROTECT,
related_name='circuit_terminations'
)
interface = models.OneToOneField(
connected_endpoint = models.OneToOneField(
to='dcim.Interface',
on_delete=models.PROTECT,
related_name='circuit_termination',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
default=CONNECTION_STATUS_CONNECTED
)
port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)'
)

View File

@ -23,12 +23,6 @@ STATUS_LABEL = """
class CircuitTerminationColumn(tables.Column):
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(
value.site.get_absolute_url(),
value.site

View File

@ -1,8 +1,9 @@
from django.conf.urls import url
from dcim.views import CableCreateView
from extras.views import ObjectChangeLogView
from . import views
from .models import Circuit, CircuitType, Provider
from .models import Circuit, CircuitTermination, CircuitType, Provider
app_name = 'circuits'
urlpatterns = [
@ -42,5 +43,6 @@ urlpatterns = [
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+)/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(
'provider', 'type', 'tenant'
).prefetch_related(
'terminations__site', 'terminations__interface__device'
'terminations__site'
)
filter = filters.CircuitFilter
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)
termination_a = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_A
).first()
termination_z = CircuitTermination.objects.select_related(
'site__region', 'interface__device'
'site__region', 'connected_endpoint__device'
).filter(
circuit=circuit, term_side=TERM_SIDE_Z
).first()

View File

@ -696,8 +696,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
device = NestedDeviceSerializer()
form_factor = ChoiceField(choices=IFACE_FF_CHOICES, required=False)
lag = NestedInterfaceSerializer(required=False, allow_null=True)
connected_endpoint = NestedInterfaceSerializer(read_only=True)
circuit_termination = InterfaceCircuitTerminationSerializer(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True)
mode = ChoiceField(choices=IFACE_MODE_CHOICES, required=False, allow_null=True)
untagged_vlan = InterfaceVLANSerializer(required=False, allow_null=True)
tagged_vlans = SerializedPKRelatedField(
@ -713,7 +712,7 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
model = Interface
fields = [
'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):
@ -735,6 +734,19 @@ class InterfaceSerializer(TaggitSerializer, ValidatedModelSerializer):
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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import django_filters
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.core import AddrFormatError
@ -876,7 +876,7 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
return queryset
return queryset.filter(
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):
@ -884,5 +884,5 @@ class InterfaceConnectionFilter(django_filters.FilterSet):
return queryset
return queryset.filter(
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():
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_id = conn.interface_a_id
c.termination_a = conn.interface_a
c.termination_b_type = interface_type
c.termination_b_id = conn.interface_b_id
c.termination_b = conn.interface_b
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,
cable=c
)
Interface.objects.filter(pk=conn.interface_b_id).update(
connected_endpoint=conn.interface_a_id,
connection_status=conn.connection_status,
cable=c
)
# Cache the connected Cable on each Interface
interface_a = conn.interface_a
interface_a._connected_interface = conn.interface_b
interface_a.connection_status = conn.connection_status
interface_a.cable = c
interface_a.save()
interface_b = conn.interface_b
interface_b._connected_interface = conn.interface_a
interface_b.connection_status = conn.connection_status
interface_b.cable = c
interface_b.save()
cable_count = Cable.objects.filter(termination_a_type=interface_type).count()
print("{} cables created".format(cable_count))
@ -120,6 +118,7 @@ class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('circuits', '0006_terminations'),
('dcim', '0065_front_rear_ports'),
]
@ -217,7 +216,12 @@ class Migration(migrations.Migration):
# Alter the Interface model
migrations.AddField(
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'),
),
migrations.AddField(

View File

@ -15,7 +15,7 @@ from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager
from timezone_field import TimeZoneField
from circuits.models import Circuit
from circuits.models import Circuit, CircuitTermination
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange
from utilities.fields import ColorField, NullableCharField
from utilities.managers import NaturalOrderByManager
@ -1843,13 +1843,20 @@ class Interface(CableTermination, ComponentModel):
name = models.CharField(
max_length=64
)
connected_endpoint = models.OneToOneField(
_connected_interface = models.OneToOneField(
to='self',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
_connected_circuittermination = models.OneToOneField(
to='circuits.CircuitTermination',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.NullBooleanField(
choices=CONNECTION_STATUS_CHOICES,
default=CONNECTION_STATUS_CONNECTED
@ -2008,6 +2015,28 @@ class Interface(CableTermination, ComponentModel):
object_data=serialize_object(self)
).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
def parent(self):
return self.device or self.virtual_machine

View File

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

View File

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

View File

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

View File

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

View File

@ -39,10 +39,17 @@
<tr>
<td>Termination</td>
<td>
{% if termination.interface %}
<a href="{% url 'dcim:device' pk=termination.interface.device.pk %}">{{ termination.interface.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.interface }}
{% if termination.connected_endpoint %}
<a href="{% url 'dcim:device' pk=termination.connected_endpoint.device.pk %}">{{ termination.connected_endpoint.device }}</a>
<i class="fa fa-angle-right"></i> {{ termination.connected_endpoint }}
{% 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>
{% endif %}
</td>
@ -61,8 +68,8 @@
<tr>
<td>IP Addressing</td>
<td>
{% if termination.interface %}
{% for ip in termination.interface.ip_addresses.all %}
{% if termination.connected_endpoint %}
{% for ip in termination.connected_endpoint.ip_addresses.all %}
{% if not forloop.first %}<br />{% endif %}
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
{% empty %}

View File

@ -29,30 +29,59 @@
<strong>A Side</strong>
</div>
<div class="panel-body">
<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.device.site }}</p>
{% if termination_a.device %}
{# Device component #}
<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.device.site }}</p>
</div>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Rack</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device.rack|default:"None" }}</p>
<div class="form-group">
<label class="col-md-3 control-label required">Rack</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device.rack|default:"None" }}</p>
</div>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device }}</p>
<div class="form-group">
<label class="col-md-3 control-label required">Device</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a.device }}</p>
</div>
</div>
</div>
<div class="form-group">
<label class="col-md-3 control-label required">Name</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a }}</p>
<div class="form-group">
<label class="col-md-3 control-label required">Name</label>
<div class="col-md-9">
<p class="form-control-static">{{ termination_a }}</p>
</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>

View File

@ -1,12 +1,29 @@
<table class="table table-hover panel-body attr-table">
<tr>
<td>Device</td>
<td>
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
</td>
</tr>
<tr>
<td>Component</td>
<td>{{ termination }}</td>
</tr>
{% if termination.device %}
{# Device component #}
<tr>
<td>Device</td>
<td>
<a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
</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>

View File

@ -50,17 +50,17 @@
<td colspan="2" class="text-muted">Virtual interface</td>
{% elif iface.is_wireless %}
<td colspan="2" class="text-muted">Wireless interface</td>
{% elif iface.connected_endpoint %}
{% with connected_iface=iface.connected_endpoint %}
<td>
<a href="{% url 'dcim:device' pk=connected_iface.device.pk %}">{{ connected_iface.device }}</a>
</td>
<td>
<a href="{% url 'dcim:interface' pk=connected_iface.pk %}"><span title="{{ connected_iface.get_form_factor_display }}">{{ connected_iface }}</span></a>
</td>
{% endwith %}
{% elif iface.circuit_termination %}
{% with iface.circuit_termination.get_peer_termination as peer_termination %}
{% elif iface.connected_endpoint.name %}
{# Connected to an Interface #}
<td>
<a href="{% url 'dcim:device' pk=iface.connected_endpoint.device.pk %}">{{ iface.connected_endpoint.device }}</a>
</td>
<td>
<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>
{% elif iface.connected_endpoint.term_side %}
{# Connected to a CircuitTermination #}
{% with iface.connected_endpoint.get_peer_termination as peer_termination %}
<td colspan="2">
<i class="fa fa-fw fa-globe" title="Circuit"></i>
{% if peer_termination %}
@ -72,7 +72,7 @@
{% endif %}
via
{% 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>
{% endwith %}
{% else %}
@ -84,7 +84,7 @@
{# Buttons #}
<td class="text-right text-nowrap">
{% 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">
<i class="glyphicon glyphicon-signal" aria-hidden="true"></i>
</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">
<i class="glyphicon glyphicon-resize-full" aria-hidden="true"></i>
</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 %}
<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>