mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Extended Cables to connect CircuitTerminations
This commit is contained in:
parent
cbfb25f003
commit
4df74780b8
@ -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']
|
||||
|
@ -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
|
||||
|
@ -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'),
|
||||
})
|
||||
)
|
||||
|
80
netbox/circuits/migrations/0013_cables.py
Normal file
80
netbox/circuits/migrations/0013_cables.py
Normal 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',
|
||||
),
|
||||
]
|
@ -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)'
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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}),
|
||||
|
||||
]
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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 = {
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user