Merge pull request #5212 from netbox-community/4900-cable-paths

#4900: New model for cable paths
This commit is contained in:
Jeremy Stretch 2020-10-08 15:15:40 -04:00 committed by GitHub
commit 6470613221
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 2593 additions and 2174 deletions

View File

@ -34,6 +34,12 @@ http://netbox/api/dcim/sites/ \
--data '[{"id": 10, "description": "Foo"}, {"id": 11, "description": "Bar"}]' --data '[{"id": 10, "description": "Foo"}, {"id": 11, "description": "Bar"}]'
``` ```
#### Improved Cable Trace Performance ([#4900](https://github.com/netbox-community/netbox/issues/4900))
All end-to-end cable paths are now cached using the new CablePath model. This allows NetBox to now immediately return the complete path originating from any endpoint directly from the database, rather than having to trace each cable recursively. It also resolves some systemic validation issues with the original implementation.
**Note:** As part of this change, cable traces will no longer traverse circuits: A circuit termination will be considered the origin or destination of an end-to-end path.
### Enhancements ### Enhancements
* [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines * [#1503](https://github.com/netbox-community/netbox/issues/1503) - Allow assigment of secrets to virtual machines
@ -54,11 +60,44 @@ http://netbox/api/dcim/sites/ \
### REST API Changes ### REST API Changes
* Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints * Added support for `PUT`, `PATCH`, and `DELETE` operations on list endpoints (bulk update and delete)
* circuits.CircuitTermination:
* Added the `/trace/` endpoint
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* dcim.Cable: Added `custom_fields` * dcim.Cable: Added `custom_fields`
* dcim.ConsolePort:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer
* dcim.ConsoleServerPort:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer
* dcim.FrontPort:
* Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
* Added `cable_peer` and `cable_peer_type`
* dcim.Interface:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer
* dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning * dcim.InventoryItem: The `_depth` field has been added to reflect MPTT positioning
* dcim.PowerFeed:
* Added the `/trace/` endpoint
* Added fields `connected_endpoint`, `connected_endpoint_type`, `connected_endpoint_reachable`, `cable_peer`, and `cable_peer_type`
* dcim.PowerOutlet:
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer
* dcim.PowerPanel: Added `custom_fields` * dcim.PowerPanel: Added `custom_fields`
* dcim.PowerPort
* Replaced `connection_status` with `connected_endpoint_reachable` (boolean)
* Added `cable_peer` and `cable_peer_type`
* Removed `connection_status` from nested serializer
* dcim.RackReservation: Added `custom_fields` * dcim.RackReservation: Added `custom_fields`
* dcim.RearPort:
* Replaced the `/trace/` endpoint with `/paths/`, which returns a list of cable paths
* Added `cable_peer` and `cable_peer_type`
* dcim.VirtualChassis: Added `custom_fields` * dcim.VirtualChassis: Added `custom_fields`
* extras.ExportTemplate: The `template_language` field has been removed * extras.ExportTemplate: The `template_language` field has been removed
* extras.Graph: This API endpoint has been removed (see #4349) * extras.Graph: This API endpoint has been removed (see #4349)

View File

@ -3,7 +3,7 @@ from rest_framework import serializers
from circuits.choices import CircuitStatusChoices from circuits.choices import CircuitStatusChoices
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer from dcim.api.nested_serializers import NestedCableSerializer, NestedInterfaceSerializer, NestedSiteSerializer
from dcim.api.serializers import ConnectedEndpointSerializer from dcim.api.serializers import CableTerminationSerializer, ConnectedEndpointSerializer
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer from extras.api.serializers import TaggedObjectSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer from tenancy.api.nested_serializers import NestedTenantSerializer
@ -67,7 +67,7 @@ class CircuitSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
] ]
class CircuitTerminationSerializer(ConnectedEndpointSerializer): class CircuitTerminationSerializer(CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
circuit = NestedCircuitSerializer() circuit = NestedCircuitSerializer()
site = NestedSiteSerializer() site = NestedSiteSerializer()
@ -77,5 +77,6 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer):
model = CircuitTermination model = CircuitTermination
fields = [ fields = [
'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', 'id', 'url', 'circuit', 'term_side', 'site', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable'
] ]

View File

@ -3,6 +3,7 @@ from rest_framework.routers import APIRootView
from circuits import filters from circuits import filters
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
from dcim.api.views import PathEndpointMixin
from extras.api.views import CustomFieldModelViewSet from extras.api.views import CustomFieldModelViewSet
from utilities.api import ModelViewSet from utilities.api import ModelViewSet
from . import serializers from . import serializers
@ -46,9 +47,7 @@ class CircuitTypeViewSet(ModelViewSet):
class CircuitViewSet(CustomFieldModelViewSet): class CircuitViewSet(CustomFieldModelViewSet):
queryset = Circuit.objects.prefetch_related( queryset = Circuit.objects.prefetch_related(
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related( Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')),
'site', 'connected_endpoint__device'
)),
'type', 'tenant', 'provider', 'type', 'tenant', 'provider',
).prefetch_related('tags') ).prefetch_related('tags')
serializer_class = serializers.CircuitSerializer serializer_class = serializers.CircuitSerializer
@ -59,9 +58,9 @@ class CircuitViewSet(CustomFieldModelViewSet):
# Circuit Terminations # Circuit Terminations
# #
class CircuitTerminationViewSet(ModelViewSet): class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
queryset = CircuitTermination.objects.prefetch_related( queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', 'connected_endpoint__device', 'cable' 'circuit', 'site', '_path__destination', 'cable'
) )
serializer_class = serializers.CircuitTerminationSerializer serializer_class = serializers.CircuitTerminationSerializer
filterset_class = filters.CircuitTerminationFilterSet filterset_class = filters.CircuitTerminationFilterSet

View File

@ -1,6 +1,7 @@
import django_filters import django_filters
from django.db.models import Q from django.db.models import Q
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
from dcim.models import Region, Site from dcim.models import Region, Site
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
@ -144,7 +145,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
).distinct() ).distinct()
class CircuitTerminationFilterSet(BaseFilterSet): class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

View File

@ -0,0 +1,49 @@
import sys
from django.db import migrations, models
import django.db.models.deletion
def cache_cable_peers(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Cable = apps.get_model('dcim', 'Cable')
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
if 'test' not in sys.argv:
print(f"\n Updating circuit termination cable peers...", flush=True)
ct = ContentType.objects.get_for_model(CircuitTermination)
for cable in Cable.objects.filter(termination_a_type=ct):
CircuitTermination.objects.filter(pk=cable.termination_a_id).update(
_cable_peer_type_id=cable.termination_b_type_id,
_cable_peer_id=cable.termination_b_id
)
for cable in Cable.objects.filter(termination_b_type=ct):
CircuitTermination.objects.filter(pk=cable.termination_b_id).update(
_cable_peer_type_id=cable.termination_a_type_id,
_cable_peer_id=cable.termination_a_id
)
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('circuits', '0020_custom_field_data'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='circuittermination',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.RunPython(
code=cache_cable_peers,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,26 @@
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0121_cablepath'),
('circuits', '0021_cache_cable_peer'),
]
operations = [
migrations.AddField(
model_name='circuittermination',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.RemoveField(
model_name='circuittermination',
name='connected_endpoint',
),
migrations.RemoveField(
model_name='circuittermination',
name='connection_status',
),
]

View File

@ -2,9 +2,8 @@ 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 CONNECTION_STATUS_CHOICES
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.models import CableTermination from dcim.models import CableTermination, PathEndpoint
from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem from extras.models import ChangeLoggedModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
@ -232,7 +231,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
return self._get_termination('Z') return self._get_termination('Z')
class CircuitTermination(CableTermination): class CircuitTermination(PathEndpoint, CableTermination):
circuit = models.ForeignKey( circuit = models.ForeignKey(
to='circuits.Circuit', to='circuits.Circuit',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -248,18 +247,6 @@ class CircuitTermination(CableTermination):
on_delete=models.PROTECT, on_delete=models.PROTECT,
related_name='circuit_terminations' related_name='circuit_terminations'
) )
connected_endpoint = models.OneToOneField(
to='dcim.Interface',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
port_speed = models.PositiveIntegerField( port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)' verbose_name='Port speed (Kbps)'
) )

View File

@ -3,7 +3,7 @@ from django.test import TestCase
from circuits.choices import * from circuits.choices import *
from circuits.filters import * from circuits.filters import *
from circuits.models import Circuit, CircuitTermination, CircuitType, Provider from circuits.models import Circuit, CircuitTermination, CircuitType, Provider
from dcim.models import Region, Site from dcim.models import Cable, Region, Site
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
@ -286,6 +286,8 @@ class CircuitTerminationTestCase(TestCase):
)) ))
CircuitTermination.objects.bulk_create(circuit_terminations) CircuitTermination.objects.bulk_create(circuit_terminations)
Cable(termination_a=circuit_terminations[0], termination_b=circuit_terminations[1]).save()
def test_term_side(self): def test_term_side(self):
params = {'term_side': 'A'} params = {'term_side': 'A'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 3)
@ -313,3 +315,13 @@ class CircuitTerminationTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'site': [sites[0].slug, sites[1].slug]} params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
def test_cabled(self):
params = {'cabled': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)

View File

@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from dcim.views import CableCreateView, CableTraceView from dcim.views import CableCreateView, PathTraceView
from extras.views import ObjectChangeLogView from extras.views import ObjectChangeLogView
from . import views from . import views
from .models import Circuit, CircuitTermination, CircuitType, Provider from .models import Circuit, CircuitTermination, CircuitType, Provider
@ -45,6 +45,6 @@ urlpatterns = [
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'), path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'), path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}), path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
path('circuit-terminations/<int:pk>/trace/', CableTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}), path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
] ]

View File

@ -131,7 +131,7 @@ class CircuitView(ObjectView):
circuit = get_object_or_404(self.queryset, pk=pk) circuit = get_object_or_404(self.queryset, pk=pk)
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region'
).filter( ).filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
).first() ).first()
@ -139,7 +139,7 @@ class CircuitView(ObjectView):
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view') termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related( termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region', 'connected_endpoint__device' 'site__region'
).filter( ).filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first() ).first()

View File

@ -1,8 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from dcim.constants import CONNECTION_STATUS_CHOICES
from dcim import models from dcim import models
from utilities.api import ChoiceField, WritableNestedSerializer from utilities.api import WritableNestedSerializer
__all__ = [ __all__ = [
'NestedCableSerializer', 'NestedCableSerializer',
@ -228,51 +227,46 @@ class NestedDeviceSerializer(WritableNestedSerializer):
class NestedConsoleServerPortSerializer(WritableNestedSerializer): class NestedConsoleServerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta: class Meta:
model = models.ConsoleServerPort model = models.ConsoleServerPort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] fields = ['id', 'url', 'device', 'name', 'cable']
class NestedConsolePortSerializer(WritableNestedSerializer): class NestedConsolePortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta: class Meta:
model = models.ConsolePort model = models.ConsolePort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] fields = ['id', 'url', 'device', 'name', 'cable']
class NestedPowerOutletSerializer(WritableNestedSerializer): class NestedPowerOutletSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta: class Meta:
model = models.PowerOutlet model = models.PowerOutlet
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] fields = ['id', 'url', 'device', 'name', 'cable']
class NestedPowerPortSerializer(WritableNestedSerializer): class NestedPowerPortSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta: class Meta:
model = models.PowerPort model = models.PowerPort
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] fields = ['id', 'url', 'device', 'name', 'cable']
class NestedInterfaceSerializer(WritableNestedSerializer): class NestedInterfaceSerializer(WritableNestedSerializer):
device = NestedDeviceSerializer(read_only=True) device = NestedDeviceSerializer(read_only=True)
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
class Meta: class Meta:
model = models.Interface model = models.Interface
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status'] fields = ['id', 'url', 'device', 'name', 'cable']
class NestedRearPortSerializer(WritableNestedSerializer): class NestedRearPortSerializer(WritableNestedSerializer):

View File

@ -7,12 +7,13 @@ from rest_framework.validators import UniqueTogetherValidator
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceType, DeviceRole, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis, VirtualChassis,
) )
from dcim.utils import decompile_path_node
from extras.api.customfields import CustomFieldModelSerializer from extras.api.customfields import CustomFieldModelSerializer
from extras.api.serializers import TaggedObjectSerializer from extras.api.serializers import TaggedObjectSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
@ -27,17 +28,35 @@ from virtualization.api.nested_serializers import NestedClusterSerializer
from .nested_serializers import * from .nested_serializers import *
class CableTerminationSerializer(serializers.ModelSerializer):
cable_peer_type = serializers.SerializerMethodField(read_only=True)
cable_peer = serializers.SerializerMethodField(read_only=True)
def get_cable_peer_type(self, obj):
if obj._cable_peer is not None:
return f'{obj._cable_peer._meta.app_label}.{obj._cable_peer._meta.model_name}'
return None
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_cable_peer(self, obj):
"""
Return the appropriate serializer for the cable termination model.
"""
if obj._cable_peer is not None:
serializer = get_serializer_for_model(obj._cable_peer, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj._cable_peer, context=context).data
return None
class ConnectedEndpointSerializer(ValidatedModelSerializer): class ConnectedEndpointSerializer(ValidatedModelSerializer):
connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
connected_endpoint = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True)
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
def get_connected_endpoint_type(self, obj): def get_connected_endpoint_type(self, obj):
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None: if obj._path is not None and obj._path.destination is not None:
return '{}.{}'.format( return f'{obj._path.destination._meta.app_label}.{obj._path.destination._meta.model_name}'
obj.connected_endpoint._meta.app_label,
obj.connected_endpoint._meta.model_name
)
return None return None
@swagger_serializer_method(serializer_or_field=serializers.DictField) @swagger_serializer_method(serializer_or_field=serializers.DictField)
@ -45,14 +64,17 @@ class ConnectedEndpointSerializer(ValidatedModelSerializer):
""" """
Return the appropriate serializer for the type of connected object. Return the appropriate serializer for the type of connected object.
""" """
if getattr(obj, 'connected_endpoint', None) is None: if obj._path is not None and obj._path.destination is not None:
return None serializer = get_serializer_for_model(obj._path.destination, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj._path.destination, context=context).data
return None
serializer = get_serializer_for_model(obj.connected_endpoint, prefix='Nested') @swagger_serializer_method(serializer_or_field=serializers.BooleanField)
context = {'request': self.context['request']} def get_connected_endpoint_reachable(self, obj):
data = serializer(obj.connected_endpoint, context=context).data if obj._path is not None:
return obj._path.is_active
return data return None
# #
@ -452,7 +474,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
method = serializers.DictField() method = serializers.DictField()
class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -465,12 +487,12 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSeria
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
] ]
class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -483,12 +505,12 @@ class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type', 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
'connected_endpoint', 'connection_status', 'cable', 'tags', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
] ]
class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -511,12 +533,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags',
] ]
class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField( type = ChoiceField(
@ -529,12 +552,13 @@ class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable',
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type',
'connected_endpoint_reachable', 'tags',
] ]
class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer): class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=InterfaceTypeChoices) type = ChoiceField(choices=InterfaceTypeChoices)
@ -554,8 +578,9 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
model = Interface model = Interface
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type',
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
'count_ipaddresses',
] ]
# TODO: This validation should be handled by Interface.clean() # TODO: This validation should be handled by Interface.clean()
@ -579,7 +604,7 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
return super().validate(data) return super().validate(data)
class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@ -587,7 +612,10 @@ class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
class Meta: class Meta:
model = RearPort model = RearPort
fields = ['id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags'] fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer',
'cable_peer_type', 'tags',
]
class FrontPortRearPortSerializer(WritableNestedSerializer): class FrontPortRearPortSerializer(WritableNestedSerializer):
@ -601,7 +629,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'name', 'label'] fields = ['id', 'url', 'name', 'label']
class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer() device = NestedDeviceSerializer()
type = ChoiceField(choices=PortTypeChoices) type = ChoiceField(choices=PortTypeChoices)
@ -612,7 +640,7 @@ class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
model = FrontPort model = FrontPort
fields = [ fields = [
'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable',
'tags', 'cable_peer', 'cable_peer_type', 'tags',
] ]
@ -708,6 +736,50 @@ class TracedCableSerializer(serializers.ModelSerializer):
] ]
class CablePathSerializer(serializers.ModelSerializer):
origin_type = ContentTypeField(read_only=True)
origin = serializers.SerializerMethodField(read_only=True)
destination_type = ContentTypeField(read_only=True)
destination = serializers.SerializerMethodField(read_only=True)
path = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CablePath
fields = [
'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active',
]
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_origin(self, obj):
"""
Return the appropriate serializer for the origin.
"""
serializer = get_serializer_for_model(obj.origin, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.origin, context=context).data
@swagger_serializer_method(serializer_or_field=serializers.DictField)
def get_destination(self, obj):
"""
Return the appropriate serializer for the destination, if any.
"""
if obj.destination_id is not None:
serializer = get_serializer_for_model(obj.destination, prefix='Nested')
context = {'request': self.context['request']}
return serializer(obj.destination, context=context).data
return None
@swagger_serializer_method(serializer_or_field=serializers.ListField)
def get_path(self, obj):
ret = []
for node in obj.path:
ct_id, object_id = decompile_path_node(node)
ct = ContentType.objects.get_for_id(ct_id)
# TODO: Return the object URL
ret.append(f'{ct.app_label}.{ct.model}:{object_id}')
return ret
# #
# Interface connections # Interface connections
# #
@ -715,17 +787,23 @@ class TracedCableSerializer(serializers.ModelSerializer):
class InterfaceConnectionSerializer(ValidatedModelSerializer): class InterfaceConnectionSerializer(ValidatedModelSerializer):
interface_a = serializers.SerializerMethodField() interface_a = serializers.SerializerMethodField()
interface_b = NestedInterfaceSerializer(source='connected_endpoint') interface_b = NestedInterfaceSerializer(source='connected_endpoint')
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, required=False) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
class Meta: class Meta:
model = Interface model = Interface
fields = ['interface_a', 'interface_b', 'connection_status'] fields = ['interface_a', 'interface_b', 'connected_endpoint_reachable']
@swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer) @swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer)
def get_interface_a(self, obj): def get_interface_a(self, obj):
context = {'request': self.context['request']} context = {'request': self.context['request']}
return NestedInterfaceSerializer(instance=obj, context=context).data return NestedInterfaceSerializer(instance=obj, context=context).data
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
def get_connected_endpoint_reachable(self, obj):
if obj._path is not None:
return obj._path.is_active
return None
# #
# Virtual chassis # Virtual chassis
@ -760,7 +838,12 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count']
class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): class PowerFeedSerializer(
TaggedObjectSerializer,
CableTerminationSerializer,
ConnectedEndpointSerializer,
CustomFieldModelSerializer
):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer() power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer( rack = NestedRackSerializer(
@ -790,5 +873,7 @@ class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
model = PowerFeed model = PowerFeed
fields = [ fields = [
'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'id', 'url', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'cable', 'max_utilization', 'comments', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint',
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
'last_updated',
] ]

View File

@ -17,7 +17,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
from circuits.models import Circuit from circuits.models import Circuit
from dcim import filters from dcim import filters
from dcim.models import ( from dcim.models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
@ -45,7 +45,7 @@ class DCIMRootView(APIRootView):
# Mixins # Mixins
class CableTraceMixin(object): class PathEndpointMixin(object):
@action(detail=True, url_path='trace') @action(detail=True, url_path='trace')
def trace(self, request, pk): def trace(self, request, pk):
@ -57,7 +57,10 @@ class CableTraceMixin(object):
# Initialize the path array # Initialize the path array
path = [] path = []
for near_end, cable, far_end in obj.trace()[0]: for near_end, cable, far_end in obj.trace():
if near_end is None:
# Split paths
break
# Serialize each object # Serialize each object
serializer_a = get_serializer_for_model(near_end, prefix='Nested') serializer_a = get_serializer_for_model(near_end, prefix='Nested')
@ -77,6 +80,20 @@ class CableTraceMixin(object):
return Response(path) return Response(path)
class PassThroughPortMixin(object):
@action(detail=True, url_path='paths')
def paths(self, request, pk):
"""
Return all CablePaths which traverse a given pass-through port.
"""
obj = get_object_or_404(self.queryset, pk=pk)
cablepaths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin', 'destination')
serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
return Response(serializer.data)
# #
# Regions # Regions
# #
@ -469,49 +486,47 @@ class DeviceViewSet(CustomFieldModelViewSet):
# Device components # Device components
# #
class ConsolePortViewSet(CableTraceMixin, ModelViewSet): class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsolePortFilterSet filterset_class = filters.ConsolePortFilterSet
class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet): class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = ConsoleServerPort.objects.prefetch_related(
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
)
serializer_class = serializers.ConsoleServerPortSerializer serializer_class = serializers.ConsoleServerPortSerializer
filterset_class = filters.ConsoleServerPortFilterSet filterset_class = filters.ConsoleServerPortFilterSet
class PowerPortViewSet(CableTraceMixin, ModelViewSet): class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerPort.objects.prefetch_related( queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
)
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerPortFilterSet filterset_class = filters.PowerPortFilterSet
class PowerOutletViewSet(CableTraceMixin, ModelViewSet): class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags') queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
serializer_class = serializers.PowerOutletSerializer serializer_class = serializers.PowerOutletSerializer
filterset_class = filters.PowerOutletFilterSet filterset_class = filters.PowerOutletFilterSet
class InterfaceViewSet(CableTraceMixin, ModelViewSet): class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags' 'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
).filter(
device__isnull=False
) )
serializer_class = serializers.InterfaceSerializer serializer_class = serializers.InterfaceSerializer
filterset_class = filters.InterfaceFilterSet filterset_class = filters.InterfaceFilterSet
class FrontPortViewSet(CableTraceMixin, ModelViewSet): class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags') queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
serializer_class = serializers.FrontPortSerializer serializer_class = serializers.FrontPortSerializer
filterset_class = filters.FrontPortFilterSet filterset_class = filters.FrontPortFilterSet
class RearPortViewSet(CableTraceMixin, ModelViewSet): class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags') queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
serializer_class = serializers.RearPortSerializer serializer_class = serializers.RearPortSerializer
filterset_class = filters.RearPortFilterSet filterset_class = filters.RearPortFilterSet
@ -534,32 +549,26 @@ class InventoryItemViewSet(ModelViewSet):
# #
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet): class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = ConsolePort.objects.prefetch_related( queryset = ConsolePort.objects.prefetch_related('device', '_path').filter(
'device', 'connected_endpoint__device' _path__destination_id__isnull=False
).filter(
connected_endpoint__isnull=False
) )
serializer_class = serializers.ConsolePortSerializer serializer_class = serializers.ConsolePortSerializer
filterset_class = filters.ConsoleConnectionFilterSet filterset_class = filters.ConsoleConnectionFilterSet
class PowerConnectionViewSet(ListModelMixin, GenericViewSet): class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = PowerPort.objects.prefetch_related( queryset = PowerPort.objects.prefetch_related('device', '_path').filter(
'device', 'connected_endpoint__device' _path__destination_id__isnull=False
).filter(
_connected_poweroutlet__isnull=False
) )
serializer_class = serializers.PowerPortSerializer serializer_class = serializers.PowerPortSerializer
filterset_class = filters.PowerConnectionFilterSet filterset_class = filters.PowerConnectionFilterSet
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet): class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related('device', '_path').filter(
'device', '_connected_interface__device'
).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair # Avoid duplicate connections by only selecting the lower PK in a connected pair
_connected_interface__isnull=False, _path__destination_id__isnull=False,
pk__lt=F('_connected_interface') pk__lt=F('_path__destination_id')
) )
serializer_class = serializers.InterfaceConnectionSerializer serializer_class = serializers.InterfaceConnectionSerializer
filterset_class = filters.InterfaceConnectionFilterSet filterset_class = filters.InterfaceConnectionFilterSet
@ -608,8 +617,10 @@ class PowerPanelViewSet(ModelViewSet):
# Power feeds # Power feeds
# #
class PowerFeedViewSet(CustomFieldModelViewSet): class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags') queryset = PowerFeed.objects.prefetch_related(
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
)
serializer_class = serializers.PowerFeedSerializer serializer_class = serializers.PowerFeedSerializer
filterset_class = filters.PowerFeedFilterSet filterset_class = filters.PowerFeedFilterSet
@ -664,7 +675,7 @@ class ConnectedDeviceViewSet(ViewSet):
device__name=peer_device_name, device__name=peer_device_name,
name=peer_interface_name name=peer_interface_name
) )
local_interface = peer_interface._connected_interface local_interface = peer_interface.connected_endpoint
if local_interface is None: if local_interface is None:
return Response() return Response()

View File

@ -59,12 +59,6 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
# Cabling and connections # Cabling and connections
# #
# Console/power/interface connection statuses
CONNECTION_STATUS_CHOICES = [
[False, 'Not Connected'],
[True, 'Connected'],
]
# Cable endpoint types # Cable endpoint types
CABLE_TERMINATION_MODELS = Q( CABLE_TERMINATION_MODELS = Q(
Q(app_label='circuits', model__in=( Q(app_label='circuits', model__in=(

View File

@ -1,14 +0,0 @@
class LoopDetected(Exception):
"""
A loop has been detected while tracing a cable path.
"""
pass
class CableTraceSplit(Exception):
"""
A cable trace cannot be completed because a RearPort maps to multiple FrontPorts and
we don't know which one to follow.
"""
def __init__(self, termination, *args, **kwargs):
self.termination = termination

View File

@ -1,9 +1,11 @@
from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator, MaxValueValidator from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models from django.db import models
from netaddr import AddrFormatError, EUI, mac_unix_expanded from netaddr import AddrFormatError, EUI, mac_unix_expanded
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from .lookups import PathContains
class ASNField(models.BigIntegerField): class ASNField(models.BigIntegerField):
@ -50,3 +52,15 @@ class MACAddressField(models.Field):
if not value: if not value:
return None return None
return str(self.to_python(value)) return str(self.to_python(value))
class PathField(ArrayField):
"""
An ArrayField which holds a set of objects, each identified by a (type, ID) tuple.
"""
def __init__(self, **kwargs):
kwargs['base_field'] = models.CharField(max_length=40)
super().__init__(**kwargs)
PathField.register_lookup(PathContains)

View File

@ -1,5 +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
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
from tenancy.filters import TenancyFilterSet from tenancy.filters import TenancyFilterSet
@ -23,6 +24,7 @@ from .models import (
__all__ = ( __all__ = (
'CableFilterSet', 'CableFilterSet',
'CableTerminationFilterSet',
'ConsoleConnectionFilterSet', 'ConsoleConnectionFilterSet',
'ConsolePortFilterSet', 'ConsolePortFilterSet',
'ConsolePortTemplateFilterSet', 'ConsolePortTemplateFilterSet',
@ -40,6 +42,7 @@ __all__ = (
'InterfaceTemplateFilterSet', 'InterfaceTemplateFilterSet',
'InventoryItemFilterSet', 'InventoryItemFilterSet',
'ManufacturerFilterSet', 'ManufacturerFilterSet',
'PathEndpointFilterSet',
'PlatformFilterSet', 'PlatformFilterSet',
'PowerConnectionFilterSet', 'PowerConnectionFilterSet',
'PowerFeedFilterSet', 'PowerFeedFilterSet',
@ -752,71 +755,76 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
) )
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class CableTerminationFilterSet(django_filters.FilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
cabled = django_filters.BooleanFilter( cabled = django_filters.BooleanFilter(
field_name='cable', field_name='cable',
lookup_expr='isnull', lookup_expr='isnull',
exclude=True exclude=True
) )
class PathEndpointFilterSet(django_filters.FilterSet):
connected = django_filters.BooleanFilter(
method='filter_connected'
)
def filter_connected(self, queryset, name, value):
if value:
return queryset.filter(_path__is_active=True)
else:
return queryset.filter(Q(_path__isnull=True) | Q(_path__is_active=False))
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices,
null_value=None
)
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['id', 'name', 'description', 'connection_status'] fields = ['id', 'name', 'description']
class ConsoleServerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class ConsoleServerPortFilterSet(
BaseFilterSet,
DeviceComponentFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet
):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=ConsolePortTypeChoices, choices=ConsolePortTypeChoices,
null_value=None null_value=None
) )
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta: class Meta:
model = ConsoleServerPort model = ConsoleServerPort
fields = ['id', 'name', 'description', 'connection_status'] fields = ['id', 'name', 'description']
class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class PowerPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerPortTypeChoices, choices=PowerPortTypeChoices,
null_value=None null_value=None
) )
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description', 'connection_status'] fields = ['id', 'name', 'maximum_draw', 'allocated_draw', 'description']
class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet): class PowerOutletFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
type = django_filters.MultipleChoiceFilter( type = django_filters.MultipleChoiceFilter(
choices=PowerOutletTypeChoices, choices=PowerOutletTypeChoices,
null_value=None null_value=None
) )
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta: class Meta:
model = PowerOutlet model = PowerOutlet
fields = ['id', 'name', 'feed_leg', 'description', 'connection_status'] fields = ['id', 'name', 'feed_leg', 'description']
class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet): class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',
@ -833,11 +841,6 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
field_name='pk', field_name='pk',
label='Device (ID)', label='Device (ID)',
) )
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
kind = django_filters.CharFilter( kind = django_filters.CharFilter(
method='filter_kind', method='filter_kind',
label='Kind of interface', label='Kind of interface',
@ -864,7 +867,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = ['id', 'name', 'connection_status', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description'] fields = ['id', 'name', 'type', 'enabled', 'mtu', 'mgmt_only', 'mode', 'description']
def filter_device(self, queryset, name, value): def filter_device(self, queryset, name, value):
try: try:
@ -914,24 +917,14 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
}.get(value, queryset.none()) }.get(value, queryset.none())
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta: class Meta:
model = FrontPort model = FrontPort
fields = ['id', 'name', 'type', 'description'] fields = ['id', 'name', 'type', 'description']
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet): class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
cabled = django_filters.BooleanFilter(
field_name='cable',
lookup_expr='isnull',
exclude=True
)
class Meta: class Meta:
model = RearPort model = RearPort
@ -1139,7 +1132,20 @@ class CableFilterSet(BaseFilterSet):
return queryset return queryset
class ConsoleConnectionFilterSet(BaseFilterSet): class ConnectionFilterSet:
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(device_id__in=value)
class ConsoleConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1154,23 +1160,10 @@ class ConsoleConnectionFilterSet(BaseFilterSet):
class Meta: class Meta:
model = ConsolePort model = ConsolePort
fields = ['name', 'connection_status'] fields = ['name']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(connected_endpoint__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(
Q(**{'{}__in'.format(name): value}) |
Q(**{'connected_endpoint__{}__in'.format(name): value})
)
class PowerConnectionFilterSet(BaseFilterSet): class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1185,23 +1178,10 @@ class PowerConnectionFilterSet(BaseFilterSet):
class Meta: class Meta:
model = PowerPort model = PowerPort
fields = ['name', 'connection_status'] fields = ['name']
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(_connected_poweroutlet__device__site__slug=value)
def filter_device(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_poweroutlet__{}__in'.format(name): value})
)
class InterfaceConnectionFilterSet(BaseFilterSet): class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
site = django_filters.CharFilter( site = django_filters.CharFilter(
method='filter_site', method='filter_site',
label='Site (slug)', label='Site (slug)',
@ -1216,23 +1196,7 @@ class InterfaceConnectionFilterSet(BaseFilterSet):
class Meta: class Meta:
model = Interface model = Interface
fields = ['connection_status'] fields = []
def filter_site(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(device__site__slug=value) |
Q(_connected_interface__device__site__slug=value)
)
def filter_device(self, queryset, name, value):
if not value:
return queryset
return queryset.filter(
Q(**{'{}__in'.format(name): value}) |
Q(**{'_connected_interface__{}__in'.format(name): value})
)
class PowerPanelFilterSet(BaseFilterSet): class PowerPanelFilterSet(BaseFilterSet):
@ -1284,7 +1248,13 @@ class PowerPanelFilterSet(BaseFilterSet):
return queryset.filter(qs_filter) return queryset.filter(qs_filter)
class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet): class PowerFeedFilterSet(
BaseFilterSet,
CableTerminationFilterSet,
PathEndpointFilterSet,
CustomFieldFilterSet,
CreatedUpdatedFilterSet
):
q = django_filters.CharFilter( q = django_filters.CharFilter(
method='search', method='search',
label='Search', label='Search',

10
netbox/dcim/lookups.py Normal file
View File

@ -0,0 +1,10 @@
from django.contrib.postgres.fields.array import ArrayContains
from dcim.utils import object_to_path_node
class PathContains(ArrayContains):
def get_prep_lookup(self):
self.rhs = [object_to_path_node(self.rhs)]
return super().get_prep_lookup()

View File

View File

@ -0,0 +1,81 @@
from django.core.management.base import BaseCommand
from django.core.management.color import no_style
from django.db import connection
from circuits.models import CircuitTermination
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
from dcim.signals import create_cablepath
ENDPOINT_MODELS = (
CircuitTermination,
ConsolePort,
ConsoleServerPort,
Interface,
PowerFeed,
PowerOutlet,
PowerPort
)
class Command(BaseCommand):
help = "Generate any missing cable paths among all cable termination objects in NetBox"
def add_arguments(self, parser):
parser.add_argument(
"--force", action='store_true', dest='force',
help="Force recalculation of all existing cable paths"
)
parser.add_argument(
"--no-input", action='store_true', dest='no_input',
help="Do not prompt user for any input/confirmation"
)
def handle(self, *model_names, **options):
# If --force was passed, first delete all existing CablePaths
if options['force']:
cable_paths = CablePath.objects.all()
paths_count = cable_paths.count()
# Prompt the user to confirm recalculation of all paths
if paths_count and not options['no_input']:
self.stdout.write(self.style.ERROR("WARNING: Forcing recalculation of all cable paths."))
self.stdout.write(
f"This will delete and recalculate all {paths_count} existing cable paths. Are you sure?"
)
confirmation = input("Type yes to confirm: ")
if confirmation != 'yes':
self.stdout.write(self.style.SUCCESS("Aborting"))
return
# Delete all existing CablePath instances
self.stdout.write(f"Deleting {paths_count} existing cable paths...")
deleted_count, _ = CablePath.objects.all().delete()
self.stdout.write((self.style.SUCCESS(f' Deleted {deleted_count} paths')))
# Reinitialize the model's PK sequence
self.stdout.write(f'Resetting database sequence for CablePath model')
sequence_sql = connection.ops.sequence_reset_sql(no_style(), [CablePath])
with connection.cursor() as cursor:
for sql in sequence_sql:
cursor.execute(sql)
# Retrace paths
for model in ENDPOINT_MODELS:
origins = model.objects.filter(cable__isnull=False)
if not options['force']:
origins = origins.filter(_path__isnull=True)
origins_count = origins.count()
if not origins_count:
print(f'Found no missing {model._meta.verbose_name} paths; skipping')
continue
print(f'Retracing {origins_count} cabled {model._meta.verbose_name_plural}...')
i = 0
for i, obj in enumerate(origins, start=1):
create_cablepath(obj)
# TODO: Come up with a better progress indicator
if not i % 1000:
self.stdout.write(f' {i}')
self.stdout.write(self.style.SUCCESS(f' Retraced {i} {model._meta.verbose_name_plural}'))
self.stdout.write(self.style.SUCCESS('Finished.'))

View File

@ -0,0 +1,141 @@
import sys
from django.db import migrations, models
import django.db.models.deletion
def cache_cable_peers(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Cable = apps.get_model('dcim', 'Cable')
ConsolePort = apps.get_model('dcim', 'ConsolePort')
ConsoleServerPort = apps.get_model('dcim', 'ConsoleServerPort')
PowerPort = apps.get_model('dcim', 'PowerPort')
PowerOutlet = apps.get_model('dcim', 'PowerOutlet')
Interface = apps.get_model('dcim', 'Interface')
FrontPort = apps.get_model('dcim', 'FrontPort')
RearPort = apps.get_model('dcim', 'RearPort')
PowerFeed = apps.get_model('dcim', 'PowerFeed')
models = (
ConsolePort,
ConsoleServerPort,
PowerPort,
PowerOutlet,
Interface,
FrontPort,
RearPort,
PowerFeed
)
if 'test' not in sys.argv:
print("\n", end="")
for model in models:
if 'test' not in sys.argv:
print(f" Updating {model._meta.verbose_name} cable peers...", flush=True)
ct = ContentType.objects.get_for_model(model)
for cable in Cable.objects.filter(termination_a_type=ct):
model.objects.filter(pk=cable.termination_a_id).update(
_cable_peer_type_id=cable.termination_b_type_id,
_cable_peer_id=cable.termination_b_id
)
for cable in Cable.objects.filter(termination_b_type=ct):
model.objects.filter(pk=cable.termination_b_id).update(
_cable_peer_type_id=cable.termination_a_type_id,
_cable_peer_id=cable.termination_a_id
)
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0119_inventoryitem_mptt_rebuild'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='consoleport',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='consoleserverport',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='consoleserverport',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='frontport',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='frontport',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='interface',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='interface',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='powerfeed',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='powerfeed',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='poweroutlet',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='poweroutlet',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='powerport',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='powerport',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.AddField(
model_name='rearport',
name='_cable_peer_id',
field=models.PositiveIntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='rearport',
name='_cable_peer_type',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='contenttypes.contenttype'),
),
migrations.RunPython(
code=cache_cable_peers,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,107 @@
import dcim.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('dcim', '0120_cache_cable_peer'),
]
operations = [
migrations.CreateModel(
name='CablePath',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False)),
('origin_id', models.PositiveIntegerField()),
('destination_id', models.PositiveIntegerField(blank=True, null=True)),
('path', dcim.fields.PathField(base_field=models.CharField(max_length=40), size=None)),
('is_active', models.BooleanField(default=False)),
('destination_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
('origin_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='contenttypes.contenttype')),
],
options={
'unique_together': {('origin_type', 'origin_id')},
},
),
migrations.AddField(
model_name='consoleport',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.AddField(
model_name='consoleserverport',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.AddField(
model_name='interface',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.AddField(
model_name='powerfeed',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.AddField(
model_name='poweroutlet',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.AddField(
model_name='powerport',
name='_path',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='dcim.cablepath'),
),
migrations.RemoveField(
model_name='consoleport',
name='connected_endpoint',
),
migrations.RemoveField(
model_name='consoleport',
name='connection_status',
),
migrations.RemoveField(
model_name='consoleserverport',
name='connection_status',
),
migrations.RemoveField(
model_name='interface',
name='_connected_circuittermination',
),
migrations.RemoveField(
model_name='interface',
name='_connected_interface',
),
migrations.RemoveField(
model_name='interface',
name='connection_status',
),
migrations.RemoveField(
model_name='powerfeed',
name='connected_endpoint',
),
migrations.RemoveField(
model_name='powerfeed',
name='connection_status',
),
migrations.RemoveField(
model_name='poweroutlet',
name='connection_status',
),
migrations.RemoveField(
model_name='powerport',
name='_connected_powerfeed',
),
migrations.RemoveField(
model_name='powerport',
name='_connected_poweroutlet',
),
migrations.RemoveField(
model_name='powerport',
name='connection_status',
),
]

View File

@ -8,6 +8,7 @@ from .sites import *
__all__ = ( __all__ = (
'BaseInterface', 'BaseInterface',
'Cable', 'Cable',
'CablePath',
'CableTermination', 'CableTermination',
'ConsolePort', 'ConsolePort',
'ConsolePortTemplate', 'ConsolePortTemplate',

View File

@ -1,6 +1,5 @@
import logging from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
@ -11,8 +10,8 @@ from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.exceptions import CableTraceSplit
from dcim.fields import MACAddressField from dcim.fields import MACAddressField
from dcim.utils import path_node_to_object
from extras.models import ObjectChange, TaggedItem from extras.models import ObjectChange, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from utilities.fields import NaturalOrderingField from utilities.fields import NaturalOrderingField
@ -32,6 +31,7 @@ __all__ = (
'FrontPort', 'FrontPort',
'Interface', 'Interface',
'InventoryItem', 'InventoryItem',
'PathEndpoint',
'PowerOutlet', 'PowerOutlet',
'PowerPort', 'PowerPort',
'RearPort', 'RearPort',
@ -39,6 +39,9 @@ __all__ = (
class ComponentModel(models.Model): class ComponentModel(models.Model):
"""
An abstract model inherited by any model which has a parent Device.
"""
device = models.ForeignKey( device = models.ForeignKey(
to='dcim.Device', to='dcim.Device',
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -93,6 +96,14 @@ class ComponentModel(models.Model):
class CableTermination(models.Model): class CableTermination(models.Model):
"""
An abstract model inherited by all models to which a Cable can terminate (certain device components, PowerFeed, and
CircuitTermination instances). The `cable` field indicates the Cable instance which is terminated to this instance.
`_cable_peer` is a GenericForeignKey used to cache the far-end CableTermination on the local instance; this is a
shortcut to referencing `cable.termination_b`, for example. `_cable_peer` is set or cleared by the receivers in
dcim.signals when a Cable instance is created or deleted, respectively.
"""
cable = models.ForeignKey( cable = models.ForeignKey(
to='dcim.Cable', to='dcim.Cable',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -100,6 +111,21 @@ class CableTermination(models.Model):
blank=True, blank=True,
null=True null=True
) )
_cable_peer_type = models.ForeignKey(
to=ContentType,
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
_cable_peer_id = models.PositiveIntegerField(
blank=True,
null=True
)
_cable_peer = GenericForeignKey(
ct_field='_cable_peer_type',
fk_field='_cable_peer_id'
)
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted. # Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
_cabled_as_a = GenericRelation( _cabled_as_a = GenericRelation(
@ -116,138 +142,57 @@ class CableTermination(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def trace(self):
"""
Return three items: the traceable portion of a cable path, the termination points where it splits (if any), and
the remaining positions on the position stack (if any). Splits occur when the trace is initiated from a midpoint
along a path which traverses a RearPort. In cases where the originating endpoint is unknown, it is not possible
to know which corresponding FrontPort to follow. Remaining positions occur when tracing a path that traverses
a FrontPort without traversing a RearPort again.
The path is a list representing a complete cable path, with each individual segment represented as a
three-tuple:
[
(termination A, cable, termination B),
(termination C, cable, termination D),
(termination E, cable, termination F)
]
"""
endpoint = self
path = []
position_stack = []
def get_peer_port(termination):
from circuits.models import CircuitTermination
# Map a front port to its corresponding rear port
if isinstance(termination, FrontPort):
# Retrieve the corresponding RearPort from database to ensure we have an up-to-date instance
peer_port = RearPort.objects.get(pk=termination.rear_port.pk)
# Don't use the stack for RearPorts with a single position. Only remember the position at
# many-to-one points so we can select the correct FrontPort when we reach the corresponding
# one-to-many point.
if peer_port.positions > 1:
position_stack.append(termination)
return peer_port
# Map a rear port/position to its corresponding front port
elif isinstance(termination, RearPort):
if termination.positions > 1:
# Can't map to a FrontPort without a position if there are multiple options
if not position_stack:
raise CableTraceSplit(termination)
front_port = position_stack.pop()
position = front_port.rear_port_position
# Validate the position
if position not in range(1, termination.positions + 1):
raise Exception("Invalid position for {} ({} positions): {})".format(
termination, termination.positions, position
))
else:
# Don't use the stack for RearPorts with a single position. The only possible position is 1.
position = 1
try:
peer_port = FrontPort.objects.get(
rear_port=termination,
rear_port_position=position,
)
return peer_port
except ObjectDoesNotExist:
return None
# Follow a circuit to its other termination
elif isinstance(termination, CircuitTermination):
peer_termination = termination.get_peer_termination()
if peer_termination is None:
return None
return peer_termination
# Termination is not a pass-through port
else:
return None
logger = logging.getLogger('netbox.dcim.cable.trace')
logger.debug("Tracing cable from {} {}".format(self.parent, self))
while endpoint is not None:
# No cable connected; nothing to trace
if not endpoint.cable:
path.append((endpoint, None, None))
logger.debug("No cable connected")
return path, None, position_stack
# Check for loops
if endpoint.cable in [segment[1] for segment in path]:
logger.debug("Loop detected!")
return path, None, position_stack
# Record the current segment in the path
far_end = endpoint.get_cable_peer()
path.append((endpoint, endpoint.cable, far_end))
logger.debug("{}[{}] --- Cable {} ---> {}[{}]".format(
endpoint.parent, endpoint, endpoint.cable.pk, far_end.parent, far_end
))
# Get the peer port of the far end termination
try:
endpoint = get_peer_port(far_end)
except CableTraceSplit as e:
return path, e.termination.frontports.all(), position_stack
if endpoint is None:
return path, None, position_stack
def get_cable_peer(self): def get_cable_peer(self):
if self.cable is None: return self._cable_peer
return None
if self._cabled_as_a.exists():
return self.cable.termination_b
if self._cabled_as_b.exists():
return self.cable.termination_a
def get_path_endpoints(self):
class PathEndpoint(models.Model):
"""
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination.
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
CablePath model. `_path` should not be accessed directly; rather, use the `path` property.
`connected_endpoint()` is a convenience method for returning the destination of the associated CablePath, if any.
"""
_path = models.ForeignKey(
to='dcim.CablePath',
on_delete=models.SET_NULL,
null=True,
blank=True
)
class Meta:
abstract = True
def trace(self):
if self._path is None:
return []
# Construct the complete path
path = [self, *[path_node_to_object(obj) for obj in self._path.path]]
while (len(path) + 1) % 3:
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
path.append(None)
path.append(self._path.destination)
# Return the path as a list of three-tuples (A termination, cable, B termination)
return list(zip(*[iter(path)] * 3))
@property
def path(self):
return self._path
@property
def connected_endpoint(self):
""" """
Return all endpoints of paths which traverse this object. Caching accessor for the attached CablePath's destination (if any)
""" """
endpoints = [] if not hasattr(self, '_connected_endpoint'):
self._connected_endpoint = self._path.destination if self._path else None
# Get the far end of the last path segment return self._connected_endpoint
path, split_ends, position_stack = self.trace()
endpoint = path[-1][2]
if split_ends is not None:
for termination in split_ends:
endpoints.extend(termination.get_path_endpoints())
elif endpoint is not None:
endpoints.append(endpoint)
return endpoints
# #
@ -255,7 +200,7 @@ class CableTermination(models.Model):
# #
@extras_features('export_templates', 'webhooks') @extras_features('export_templates', 'webhooks')
class ConsolePort(CableTermination, ComponentModel): class ConsolePort(CableTermination, PathEndpoint, ComponentModel):
""" """
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts. A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
""" """
@ -265,18 +210,6 @@ class ConsolePort(CableTermination, ComponentModel):
blank=True, blank=True,
help_text='Physical port type' help_text='Physical port type'
) )
connected_endpoint = models.OneToOneField(
to='dcim.ConsoleServerPort',
on_delete=models.SET_NULL,
related_name='connected_endpoint',
blank=True,
null=True
)
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'description']
@ -303,7 +236,7 @@ class ConsolePort(CableTermination, ComponentModel):
# #
@extras_features('webhooks') @extras_features('webhooks')
class ConsoleServerPort(CableTermination, ComponentModel): class ConsoleServerPort(CableTermination, PathEndpoint, ComponentModel):
""" """
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts. A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
""" """
@ -313,11 +246,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
blank=True, blank=True,
help_text='Physical port type' help_text='Physical port type'
) )
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'description']
@ -344,7 +272,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
# #
@extras_features('export_templates', 'webhooks') @extras_features('export_templates', 'webhooks')
class PowerPort(CableTermination, ComponentModel): class PowerPort(CableTermination, PathEndpoint, ComponentModel):
""" """
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets. A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
""" """
@ -366,25 +294,6 @@ class PowerPort(CableTermination, ComponentModel):
validators=[MinValueValidator(1)], validators=[MinValueValidator(1)],
help_text="Allocated power draw (watts)" help_text="Allocated power draw (watts)"
) )
_connected_poweroutlet = models.OneToOneField(
to='dcim.PowerOutlet',
on_delete=models.SET_NULL,
related_name='connected_endpoint',
blank=True,
null=True
)
_connected_powerfeed = models.OneToOneField(
to='dcim.PowerFeed',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
@ -407,51 +316,18 @@ class PowerPort(CableTermination, ComponentModel):
self.description, self.description,
) )
@property
def connected_endpoint(self):
"""
Return the connected PowerOutlet, if it exists, or the connected PowerFeed, if it exists. We have to check for
ObjectDoesNotExist in case the referenced object has been deleted from the database.
"""
try:
if self._connected_poweroutlet:
return self._connected_poweroutlet
except ObjectDoesNotExist:
pass
try:
if self._connected_powerfeed:
return self._connected_powerfeed
except ObjectDoesNotExist:
pass
return None
@connected_endpoint.setter
def connected_endpoint(self, value):
# TODO: Fix circular import
from . import PowerFeed
if value is None:
self._connected_poweroutlet = None
self._connected_powerfeed = None
elif isinstance(value, PowerOutlet):
self._connected_poweroutlet = value
self._connected_powerfeed = None
elif isinstance(value, PowerFeed):
self._connected_poweroutlet = None
self._connected_powerfeed = value
else:
raise ValueError(
"Connected endpoint must be a PowerOutlet or PowerFeed, not {}.".format(type(value))
)
def get_power_draw(self): def get_power_draw(self):
""" """
Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort. Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
""" """
# Calculate aggregate draw of all child power outlets if no numbers have been defined manually # Calculate aggregate draw of all child power outlets if no numbers have been defined manually
if self.allocated_draw is None and self.maximum_draw is None: if self.allocated_draw is None and self.maximum_draw is None:
poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet)
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True) outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( utilization = PowerPort.objects.filter(
_cable_peer_type=poweroutlet_ct,
_cable_peer_id__in=outlet_ids
).aggregate(
maximum_draw_total=Sum('maximum_draw'), maximum_draw_total=Sum('maximum_draw'),
allocated_draw_total=Sum('allocated_draw'), allocated_draw_total=Sum('allocated_draw'),
) )
@ -463,10 +339,13 @@ class PowerPort(CableTermination, ComponentModel):
} }
# Calculate per-leg aggregates for three-phase feeds # Calculate per-leg aggregates for three-phase feeds
if self._connected_powerfeed and self._connected_powerfeed.phase == PowerFeedPhaseChoices.PHASE_3PHASE: if getattr(self._cable_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE:
for leg, leg_name in PowerOutletFeedLegChoices: for leg, leg_name in PowerOutletFeedLegChoices:
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True) outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
utilization = PowerPort.objects.filter(_connected_poweroutlet_id__in=outlet_ids).aggregate( utilization = PowerPort.objects.filter(
_cable_peer_type=poweroutlet_ct,
_cable_peer_id__in=outlet_ids
).aggregate(
maximum_draw_total=Sum('maximum_draw'), maximum_draw_total=Sum('maximum_draw'),
allocated_draw_total=Sum('allocated_draw'), allocated_draw_total=Sum('allocated_draw'),
) )
@ -493,7 +372,7 @@ class PowerPort(CableTermination, ComponentModel):
# #
@extras_features('webhooks') @extras_features('webhooks')
class PowerOutlet(CableTermination, ComponentModel): class PowerOutlet(CableTermination, PathEndpoint, ComponentModel):
""" """
A physical power outlet (output) within a Device which provides power to a PowerPort. A physical power outlet (output) within a Device which provides power to a PowerPort.
""" """
@ -516,11 +395,6 @@ class PowerOutlet(CableTermination, ComponentModel):
blank=True, blank=True,
help_text="Phase (for three-phase feeds)" help_text="Phase (for three-phase feeds)"
) )
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
tags = TaggableManager(through=TaggedItem) tags = TaggableManager(through=TaggedItem)
csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description'] csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
@ -585,7 +459,7 @@ class BaseInterface(models.Model):
@extras_features('export_templates', 'webhooks') @extras_features('export_templates', 'webhooks')
class Interface(CableTermination, ComponentModel, BaseInterface): class Interface(CableTermination, PathEndpoint, ComponentModel, BaseInterface):
""" """
A network interface within a Device. A physical Interface can connect to exactly one other Interface. A network interface within a Device. A physical Interface can connect to exactly one other Interface.
""" """
@ -596,25 +470,6 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
max_length=100, max_length=100,
blank=True blank=True
) )
_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.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
lag = models.ForeignKey( lag = models.ForeignKey(
to='self', to='self',
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@ -730,42 +585,6 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
@property
def connected_endpoint(self):
"""
Return the connected Interface, if it exists, or the connected CircuitTermination, if it exists. We have to
check for ObjectDoesNotExist in case the referenced object has been deleted from the database.
"""
try:
if self._connected_interface:
return self._connected_interface
except ObjectDoesNotExist:
pass
try:
if self._connected_circuittermination:
return self._connected_circuittermination
except ObjectDoesNotExist:
pass
return None
@connected_endpoint.setter
def connected_endpoint(self, value):
from circuits.models import CircuitTermination
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 return self.device

View File

@ -7,13 +7,15 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.db.models import F, ProtectedError from django.db.models import F, ProtectedError, Sum
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.fields import PathField
from dcim.utils import decompile_path_node, path_node_to_object
from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem from extras.models import ChangeLoggedModel, ConfigContextModel, CustomFieldModel, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from utilities.choices import ColorChoices from utilities.choices import ColorChoices
@ -25,6 +27,7 @@ from .device_components import *
__all__ = ( __all__ = (
'Cable', 'Cable',
'CablePath',
'Device', 'Device',
'DeviceRole', 'DeviceRole',
'DeviceType', 'DeviceType',
@ -976,6 +979,9 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
# A copy of the PK to be used by __str__ in case the object is deleted # A copy of the PK to be used by __str__ in case the object is deleted
self._pk = self.pk self._pk = self.pk
# Cache the original status so we can check later if it's been changed
self._orig_status = self.status
@classmethod @classmethod
def from_db(cls, db, field_names, values): def from_db(cls, db, field_names, values):
""" """
@ -1154,6 +1160,85 @@ class Cable(ChangeLoggedModel, CustomFieldModel):
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name] return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
class CablePath(models.Model):
"""
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
not terminate on a PathEndpoint).
`path` contains a list of nodes within the path, each represented by a tuple of (type, ID). The first element in the
path must be a Cable instance, followed by a pair of pass-through ports. For example, consider the following
topology:
1 2 3
Interface A --- Front Port A | Rear Port A --- Rear Port B | Front Port B --- Interface B
This path would be expressed as:
CablePath(
origin = Interface A
destination = Interface B
path = [Cable 1, Front Port A, Rear Port A, Cable 2, Rear Port B, Front Port B, Cable 3]
)
`is_active` is set to True only if 1) `destination` is not null, and 2) every Cable within the path has a status of
"connected".
"""
origin_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
related_name='+'
)
origin_id = models.PositiveIntegerField()
origin = GenericForeignKey(
ct_field='origin_type',
fk_field='origin_id'
)
destination_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
related_name='+',
blank=True,
null=True
)
destination_id = models.PositiveIntegerField(
blank=True,
null=True
)
destination = GenericForeignKey(
ct_field='destination_type',
fk_field='destination_id'
)
path = PathField()
is_active = models.BooleanField(
default=False
)
class Meta:
unique_together = ('origin_type', 'origin_id')
def __str__(self):
path = ', '.join([str(path_node_to_object(node)) for node in self.path])
return f"Path #{self.pk}: {self.origin} to {self.destination} via ({path})"
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
# Record a direct reference to this CablePath on its originating object
model = self.origin._meta.model
model.objects.filter(pk=self.origin.pk).update(_path=self.pk)
def get_total_length(self):
"""
Return the sum of the length of each cable in the path.
"""
cable_ids = [
# Starting from the first element, every third element in the path should be a Cable
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
]
return Cable.objects.filter(id__in=cable_ids).aggregate(total=Sum('_abs_length'))['total']
# #
# Virtual chassis # Virtual chassis
# #

View File

@ -10,7 +10,7 @@ from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
from extras.utils import extras_features from extras.utils import extras_features
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.validators import ExclusionValidator from utilities.validators import ExclusionValidator
from .device_components import CableTermination from .device_components import CableTermination, PathEndpoint
__all__ = ( __all__ = (
'PowerFeed', 'PowerFeed',
@ -73,7 +73,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel): class PowerFeed(ChangeLoggedModel, PathEndpoint, CableTermination, CustomFieldModel):
""" """
An electrical circuit delivered from a PowerPanel. An electrical circuit delivered from a PowerPanel.
""" """
@ -88,18 +88,6 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
blank=True, blank=True,
null=True null=True
) )
connected_endpoint = models.OneToOneField(
to='dcim.PowerPort',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True
)
connection_status = models.BooleanField(
choices=CONNECTION_STATUS_CHOICES,
blank=True,
null=True
)
name = models.CharField( name = models.CharField(
max_length=50 max_length=50
) )

View File

@ -3,6 +3,7 @@ from collections import OrderedDict
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
@ -22,6 +23,7 @@ from utilities.fields import ColorField, NaturalOrderingField
from utilities.querysets import RestrictedQuerySet from utilities.querysets import RestrictedQuerySet
from utilities.mptt import TreeManager from utilities.mptt import TreeManager
from utilities.utils import array_to_string, serialize_object from utilities.utils import array_to_string, serialize_object
from .device_components import PowerOutlet, PowerPort
from .devices import Device from .devices import Device
from .power import PowerFeed from .power import PowerFeed
@ -536,20 +538,22 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
""" """
Determine the utilization rate of power in the rack and return it as a percentage. Determine the utilization rate of power in the rack and return it as a percentage.
""" """
power_stats = PowerFeed.objects.filter( powerfeeds = PowerFeed.objects.filter(rack=self)
rack=self available_power_total = sum(pf.available_power for pf in powerfeeds)
).annotate( if not available_power_total:
allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'), return 0
).values(
'allocated_draw_total',
'available_power'
)
if power_stats: pf_powerports = PowerPort.objects.filter(
allocated_draw_total = sum(x['allocated_draw_total'] or 0 for x in power_stats) _cable_peer_type=ContentType.objects.get_for_model(PowerFeed),
available_power_total = sum(x['available_power'] for x in power_stats) _cable_peer_id__in=powerfeeds.values_list('id', flat=True)
return int(allocated_draw_total / available_power_total * 100) or 0 )
return 0 poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports)
allocated_draw_total = PowerPort.objects.filter(
_cable_peer_type=ContentType.objects.get_for_model(PowerOutlet),
_cable_peer_id__in=poweroutlets.values_list('id', flat=True)
).aggregate(Sum('allocated_draw'))['allocated_draw__sum'] or 0
return int(allocated_draw_total / available_power_total * 100)
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')

View File

@ -1,10 +1,35 @@
import logging import logging
from django.db.models.signals import post_save, pre_delete from django.contrib.contenttypes.models import ContentType
from django.db.models.signals import post_save, post_delete, pre_delete
from django.db import transaction
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableStatusChoices from .choices import CableStatusChoices
from .models import Cable, CableTermination, Device, FrontPort, RearPort, VirtualChassis from .models import Cable, CablePath, Device, PathEndpoint, VirtualChassis
from .utils import trace_path
def create_cablepath(node):
"""
Create CablePaths for all paths originating from the specified node.
"""
path, destination, is_active = trace_path(node)
if path:
cp = CablePath(origin=node, path=path, destination=destination, is_active=is_active)
cp.save()
def rebuild_paths(obj):
"""
Rebuild all CablePaths which traverse the specified node
"""
cable_paths = CablePath.objects.filter(path__contains=obj)
with transaction.atomic():
for cp in cable_paths:
cp.delete()
create_cablepath(cp.origin)
@receiver(post_save, sender=VirtualChassis) @receiver(post_save, sender=VirtualChassis)
@ -32,7 +57,7 @@ def clear_virtualchassis_members(instance, **kwargs):
@receiver(post_save, sender=Cable) @receiver(post_save, sender=Cable)
def update_connected_endpoints(instance, **kwargs): def update_connected_endpoints(instance, created, **kwargs):
""" """
When a Cable is saved, check for and update its two connected endpoints When a Cable is saved, check for and update its two connected endpoints
""" """
@ -40,63 +65,61 @@ def update_connected_endpoints(instance, **kwargs):
# Cache the Cable on its two termination points # Cache the Cable on its two termination points
if instance.termination_a.cable != instance: if instance.termination_a.cable != instance:
logger.debug("Updating termination A for cable {}".format(instance)) logger.debug(f"Updating termination A for cable {instance}")
instance.termination_a.cable = instance instance.termination_a.cable = instance
instance.termination_a._cable_peer = instance.termination_b
instance.termination_a.save() instance.termination_a.save()
if instance.termination_b.cable != instance: if instance.termination_b.cable != instance:
logger.debug("Updating termination B for cable {}".format(instance)) logger.debug(f"Updating termination B for cable {instance}")
instance.termination_b.cable = instance instance.termination_b.cable = instance
instance.termination_b._cable_peer = instance.termination_a
instance.termination_b.save() instance.termination_b.save()
# Update any endpoints for this Cable. # Create/update cable paths
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints() if created:
for endpoint in endpoints: for termination in (instance.termination_a, instance.termination_b):
path, split_ends, position_stack = endpoint.trace() if isinstance(termination, PathEndpoint):
# Determine overall path status (connected or planned) create_cablepath(termination)
path_status = True else:
for segment in path: rebuild_paths(termination)
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED: elif instance.status != instance._orig_status:
path_status = False # We currently don't support modifying either termination of an existing Cable. (This
break # may change in the future.) However, we do need to capture status changes and update
# any CablePaths accordingly.
endpoint_a = path[0][0] if instance.status != CableStatusChoices.STATUS_CONNECTED:
endpoint_b = path[-1][2] if not split_ends and not position_stack else None CablePath.objects.filter(path__contains=instance).update(is_active=False)
else:
# Patch panel ports are not connected endpoints, all other cable terminations are rebuild_paths(instance)
if isinstance(endpoint_a, CableTermination) and not isinstance(endpoint_a, (FrontPort, RearPort)) and \
isinstance(endpoint_b, CableTermination) and not isinstance(endpoint_b, (FrontPort, RearPort)):
logger.debug("Updating path endpoints: {} <---> {}".format(endpoint_a, endpoint_b))
endpoint_a.connected_endpoint = endpoint_b
endpoint_a.connection_status = path_status
endpoint_a.save()
endpoint_b.connected_endpoint = endpoint_a
endpoint_b.connection_status = path_status
endpoint_b.save()
@receiver(pre_delete, sender=Cable) @receiver(post_delete, sender=Cable)
def nullify_connected_endpoints(instance, **kwargs): def nullify_connected_endpoints(instance, **kwargs):
""" """
When a Cable is deleted, check for and update its two connected endpoints When a Cable is deleted, check for and update its two connected endpoints
""" """
logger = logging.getLogger('netbox.dcim.cable') logger = logging.getLogger('netbox.dcim.cable')
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
# Disassociate the Cable from its termination points # Disassociate the Cable from its termination points
if instance.termination_a is not None: if instance.termination_a is not None:
logger.debug("Nullifying termination A for cable {}".format(instance)) logger.debug(f"Nullifying termination A for cable {instance}")
instance.termination_a.cable = None instance.termination_a.cable = None
instance.termination_a._cable_peer = None
instance.termination_a.save() instance.termination_a.save()
if instance.termination_b is not None: if instance.termination_b is not None:
logger.debug("Nullifying termination B for cable {}".format(instance)) logger.debug(f"Nullifying termination B for cable {instance}")
instance.termination_b.cable = None instance.termination_b.cable = None
instance.termination_b._cable_peer = None
instance.termination_b.save() instance.termination_b.save()
# If this Cable was part of any complete end-to-end paths, tear them down. # Delete and retrace any dependent cable paths
for endpoint in endpoints: for cablepath in CablePath.objects.filter(path__contains=instance):
logger.debug(f"Removing path information for {endpoint}") path, destination, is_active = trace_path(cablepath.origin)
if hasattr(endpoint, 'connected_endpoint'): if path:
endpoint.connected_endpoint = None CablePath.objects.filter(pk=cablepath.pk).update(
endpoint.connection_status = None path=path,
endpoint.save() destination_type=ContentType.objects.get_for_model(destination) if destination else None,
destination_id=destination.pk if destination else None,
is_active=is_active
)
else:
cablepath.delete()

View File

@ -67,8 +67,17 @@ INTERFACE_TAGGED_VLANS = """
{% endfor %} {% endfor %}
""" """
CONNECTION_STATUS = """ POWERFEED_CABLE = """
<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span> <a href="{{ value.get_absolute_url }}">{{ value }}</a>
<a href="{% url 'dcim:powerfeed_trace' pk=record.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
"""
POWERFEED_CABLETERMINATION = """
<a href="{{ value.parent.get_absolute_url }}">{{ value.parent }}</a>
<i class="fa fa-caret-right"></i>
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
""" """
@ -812,13 +821,15 @@ class CableTable(BaseTable):
# #
class ConsoleConnectionTable(BaseTable): class ConsoleConnectionTable(BaseTable):
console_server = tables.LinkColumn( console_server = tables.Column(
viewname='dcim:device', accessor=Accessor('_path__destination__device'),
accessor=Accessor('connected_endpoint__device'), orderable=False,
args=[Accessor('connected_endpoint__device__pk')], linkify=True,
verbose_name='Console Server' verbose_name='Console Server'
) )
connected_endpoint = tables.Column( console_server_port = tables.Column(
accessor=Accessor('_path__destination'),
orderable=False,
linkify=True, linkify=True,
verbose_name='Port' verbose_name='Port'
) )
@ -829,26 +840,28 @@ class ConsoleConnectionTable(BaseTable):
linkify=True, linkify=True,
verbose_name='Console Port' verbose_name='Console Port'
) )
connection_status = tables.TemplateColumn( reachable = BooleanColumn(
template_code=CONNECTION_STATUS, accessor=Accessor('_path__is_active'),
verbose_name='Status' verbose_name='Reachable'
) )
add_prefetch = False
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = ConsolePort model = ConsolePort
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status') fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
class PowerConnectionTable(BaseTable): class PowerConnectionTable(BaseTable):
pdu = tables.LinkColumn( pdu = tables.Column(
viewname='dcim:device', accessor=Accessor('_path__destination__device'),
accessor=Accessor('connected_endpoint__device'), orderable=False,
args=[Accessor('connected_endpoint__device__pk')], linkify=True,
order_by='_connected_poweroutlet__device',
verbose_name='PDU' verbose_name='PDU'
) )
outlet = tables.Column( outlet = tables.Column(
accessor=Accessor('_connected_poweroutlet'), accessor=Accessor('_path__destination'),
orderable=False,
linkify=True, linkify=True,
verbose_name='Outlet' verbose_name='Outlet'
) )
@ -859,51 +872,51 @@ class PowerConnectionTable(BaseTable):
linkify=True, linkify=True,
verbose_name='Power Port' verbose_name='Power Port'
) )
connection_status = tables.TemplateColumn( reachable = BooleanColumn(
template_code=CONNECTION_STATUS, accessor=Accessor('_path__is_active'),
verbose_name='Status' verbose_name='Reachable'
) )
add_prefetch = False
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = PowerPort model = PowerPort
fields = ('pdu', 'outlet', 'device', 'name', 'connection_status') fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
class InterfaceConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable):
device_a = tables.LinkColumn( device_a = tables.Column(
viewname='dcim:device',
accessor=Accessor('device'), accessor=Accessor('device'),
args=[Accessor('device__pk')], linkify=True,
verbose_name='Device A' verbose_name='Device A'
) )
interface_a = tables.LinkColumn( interface_a = tables.Column(
viewname='dcim:interface',
accessor=Accessor('name'), accessor=Accessor('name'),
args=[Accessor('pk')], linkify=True,
verbose_name='Interface A' verbose_name='Interface A'
) )
device_b = tables.LinkColumn( device_b = tables.Column(
viewname='dcim:device', accessor=Accessor('_path__destination__device'),
accessor=Accessor('_connected_interface__device'), orderable=False,
args=[Accessor('_connected_interface__device__pk')], linkify=True,
verbose_name='Device B' verbose_name='Device B'
) )
interface_b = tables.LinkColumn( interface_b = tables.Column(
viewname='dcim:interface', accessor=Accessor('_path__destination'),
accessor=Accessor('_connected_interface'), orderable=False,
args=[Accessor('_connected_interface__pk')], linkify=True,
verbose_name='Interface B' verbose_name='Interface B'
) )
connection_status = tables.TemplateColumn( reachable = BooleanColumn(
template_code=CONNECTION_STATUS, accessor=Accessor('_path__is_active'),
verbose_name='Status' verbose_name='Reachable'
) )
add_prefetch = False
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
model = Interface model = Interface
fields = ( fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
)
# #
@ -977,6 +990,15 @@ class PowerFeedTable(BaseTable):
max_utilization = tables.TemplateColumn( max_utilization = tables.TemplateColumn(
template_code="{{ value }}%" template_code="{{ value }}%"
) )
cable = tables.TemplateColumn(
template_code=POWERFEED_CABLE,
orderable=False
)
connection = tables.TemplateColumn(
accessor='get_cable_peer',
template_code=POWERFEED_CABLETERMINATION,
orderable=False
)
available_power = tables.Column( available_power = tables.Column(
verbose_name='Available power (VA)' verbose_name='Available power (VA)'
) )
@ -988,8 +1010,9 @@ class PowerFeedTable(BaseTable):
model = PowerFeed model = PowerFeed
fields = ( fields = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'available_power', 'tags', 'max_utilization', 'cable', 'connection', 'available_power', 'tags',
) )
default_columns = ( default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
'connection',
) )

View File

@ -977,7 +977,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsolePort model = ConsolePort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }
@ -1016,7 +1016,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = ConsoleServerPort model = ConsoleServerPort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }
@ -1055,7 +1055,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerPort model = PowerPort
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }
@ -1094,7 +1094,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = PowerOutlet model = PowerOutlet
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }
@ -1133,7 +1133,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
model = Interface model = Interface
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
'description': 'New description', 'description': 'New description',
} }
@ -1189,7 +1189,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
] ]
class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class FrontPortTest(APIViewTestCases.APIViewTestCase):
model = FrontPort model = FrontPort
brief_fields = ['cable', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
@ -1247,7 +1247,7 @@ class FrontPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
] ]
class RearPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase): class RearPortTest(APIViewTestCases.APIViewTestCase):
model = RearPort model = RearPort
brief_fields = ['cable', 'device', 'id', 'name', 'url'] brief_fields = ['cable', 'device', 'id', 'name', 'url']
bulk_update_data = { bulk_update_data = {
@ -1453,377 +1453,6 @@ class CableTest(APIViewTestCases.APIViewTestCase):
] ]
class ConnectionTest(APITestCase):
def setUp(self):
super().setUp()
self.site = Site.objects.create(
name='Test Site 1', slug='test-site-1'
)
manufacturer = Manufacturer.objects.create(
name='Test Manufacturer 1', slug='test-manufacturer-1'
)
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Test Device Type 1', slug='test-device-type-1'
)
devicerole = DeviceRole.objects.create(
name='Test Device Role 1', slug='test-device-role-1', color='ff0000'
)
self.device1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 1', site=self.site
)
self.device2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Device 2', site=self.site
)
self.panel1 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Panel 1', site=self.site
)
self.panel2 = Device.objects.create(
device_type=devicetype, device_role=devicerole, name='Test Panel 2', site=self.site
)
def test_create_direct_console_connection(self):
consoleport1 = ConsolePort.objects.create(
device=self.device1, name='Test Console Port 1'
)
consoleserverport1 = ConsoleServerPort.objects.create(
device=self.device2, name='Test Console Server Port 1'
)
data = {
'termination_a_type': 'dcim.consoleport',
'termination_a_id': consoleport1.pk,
'termination_b_type': 'dcim.consoleserverport',
'termination_b_id': consoleserverport1.pk,
}
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Cable.objects.count(), 1)
cable = Cable.objects.get(pk=response.data['id'])
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
self.assertEqual(cable.termination_a, consoleport1)
self.assertEqual(cable.termination_b, consoleserverport1)
self.assertEqual(consoleport1.cable, cable)
self.assertEqual(consoleserverport1.cable, cable)
self.assertEqual(consoleport1.connected_endpoint, consoleserverport1)
self.assertEqual(consoleserverport1.connected_endpoint, consoleport1)
def test_create_patched_console_connection(self):
consoleport1 = ConsolePort.objects.create(
device=self.device1, name='Test Console Port 1'
)
consoleserverport1 = ConsoleServerPort.objects.create(
device=self.device2, name='Test Console Server Port 1'
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
cables = [
# Console port to panel1 front
{
'termination_a_type': 'dcim.consoleport',
'termination_a_id': consoleport1.pk,
'termination_b_type': 'dcim.frontport',
'termination_b_id': frontport1.pk,
},
# Panel1 rear to panel2 rear
{
'termination_a_type': 'dcim.rearport',
'termination_a_id': rearport1.pk,
'termination_b_type': 'dcim.rearport',
'termination_b_id': rearport2.pk,
},
# Panel2 front to console server port
{
'termination_a_type': 'dcim.frontport',
'termination_a_id': frontport2.pk,
'termination_b_type': 'dcim.consoleserverport',
'termination_b_id': consoleserverport1.pk,
},
]
for data in cables:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
cable = Cable.objects.get(pk=response.data['id'])
self.assertEqual(cable.termination_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable)
consoleport1 = ConsolePort.objects.get(pk=consoleport1.pk)
consoleserverport1 = ConsoleServerPort.objects.get(pk=consoleserverport1.pk)
self.assertEqual(consoleport1.connected_endpoint, consoleserverport1)
self.assertEqual(consoleserverport1.connected_endpoint, consoleport1)
def test_create_direct_power_connection(self):
powerport1 = PowerPort.objects.create(
device=self.device1, name='Test Power Port 1'
)
poweroutlet1 = PowerOutlet.objects.create(
device=self.device2, name='Test Power Outlet 1'
)
data = {
'termination_a_type': 'dcim.powerport',
'termination_a_id': powerport1.pk,
'termination_b_type': 'dcim.poweroutlet',
'termination_b_id': poweroutlet1.pk,
}
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Cable.objects.count(), 1)
cable = Cable.objects.get(pk=response.data['id'])
powerport1 = PowerPort.objects.get(pk=powerport1.pk)
poweroutlet1 = PowerOutlet.objects.get(pk=poweroutlet1.pk)
self.assertEqual(cable.termination_a, powerport1)
self.assertEqual(cable.termination_b, poweroutlet1)
self.assertEqual(powerport1.cable, cable)
self.assertEqual(poweroutlet1.cable, cable)
self.assertEqual(powerport1.connected_endpoint, poweroutlet1)
self.assertEqual(poweroutlet1.connected_endpoint, powerport1)
# Note: Power connections via patch ports are not supported.
def test_create_direct_interface_connection(self):
interface1 = Interface.objects.create(
device=self.device1, name='Test Interface 1'
)
interface2 = Interface.objects.create(
device=self.device2, name='Test Interface 2'
)
data = {
'termination_a_type': 'dcim.interface',
'termination_a_id': interface1.pk,
'termination_b_type': 'dcim.interface',
'termination_b_id': interface2.pk,
}
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Cable.objects.count(), 1)
cable = Cable.objects.get(pk=response.data['id'])
interface1 = Interface.objects.get(pk=interface1.pk)
interface2 = Interface.objects.get(pk=interface2.pk)
self.assertEqual(cable.termination_a, interface1)
self.assertEqual(cable.termination_b, interface2)
self.assertEqual(interface1.cable, cable)
self.assertEqual(interface2.cable, cable)
self.assertEqual(interface1.connected_endpoint, interface2)
self.assertEqual(interface2.connected_endpoint, interface1)
def test_create_patched_interface_connection(self):
interface1 = Interface.objects.create(
device=self.device1, name='Test Interface 1'
)
interface2 = Interface.objects.create(
device=self.device2, name='Test Interface 2'
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
cables = [
# Interface1 to panel1 front
{
'termination_a_type': 'dcim.interface',
'termination_a_id': interface1.pk,
'termination_b_type': 'dcim.frontport',
'termination_b_id': frontport1.pk,
},
# Panel1 rear to panel2 rear
{
'termination_a_type': 'dcim.rearport',
'termination_a_id': rearport1.pk,
'termination_b_type': 'dcim.rearport',
'termination_b_id': rearport2.pk,
},
# Panel2 front to interface2
{
'termination_a_type': 'dcim.frontport',
'termination_a_id': frontport2.pk,
'termination_b_type': 'dcim.interface',
'termination_b_id': interface2.pk,
},
]
for data in cables:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
cable = Cable.objects.get(pk=response.data['id'])
self.assertEqual(cable.termination_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable)
interface1 = Interface.objects.get(pk=interface1.pk)
interface2 = Interface.objects.get(pk=interface2.pk)
self.assertEqual(interface1.connected_endpoint, interface2)
self.assertEqual(interface2.connected_endpoint, interface1)
def test_create_direct_circuittermination_connection(self):
provider = Provider.objects.create(
name='Test Provider 1', slug='test-provider-1'
)
circuittype = CircuitType.objects.create(
name='Test Circuit Type 1', slug='test-circuit-type-1'
)
circuit = Circuit.objects.create(
provider=provider, type=circuittype, cid='Test Circuit 1'
)
interface1 = Interface.objects.create(
device=self.device1, name='Test Interface 1'
)
circuittermination1 = CircuitTermination.objects.create(
circuit=circuit, term_side='A', site=self.site, port_speed=10000
)
data = {
'termination_a_type': 'dcim.interface',
'termination_a_id': interface1.pk,
'termination_b_type': 'circuits.circuittermination',
'termination_b_id': circuittermination1.pk,
}
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
self.assertEqual(Cable.objects.count(), 1)
cable = Cable.objects.get(pk=response.data['id'])
interface1 = Interface.objects.get(pk=interface1.pk)
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
self.assertEqual(cable.termination_a, interface1)
self.assertEqual(cable.termination_b, circuittermination1)
self.assertEqual(interface1.cable, cable)
self.assertEqual(circuittermination1.cable, cable)
self.assertEqual(interface1.connected_endpoint, circuittermination1)
self.assertEqual(circuittermination1.connected_endpoint, interface1)
def test_create_patched_circuittermination_connection(self):
provider = Provider.objects.create(
name='Test Provider 1', slug='test-provider-1'
)
circuittype = CircuitType.objects.create(
name='Test Circuit Type 1', slug='test-circuit-type-1'
)
circuit = Circuit.objects.create(
provider=provider, type=circuittype, cid='Test Circuit 1'
)
interface1 = Interface.objects.create(
device=self.device1, name='Test Interface 1'
)
circuittermination1 = CircuitTermination.objects.create(
circuit=circuit, term_side='A', site=self.site, port_speed=10000
)
rearport1 = RearPort.objects.create(
device=self.panel1, name='Test Rear Port 1', type=PortTypeChoices.TYPE_8P8C
)
frontport1 = FrontPort.objects.create(
device=self.panel1, name='Test Front Port 1', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport1
)
rearport2 = RearPort.objects.create(
device=self.panel2, name='Test Rear Port 2', type=PortTypeChoices.TYPE_8P8C
)
frontport2 = FrontPort.objects.create(
device=self.panel2, name='Test Front Port 2', type=PortTypeChoices.TYPE_8P8C, rear_port=rearport2
)
self.add_permissions('dcim.add_cable')
url = reverse('dcim-api:cable-list')
cables = [
# Interface to panel1 front
{
'termination_a_type': 'dcim.interface',
'termination_a_id': interface1.pk,
'termination_b_type': 'dcim.frontport',
'termination_b_id': frontport1.pk,
},
# Panel1 rear to panel2 rear
{
'termination_a_type': 'dcim.rearport',
'termination_a_id': rearport1.pk,
'termination_b_type': 'dcim.rearport',
'termination_b_id': rearport2.pk,
},
# Panel2 front to circuit termination
{
'termination_a_type': 'dcim.frontport',
'termination_a_id': frontport2.pk,
'termination_b_type': 'circuits.circuittermination',
'termination_b_id': circuittermination1.pk,
},
]
for data in cables:
response = self.client.post(url, data, format='json', **self.header)
self.assertHttpStatus(response, status.HTTP_201_CREATED)
cable = Cable.objects.get(pk=response.data['id'])
self.assertEqual(cable.termination_a.cable, cable)
self.assertEqual(cable.termination_b.cable, cable)
interface1 = Interface.objects.get(pk=interface1.pk)
circuittermination1 = CircuitTermination.objects.get(pk=circuittermination1.pk)
self.assertEqual(interface1.connected_endpoint, circuittermination1)
self.assertEqual(circuittermination1.connected_endpoint, interface1)
class ConnectedDeviceTest(APITestCase): class ConnectedDeviceTest(APITestCase):
def setUp(self): def setUp(self):

View File

@ -0,0 +1,901 @@
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from circuits.models import *
from dcim.choices import CableStatusChoices
from dcim.models import *
from dcim.utils import object_to_path_node
class CablePathTestCase(TestCase):
"""
Test NetBox's ability to trace and retrace CablePaths in response to data model changes. Tests are numbered
as follows:
1XX: Test direct connections between different endpoint types
2XX: Test different cable topologies
3XX: Test responses to changes in existing objects
"""
@classmethod
def setUpTestData(cls):
# Create a single device that will hold all components
cls.site = Site.objects.create(name='Site', slug='site')
manufacturer = Manufacturer.objects.create(name='Generic', slug='generic')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Test Device')
device_role = DeviceRole.objects.create(name='Device Role', slug='device-role')
cls.device = Device.objects.create(site=cls.site, device_type=device_type, device_role=device_role, name='Test Device')
cls.powerpanel = PowerPanel.objects.create(site=cls.site, name='Power Panel')
provider = Provider.objects.create(name='Provider', slug='provider')
circuit_type = CircuitType.objects.create(name='Circuit Type', slug='circuit-type')
cls.circuit = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 1')
def assertPathExists(self, origin, destination, path=None, is_active=None, msg=None):
"""
Assert that a CablePath from origin to destination with a specific intermediate path exists.
:param origin: Originating endpoint
:param destination: Terminating endpoint, or None
:param path: Sequence of objects comprising the intermediate path (optional)
:param is_active: Boolean indicating whether the end-to-end path is complete and active (optional)
:param msg: Custom failure message (optional)
:return: The matching CablePath (if any)
"""
kwargs = {
'origin_type': ContentType.objects.get_for_model(origin),
'origin_id': origin.pk,
}
if destination is not None:
kwargs['destination_type'] = ContentType.objects.get_for_model(destination)
kwargs['destination_id'] = destination.pk
else:
kwargs['destination_type__isnull'] = True
kwargs['destination_id__isnull'] = True
if path is not None:
kwargs['path'] = [object_to_path_node(obj) for obj in path]
if is_active is not None:
kwargs['is_active'] = is_active
if msg is None:
if destination is not None:
msg = f"Missing path from {origin} to {destination}"
else:
msg = f"Missing partial path originating from {origin}"
cablepath = CablePath.objects.filter(**kwargs).first()
self.assertIsNotNone(cablepath, msg=msg)
return cablepath
def assertPathIsSet(self, origin, cablepath, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param cablepath: The CablePath instance originating from this endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{cablepath.pk} not set on originating endpoint {origin}"
self.assertEqual(origin._path_id, cablepath.pk, msg=msg)
def assertPathIsNotSet(self, origin, msg=None):
"""
Assert that a specific CablePath instance is set as the path on the origin.
:param origin: The originating path endpoint
:param msg: Custom failure message (optional)
"""
if msg is None:
msg = f"Path #{origin._path_id} set as origin on {origin}; should be None!"
self.assertIsNone(origin._path_id, msg=msg)
def test_101_interface_to_interface(self):
"""
[IF1] --C1-- [IF2]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
# Create cable 1
cable1 = Cable(termination_a=interface1, termination_b=interface2)
cable1.save()
path1 = self.assertPathExists(
origin=interface1,
destination=interface2,
path=(cable1,),
is_active=True
)
path2 = self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable1,),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
interface1.refresh_from_db()
interface2.refresh_from_db()
self.assertPathIsSet(interface1, path1)
self.assertPathIsSet(interface2, path2)
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_102_consoleport_to_consoleserverport(self):
"""
[CP1] --C1-- [CSP1]
"""
consoleport1 = ConsolePort.objects.create(device=self.device, name='Console Port 1')
consoleserverport1 = ConsoleServerPort.objects.create(device=self.device, name='Console Server Port 1')
# Create cable 1
cable1 = Cable(termination_a=consoleport1, termination_b=consoleserverport1)
cable1.save()
path1 = self.assertPathExists(
origin=consoleport1,
destination=consoleserverport1,
path=(cable1,),
is_active=True
)
path2 = self.assertPathExists(
origin=consoleserverport1,
destination=consoleport1,
path=(cable1,),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
consoleport1.refresh_from_db()
consoleserverport1.refresh_from_db()
self.assertPathIsSet(consoleport1, path1)
self.assertPathIsSet(consoleserverport1, path2)
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_103_powerport_to_poweroutlet(self):
"""
[PP1] --C1-- [PO1]
"""
powerport1 = PowerPort.objects.create(device=self.device, name='Power Port 1')
poweroutlet1 = PowerOutlet.objects.create(device=self.device, name='Power Outlet 1')
# Create cable 1
cable1 = Cable(termination_a=powerport1, termination_b=poweroutlet1)
cable1.save()
path1 = self.assertPathExists(
origin=powerport1,
destination=poweroutlet1,
path=(cable1,),
is_active=True
)
path2 = self.assertPathExists(
origin=poweroutlet1,
destination=powerport1,
path=(cable1,),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
powerport1.refresh_from_db()
poweroutlet1.refresh_from_db()
self.assertPathIsSet(powerport1, path1)
self.assertPathIsSet(poweroutlet1, path2)
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_104_powerport_to_powerfeed(self):
"""
[PP1] --C1-- [PF1]
"""
powerport1 = PowerPort.objects.create(device=self.device, name='Power Port 1')
powerfeed1 = PowerFeed.objects.create(power_panel=self.powerpanel, name='Power Feed 1')
# Create cable 1
cable1 = Cable(termination_a=powerport1, termination_b=powerfeed1)
cable1.save()
path1 = self.assertPathExists(
origin=powerport1,
destination=powerfeed1,
path=(cable1,),
is_active=True
)
path2 = self.assertPathExists(
origin=powerfeed1,
destination=powerport1,
path=(cable1,),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
powerport1.refresh_from_db()
powerfeed1.refresh_from_db()
self.assertPathIsSet(powerport1, path1)
self.assertPathIsSet(powerfeed1, path2)
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_105_interface_to_circuittermination(self):
"""
[IF1] --C1-- [CT1A]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
circuittermination1 = CircuitTermination.objects.create(
circuit=self.circuit, site=self.site, term_side='A', port_speed=1000
)
# Create cable 1
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
cable1.save()
path1 = self.assertPathExists(
origin=interface1,
destination=circuittermination1,
path=(cable1,),
is_active=True
)
path2 = self.assertPathExists(
origin=circuittermination1,
destination=interface1,
path=(cable1,),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
interface1.refresh_from_db()
circuittermination1.refresh_from_db()
self.assertPathIsSet(interface1, path1)
self.assertPathIsSet(circuittermination1, path2)
# Delete cable 1
cable1.delete()
# Check that all CablePaths have been deleted
self.assertEqual(CablePath.objects.count(), 0)
def test_201_single_path_via_pass_through(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
)
# Create cable 1
cable1 = Cable(termination_a=interface1, termination_b=frontport1)
cable1.save()
self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, frontport1, rearport1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 1)
# Create cable 2
cable2 = Cable(termination_a=rearport1, termination_b=interface2)
cable2.save()
self.assertPathExists(
origin=interface1,
destination=interface2,
path=(cable1, frontport1, rearport1, cable2),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable2, rearport1, frontport1, cable1),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
# Delete cable 2
cable2.delete()
path1 = self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, frontport1, rearport1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 1)
interface1.refresh_from_db()
interface2.refresh_from_db()
self.assertPathIsSet(interface1, path1)
self.assertPathIsNotSet(interface2)
def test_202_multiple_paths_via_pass_through(self):
"""
[IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [IF3]
[IF2] --C2-- [FP1:2] [FP2:2] --C5-- [IF4]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
frontport2_1 = FrontPort.objects.create(
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
)
frontport2_2 = FrontPort.objects.create(
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
)
# Create cables 1-2
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1)
cable1.save()
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2)
cable2.save()
self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, frontport1_1, rearport1),
is_active=False
)
self.assertPathExists(
origin=interface2,
destination=None,
path=(cable2, frontport1_2, rearport1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 2)
# Create cable 3
cable3 = Cable(termination_a=rearport1, termination_b=rearport2)
cable3.save()
self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1),
is_active=False
)
self.assertPathExists(
origin=interface2,
destination=None,
path=(cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 2)
# Create cables 4-5
cable4 = Cable(termination_a=frontport2_1, termination_b=interface3)
cable4.save()
cable5 = Cable(termination_a=frontport2_2, termination_b=interface4)
cable5.save()
path1 = self.assertPathExists(
origin=interface1,
destination=interface3,
path=(
cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1,
cable4,
),
is_active=True
)
path2 = self.assertPathExists(
origin=interface2,
destination=interface4,
path=(
cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2,
cable5,
),
is_active=True
)
path3 = self.assertPathExists(
origin=interface3,
destination=interface1,
path=(
cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1,
cable1
),
is_active=True
)
path4 = self.assertPathExists(
origin=interface4,
destination=interface2,
path=(
cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2,
cable2
),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
# Delete cable 3
cable3.delete()
# Check for four partial paths; one from each interface
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4)
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
interface1.refresh_from_db()
interface2.refresh_from_db()
interface3.refresh_from_db()
interface4.refresh_from_db()
self.assertPathIsSet(interface1, path1)
self.assertPathIsSet(interface2, path2)
self.assertPathIsSet(interface3, path3)
self.assertPathIsSet(interface4, path4)
def test_203_multiple_paths_via_nested_pass_throughs(self):
"""
[IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2] [RP2] --C4-- [RP3] [FP3] --C5-- [RP4] [FP4:1] --C6-- [IF3]
[IF2] --C2-- [FP1:2] [FP4:2] --C7-- [IF4]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=1)
rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
frontport2 = FrontPort.objects.create(
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
)
frontport3 = FrontPort.objects.create(
device=self.device, name='Front Port 3', rear_port=rearport3, rear_port_position=1
)
frontport4_1 = FrontPort.objects.create(
device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1
)
frontport4_2 = FrontPort.objects.create(
device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2
)
# Create cables 1-2, 6-7
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1)
cable1.save()
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2)
cable2.save()
cable6 = Cable(termination_a=interface3, termination_b=frontport4_1)
cable6.save()
cable7 = Cable(termination_a=interface4, termination_b=frontport4_2)
cable7.save()
self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface
# Create cables 3 and 5
cable3 = Cable(termination_a=rearport1, termination_b=frontport2)
cable3.save()
cable5 = Cable(termination_a=rearport4, termination_b=frontport3)
cable5.save()
self.assertEqual(CablePath.objects.count(), 4) # Four (longer) partial paths; one from each interface
# Create cable 4
cable4 = Cable(termination_a=rearport2, termination_b=rearport3)
cable4.save()
self.assertPathExists(
origin=interface1,
destination=interface3,
path=(
cable1, frontport1_1, rearport1, cable3, frontport2, rearport2,
cable4, rearport3, frontport3, cable5, rearport4, frontport4_1,
cable6
),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface4,
path=(
cable2, frontport1_2, rearport1, cable3, frontport2, rearport2,
cable4, rearport3, frontport3, cable5, rearport4, frontport4_2,
cable7
),
is_active=True
)
self.assertPathExists(
origin=interface3,
destination=interface1,
path=(
cable6, frontport4_1, rearport4, cable5, frontport3, rearport3,
cable4, rearport2, frontport2, cable3, rearport1, frontport1_1,
cable1
),
is_active=True
)
self.assertPathExists(
origin=interface4,
destination=interface2,
path=(
cable7, frontport4_2, rearport4, cable5, frontport3, rearport3,
cable4, rearport2, frontport2, cable3, rearport1, frontport1_2,
cable2
),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
# Delete cable 3
cable3.delete()
# Check for four partial paths; one from each interface
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4)
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
def test_204_multiple_paths_via_multiple_pass_throughs(self):
"""
[IF1] --C1-- [FP1:1] [RP1] --C3-- [RP2] [FP2:1] --C4-- [FP3:1] [RP3] --C6-- [RP4] [FP4:1] --C7-- [IF3]
[IF2] --C2-- [FP1:2] [FP2:1] --C5-- [FP3:1] [FP4:2] --C8-- [IF4]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 3', positions=4)
rearport4 = RearPort.objects.create(device=self.device, name='Rear Port 4', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
frontport2_1 = FrontPort.objects.create(
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
)
frontport2_2 = FrontPort.objects.create(
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
)
frontport3_1 = FrontPort.objects.create(
device=self.device, name='Front Port 3:1', rear_port=rearport3, rear_port_position=1
)
frontport3_2 = FrontPort.objects.create(
device=self.device, name='Front Port 3:2', rear_port=rearport3, rear_port_position=2
)
frontport4_1 = FrontPort.objects.create(
device=self.device, name='Front Port 4:1', rear_port=rearport4, rear_port_position=1
)
frontport4_2 = FrontPort.objects.create(
device=self.device, name='Front Port 4:2', rear_port=rearport4, rear_port_position=2
)
# Create cables 1-3, 6-8
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1)
cable1.save()
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2)
cable2.save()
cable3 = Cable(termination_a=rearport1, termination_b=rearport2)
cable3.save()
cable6 = Cable(termination_a=rearport3, termination_b=rearport4)
cable6.save()
cable7 = Cable(termination_a=interface3, termination_b=frontport4_1)
cable7.save()
cable8 = Cable(termination_a=interface4, termination_b=frontport4_2)
cable8.save()
self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface
# Create cables 4 and 5
cable4 = Cable(termination_a=frontport2_1, termination_b=frontport3_1)
cable4.save()
cable5 = Cable(termination_a=frontport2_2, termination_b=frontport3_2)
cable5.save()
self.assertPathExists(
origin=interface1,
destination=interface3,
path=(
cable1, frontport1_1, rearport1, cable3, rearport2, frontport2_1,
cable4, frontport3_1, rearport3, cable6, rearport4, frontport4_1,
cable7
),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface4,
path=(
cable2, frontport1_2, rearport1, cable3, rearport2, frontport2_2,
cable5, frontport3_2, rearport3, cable6, rearport4, frontport4_2,
cable8
),
is_active=True
)
self.assertPathExists(
origin=interface3,
destination=interface1,
path=(
cable7, frontport4_1, rearport4, cable6, rearport3, frontport3_1,
cable4, frontport2_1, rearport2, cable3, rearport1, frontport1_1,
cable1
),
is_active=True
)
self.assertPathExists(
origin=interface4,
destination=interface2,
path=(
cable8, frontport4_2, rearport4, cable6, rearport3, frontport3_2,
cable5, frontport2_2, rearport2, cable3, rearport1, frontport1_2,
cable2
),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
# Delete cable 5
cable5.delete()
# Check for two complete paths (IF1 <--> IF2) and two partial (IF3 <--> IF4)
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 2)
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 2)
def test_205_multiple_paths_via_patched_pass_throughs(self):
"""
[IF1] --C1-- [FP1:1] [RP1] --C3-- [FP2] [RP2] --C4-- [RP3] [FP3:1] --C5-- [IF3]
[IF2] --C2-- [FP1:2] [FP3:2] --C6-- [IF4]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 5', positions=1)
rearport3 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
frontport2 = FrontPort.objects.create(
device=self.device, name='Front Port 5', rear_port=rearport2, rear_port_position=1
)
frontport3_1 = FrontPort.objects.create(
device=self.device, name='Front Port 2:1', rear_port=rearport3, rear_port_position=1
)
frontport3_2 = FrontPort.objects.create(
device=self.device, name='Front Port 2:2', rear_port=rearport3, rear_port_position=2
)
# Create cables 1-2, 5-6
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) # IF1 -> FP1:1
cable1.save()
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) # IF2 -> FP1:2
cable2.save()
cable5 = Cable(termination_a=interface3, termination_b=frontport3_1) # IF3 -> FP3:1
cable5.save()
cable6 = Cable(termination_a=interface4, termination_b=frontport3_2) # IF4 -> FP3:2
cable6.save()
self.assertEqual(CablePath.objects.count(), 4) # Four partial paths; one from each interface
# Create cables 3-4
cable3 = Cable(termination_a=rearport1, termination_b=frontport2) # RP1 -> FP2
cable3.save()
cable4 = Cable(termination_a=rearport2, termination_b=rearport3) # RP2 -> RP3
cable4.save()
self.assertPathExists(
origin=interface1,
destination=interface3,
path=(
cable1, frontport1_1, rearport1, cable3, frontport2, rearport2,
cable4, rearport3, frontport3_1, cable5
),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface4,
path=(
cable2, frontport1_2, rearport1, cable3, frontport2, rearport2,
cable4, rearport3, frontport3_2, cable6
),
is_active=True
)
self.assertPathExists(
origin=interface3,
destination=interface1,
path=(
cable5, frontport3_1, rearport3, cable4, rearport2, frontport2,
cable3, rearport1, frontport1_1, cable1
),
is_active=True
)
self.assertPathExists(
origin=interface4,
destination=interface2,
path=(
cable6, frontport3_2, rearport3, cable4, rearport2, frontport2,
cable3, rearport1, frontport1_2, cable2
),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 4)
# Delete cable 3
cable3.delete()
# Check for four partial paths; one from each interface
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4)
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
def test_206_unidirectional_split_paths(self):
"""
[IF1] --C1-- [RP1] [FP1:1] --C2-- [IF2]
[FP1:2] --C3-- [IF3]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
frontport1_1 = FrontPort.objects.create(
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
)
frontport1_2 = FrontPort.objects.create(
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
)
# Create cables 1
cable1 = Cable(termination_a=interface1, termination_b=rearport1)
cable1.save()
self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, rearport1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 1)
# Create cables 2-3
cable2 = Cable(termination_a=interface2, termination_b=frontport1_1)
cable2.save()
cable3 = Cable(termination_a=interface3, termination_b=frontport1_2)
cable3.save()
self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable2, frontport1_1, rearport1, cable1),
is_active=True
)
self.assertPathExists(
origin=interface3,
destination=interface1,
path=(cable3, frontport1_2, rearport1, cable1),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 3)
# Delete cable 1
cable1.delete()
# Check that the partial path was deleted and the two complete paths are now partial
self.assertPathExists(
origin=interface2,
destination=None,
path=(cable2, frontport1_1, rearport1),
is_active=False
)
self.assertPathExists(
origin=interface3,
destination=None,
path=(cable3, frontport1_2, rearport1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 2)
def test_301_create_path_via_existing_cable(self):
"""
[IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
)
frontport2 = FrontPort.objects.create(
device=self.device, name='Front Port 2', rear_port=rearport2, rear_port_position=1
)
# Create cable 2
cable2 = Cable(termination_a=rearport1, termination_b=rearport2)
cable2.save()
self.assertEqual(CablePath.objects.count(), 0)
# Create cable1
cable1 = Cable(termination_a=interface1, termination_b=frontport1)
cable1.save()
self.assertPathExists(
origin=interface1,
destination=None,
path=(cable1, frontport1, rearport1, cable2, rearport2, frontport2),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 1)
# Create cable 3
cable3 = Cable(termination_a=frontport2, termination_b=interface2)
cable3.save()
self.assertPathExists(
origin=interface1,
destination=interface2,
path=(cable1, frontport1, rearport1, cable2, rearport2, frontport2, cable3),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable3, frontport2, rearport2, cable2, rearport1, frontport1, cable1),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)
def test_302_update_path_on_cable_status_change(self):
"""
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
"""
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=1)
frontport1 = FrontPort.objects.create(
device=self.device, name='Front Port 1', rear_port=rearport1, rear_port_position=1
)
# Create cables 1 and 2
cable1 = Cable(termination_a=interface1, termination_b=frontport1)
cable1.save()
cable2 = Cable(termination_a=rearport1, termination_b=interface2)
cable2.save()
self.assertEqual(CablePath.objects.filter(is_active=True).count(), 2)
self.assertEqual(CablePath.objects.count(), 2)
# Change cable 2's status to "planned"
cable2.status = CableStatusChoices.STATUS_PLANNED
cable2.save()
self.assertPathExists(
origin=interface1,
destination=interface2,
path=(cable1, frontport1, rearport1, cable2),
is_active=False
)
self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable2, rearport1, frontport1, cable1),
is_active=False
)
self.assertEqual(CablePath.objects.count(), 2)
# Change cable 2's status to "connected"
cable2 = Cable.objects.get(pk=cable2.pk)
cable2.status = CableStatusChoices.STATUS_CONNECTED
cable2.save()
self.assertPathExists(
origin=interface1,
destination=interface2,
path=(cable1, frontport1, rearport1, cable2),
is_active=True
)
self.assertPathExists(
origin=interface2,
destination=interface1,
path=(cable2, rearport1, frontport1, cable1),
is_active=True
)
self.assertEqual(CablePath.objects.count(), 2)

View File

@ -1514,10 +1514,11 @@ class ConsolePortTestCase(TestCase):
params = {'description': ['First', 'Second']} params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
# TODO: Fix boolean value def test_connected(self):
def test_connection_status(self): params = {'connected': True}
params = {'connection_status': 'True'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
@ -1609,10 +1610,11 @@ class ConsoleServerPortTestCase(TestCase):
params = {'description': ['First', 'Second']} params = {'description': ['First', 'Second']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
# TODO: Fix boolean value def test_connected(self):
def test_connection_status(self): params = {'connected': True}
params = {'connection_status': 'True'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
@ -1712,10 +1714,11 @@ class PowerPortTestCase(TestCase):
params = {'allocated_draw': [50, 100]} params = {'allocated_draw': [50, 100]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
# TODO: Fix boolean value def test_connected(self):
def test_connection_status(self): params = {'connected': True}
params = {'connection_status': 'True'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
@ -1812,10 +1815,11 @@ class PowerOutletTestCase(TestCase):
params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A} params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
# TODO: Fix boolean value def test_connected(self):
def test_connection_status(self): params = {'connected': True}
params = {'connection_status': 'True'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_region(self): def test_region(self):
regions = Region.objects.all()[:2] regions = Region.objects.all()[:2]
@ -1900,10 +1904,11 @@ class InterfaceTestCase(TestCase):
params = {'name': ['Interface 1', 'Interface 2']} params = {'name': ['Interface 1', 'Interface 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
# TODO: Fix boolean value def test_connected(self):
def test_connection_status(self): params = {'connected': True}
params = {'connection_status': 'True'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 4)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_enabled(self): def test_enabled(self):
params = {'enabled': 'true'} params = {'enabled': 'true'}
@ -2662,6 +2667,18 @@ class PowerFeedTestCase(TestCase):
) )
PowerFeed.objects.bulk_create(power_feeds) PowerFeed.objects.bulk_create(power_feeds)
manufacturer = Manufacturer.objects.create(name='Manufacturer', slug='manufacturer')
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model', slug='model')
device_role = DeviceRole.objects.create(name='Device Role', slug='device-role')
device = Device.objects.create(name='Device', device_type=device_type, device_role=device_role, site=sites[0])
power_ports = [
PowerPort(device=device, name='Power Port 1'),
PowerPort(device=device, name='Power Port 2'),
]
PowerPort.objects.bulk_create(power_ports)
Cable(termination_a=power_feeds[0], termination_b=power_ports[0]).save()
Cable(termination_a=power_feeds[1], termination_b=power_ports[1]).save()
def test_id(self): def test_id(self):
params = {'id': self.queryset.values_list('pk', flat=True)[:2]} params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -2723,5 +2740,17 @@ class PowerFeedTestCase(TestCase):
params = {'rack_id': [racks[0].pk, racks[1].pk]} params = {'rack_id': [racks[0].pk, racks[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_cabled(self):
params = {'cabled': 'true'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'cabled': 'false'}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_connected(self):
params = {'connected': True}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
# TODO: Connection filters # TODO: Connection filters

View File

@ -398,9 +398,11 @@ class CableTestCase(TestCase):
When a new Cable is created, it must be cached on either termination point. When a new Cable is created, it must be cached on either termination point.
""" """
interface1 = Interface.objects.get(pk=self.interface1.pk) interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertEqual(self.cable.termination_a, interface1)
interface2 = Interface.objects.get(pk=self.interface2.pk) interface2 = Interface.objects.get(pk=self.interface2.pk)
self.assertEqual(self.cable.termination_a, interface1)
self.assertEqual(interface1._cable_peer, interface2)
self.assertEqual(self.cable.termination_b, interface2) self.assertEqual(self.cable.termination_b, interface2)
self.assertEqual(interface2._cable_peer, interface1)
def test_cable_deletion(self): def test_cable_deletion(self):
""" """
@ -412,8 +414,10 @@ class CableTestCase(TestCase):
self.assertNotEqual(str(self.cable), '#None') self.assertNotEqual(str(self.cable), '#None')
interface1 = Interface.objects.get(pk=self.interface1.pk) interface1 = Interface.objects.get(pk=self.interface1.pk)
self.assertIsNone(interface1.cable) self.assertIsNone(interface1.cable)
self.assertIsNone(interface1._cable_peer)
interface2 = Interface.objects.get(pk=self.interface2.pk) interface2 = Interface.objects.get(pk=self.interface2.pk)
self.assertIsNone(interface2.cable) self.assertIsNone(interface2.cable)
self.assertIsNone(interface2._cable_peer)
def test_cabletermination_deletion(self): def test_cabletermination_deletion(self):
""" """
@ -561,628 +565,3 @@ class CableTestCase(TestCase):
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface) cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
with self.assertRaises(ValidationError): with self.assertRaises(ValidationError):
cable.clean() cable.clean()
class CablePathTestCase(TestCase):
@classmethod
def setUpTestData(cls):
site = Site.objects.create(name='Site 1', slug='site-1')
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
devicetype = DeviceType.objects.create(
manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'
)
devicerole = DeviceRole.objects.create(
name='Device Role 1', slug='device-role-1', color='ff0000'
)
provider = Provider.objects.create(name='Provider 1', slug='provider-1')
circuittype = CircuitType.objects.create(name='Circuit Type 1', slug='circuit-type-1')
circuit = Circuit.objects.create(provider=provider, type=circuittype, cid='1')
CircuitTermination.objects.bulk_create((
CircuitTermination(circuit=circuit, site=site, term_side='A', port_speed=1000),
CircuitTermination(circuit=circuit, site=site, term_side='Z', port_speed=1000),
))
# Create four network devices with four interfaces each
devices = (
Device(device_type=devicetype, device_role=devicerole, name='Device 1', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 3', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Device 4', site=site),
)
Device.objects.bulk_create(devices)
for device in devices:
Interface.objects.bulk_create((
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 3', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
Interface(device=device, name='Interface 4', type=InterfaceTypeChoices.TYPE_1GE_FIXED),
))
# Create four patch panels, each with one rear port and four front ports
patch_panels = (
Device(device_type=devicetype, device_role=devicerole, name='Panel 1', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 2', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 3', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 4', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 5', site=site),
Device(device_type=devicetype, device_role=devicerole, name='Panel 6', site=site),
)
Device.objects.bulk_create(patch_panels)
# Create patch panels with 4 positions
for patch_panel in patch_panels[:4]:
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=4, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.bulk_create((
FrontPort(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 2', rear_port=rearport, rear_port_position=2, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 3', rear_port=rearport, rear_port_position=3, type=PortTypeChoices.TYPE_8P8C),
FrontPort(device=patch_panel, name='Front Port 4', rear_port=rearport, rear_port_position=4, type=PortTypeChoices.TYPE_8P8C),
))
# Create 1-on-1 patch panels
for patch_panel in patch_panels[4:]:
rearport = RearPort.objects.create(device=patch_panel, name='Rear Port 1', positions=1, type=PortTypeChoices.TYPE_8P8C)
FrontPort.objects.create(device=patch_panel, name='Front Port 1', rear_port=rearport, rear_port_position=1, type=PortTypeChoices.TYPE_8P8C)
def test_direct_connection(self):
"""
Test a direct connection between two interfaces.
[Device 1] ----- [Device 2]
Iface1 Iface1
"""
# Create cable
cable = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable.full_clean()
cable.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete cable
cable.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connection_via_single_rear_port(self):
"""
Test a connection which passes through a rear port with exactly one front port.
1 2
[Device 1] ----- [Panel 5] ----- [Device 2]
Iface1 FP1 RP1 Iface1
"""
# Create cables (FP first, RP second)
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
self.assertEqual(cable2.termination_a.positions, 1) # Sanity check
cable2.full_clean()
cable2.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete cable 1
cable1.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connections_via_nested_single_position_rearport(self):
"""
Test a connection which passes through a single front/rear port pair between two multi-position rear ports.
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 4 | FP1
[Panel 1] ----- [Panel 5] ----- [Panel 2]
FP2 | RP1 RP1 FP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
5 6
"""
# Create cables (Panel 5 RP first, FP second)
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_b=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1'),
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
)
cable4.full_clean()
cable4.save()
cable5 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2'),
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1')
)
cable5.full_clean()
cable5.save()
cable6 = Cable(
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable6.full_clean()
cable6.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 3
cable3.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_patch(self):
"""
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 | FP1
[Panel 1] ----- [Panel 2]
FP2 | RP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
4 5
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=Interface.objects.get(device__name='Device 2', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
)
cable4.full_clean()
cable4.save()
cable5 = Cable(
termination_a=Interface.objects.get(device__name='Device 4', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2')
)
cable5.full_clean()
cable5.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 3
cable3.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_multiple_patches(self):
"""
Test two connections via patched rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2 3
[Device 1] -----------+ +---------------+ +----------- [Device 2]
Iface1 | | | | Iface1
FP1 | 4 | FP1 FP1 | 5 | FP1
[Panel 1] ----- [Panel 2] [Panel 3] ----- [Panel 4]
FP2 | RP1 RP1 | FP2 FP2 | RP1 RP1 | FP2
Iface1 | | | | Iface1
[Device 3] -----------+ +---------------+ +----------- [Device 4]
6 7 8
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1')
)
cable4.full_clean()
cable4.save()
cable5 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
)
cable5.full_clean()
cable5.save()
cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
)
cable6.full_clean()
cable6.save()
cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 2', name='Front Port 2'),
termination_b=FrontPort.objects.get(device__name='Panel 3', name='Front Port 2')
)
cable7.full_clean()
cable7.save()
cable8 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable8.full_clean()
cable8.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cables 4 and 5
cable4.delete()
cable5.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connections_via_nested_rear_ports(self):
"""
Test two connections via nested rear ports:
Device 1 <---> Device 2
Device 3 <---> Device 4
1 2
[Device 1] -----------+ +----------- [Device 2]
Iface1 | | Iface1
FP1 | 3 4 5 | FP1
[Panel 1] ----- [Panel 2] ----- [Panel 3] ----- [Panel 4]
FP2 | RP1 FP1 RP1 RP1 FP1 RP1 | FP2
Iface1 | | Iface1
[Device 3] -----------+ +----------- [Device 4]
6 7
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 1', name='Rear Port 1'),
termination_b=FrontPort.objects.get(device__name='Panel 2', name='Front Port 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 2', name='Rear Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 3', name='Rear Port 1')
)
cable4.full_clean()
cable4.save()
cable5 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 3', name='Front Port 1'),
termination_b=RearPort.objects.get(device__name='Panel 4', name='Rear Port 1')
)
cable5.full_clean()
cable5.save()
cable6 = Cable(
termination_a=Interface.objects.get(device__name='Device 3', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 1', name='Front Port 2')
)
cable6.full_clean()
cable6.save()
cable7 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 4', name='Front Port 2'),
termination_b=Interface.objects.get(device__name='Device 4', name='Interface 1')
)
cable7.full_clean()
cable7.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
endpoint_c = Interface.objects.get(device__name='Device 3', name='Interface 1')
endpoint_d = Interface.objects.get(device__name='Device 4', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertEqual(endpoint_c.connected_endpoint, endpoint_d)
self.assertEqual(endpoint_d.connected_endpoint, endpoint_c)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
self.assertTrue(endpoint_c.connection_status)
self.assertTrue(endpoint_d.connection_status)
# Delete cable 4
cable4.delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
endpoint_c.refresh_from_db()
endpoint_d.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_c.connected_endpoint)
self.assertIsNone(endpoint_d.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
self.assertIsNone(endpoint_c.connection_status)
self.assertIsNone(endpoint_d.connection_status)
def test_connection_via_circuit(self):
"""
1 2
[Device 1] ----- [Circuit] ----- [Device 2]
Iface1 A Z Iface1
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=CircuitTermination.objects.get(term_side='A')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable2.full_clean()
cable2.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete circuit
circuit = Circuit.objects.first().delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)
def test_connection_via_patched_circuit(self):
"""
1 2 3 4
[Device 1] ----- [Panel 5] ----- [Circuit] ----- [Panel 6] ----- [Device 2]
Iface1 FP1 RP1 A Z RP1 FP1 Iface1
"""
# Create cables
cable1 = Cable(
termination_a=Interface.objects.get(device__name='Device 1', name='Interface 1'),
termination_b=FrontPort.objects.get(device__name='Panel 5', name='Front Port 1')
)
cable1.full_clean()
cable1.save()
cable2 = Cable(
termination_a=RearPort.objects.get(device__name='Panel 5', name='Rear Port 1'),
termination_b=CircuitTermination.objects.get(term_side='A')
)
cable2.full_clean()
cable2.save()
cable3 = Cable(
termination_a=CircuitTermination.objects.get(term_side='Z'),
termination_b=RearPort.objects.get(device__name='Panel 6', name='Rear Port 1')
)
cable3.full_clean()
cable3.save()
cable4 = Cable(
termination_a=FrontPort.objects.get(device__name='Panel 6', name='Front Port 1'),
termination_b=Interface.objects.get(device__name='Device 2', name='Interface 1')
)
cable4.full_clean()
cable4.save()
# Retrieve endpoints
endpoint_a = Interface.objects.get(device__name='Device 1', name='Interface 1')
endpoint_b = Interface.objects.get(device__name='Device 2', name='Interface 1')
# Validate connections
self.assertEqual(endpoint_a.connected_endpoint, endpoint_b)
self.assertEqual(endpoint_b.connected_endpoint, endpoint_a)
self.assertTrue(endpoint_a.connection_status)
self.assertTrue(endpoint_b.connection_status)
# Delete circuit
circuit = Circuit.objects.first().delete()
# Refresh endpoints
endpoint_a.refresh_from_db()
endpoint_b.refresh_from_db()
# Check that connections have been nullified
self.assertIsNone(endpoint_a.connected_endpoint)
self.assertIsNone(endpoint_b.connected_endpoint)
self.assertIsNone(endpoint_a.connection_status)
self.assertIsNone(endpoint_b.connection_status)

View File

@ -1714,11 +1714,6 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'max_utilization': 50, 'max_utilization': 50,
'comments': 'New comments', 'comments': 'New comments',
'tags': [t.pk for t in tags], 'tags': [t.pk for t in tags],
# Connection
'cable': None,
'connected_endpoint': None,
'connection_status': None,
} }
cls.csv_data = ( cls.csv_data = (

View File

@ -207,7 +207,7 @@ urlpatterns = [
path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'), path('console-ports/<int:pk>/edit/', views.ConsolePortEditView.as_view(), name='consoleport_edit'),
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'), path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}), path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
path('console-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}), path('console-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleport_trace', kwargs={'model': ConsolePort}),
path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}), path('console-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleport_connect', kwargs={'termination_a_type': ConsolePort}),
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'), path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
@ -223,7 +223,7 @@ urlpatterns = [
path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'), path('console-server-ports/<int:pk>/edit/', views.ConsoleServerPortEditView.as_view(), name='consoleserverport_edit'),
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'), path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}), path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}), path('console-server-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='consoleserverport_trace', kwargs={'model': ConsoleServerPort}),
path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}), path('console-server-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='consoleserverport_connect', kwargs={'termination_a_type': ConsoleServerPort}),
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'), path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
@ -239,7 +239,7 @@ urlpatterns = [
path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'), path('power-ports/<int:pk>/edit/', views.PowerPortEditView.as_view(), name='powerport_edit'),
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'), path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}), path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
path('power-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}), path('power-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerport_trace', kwargs={'model': PowerPort}),
path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}), path('power-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerport_connect', kwargs={'termination_a_type': PowerPort}),
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'), path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
@ -255,7 +255,7 @@ urlpatterns = [
path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'), path('power-outlets/<int:pk>/edit/', views.PowerOutletEditView.as_view(), name='poweroutlet_edit'),
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'), path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}), path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:pk>/trace/', views.CableTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}), path('power-outlets/<int:pk>/trace/', views.PathTraceView.as_view(), name='poweroutlet_trace', kwargs={'model': PowerOutlet}),
path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}), path('power-outlets/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='poweroutlet_connect', kwargs={'termination_a_type': PowerOutlet}),
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'), path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
@ -271,7 +271,7 @@ urlpatterns = [
path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'), path('interfaces/<int:pk>/edit/', views.InterfaceEditView.as_view(), name='interface_edit'),
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'), path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}), path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
path('interfaces/<int:pk>/trace/', views.CableTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}), path('interfaces/<int:pk>/trace/', views.PathTraceView.as_view(), name='interface_trace', kwargs={'model': Interface}),
path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}), path('interfaces/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='interface_connect', kwargs={'termination_a_type': Interface}),
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'), path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
@ -287,7 +287,7 @@ urlpatterns = [
path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'), path('front-ports/<int:pk>/edit/', views.FrontPortEditView.as_view(), name='frontport_edit'),
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'), path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}), path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
path('front-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}), path('front-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='frontport_trace', kwargs={'model': FrontPort}),
path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}), path('front-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='frontport_connect', kwargs={'termination_a_type': FrontPort}),
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'), # path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
@ -303,7 +303,7 @@ urlpatterns = [
path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'), path('rear-ports/<int:pk>/edit/', views.RearPortEditView.as_view(), name='rearport_edit'),
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'), path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}), path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
path('rear-ports/<int:pk>/trace/', views.CableTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}), path('rear-ports/<int:pk>/trace/', views.PathTraceView.as_view(), name='rearport_trace', kwargs={'model': RearPort}),
path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}), path('rear-ports/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='rearport_connect', kwargs={'termination_a_type': RearPort}),
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'), path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
@ -383,6 +383,8 @@ urlpatterns = [
path('power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'), path('power-feeds/<int:pk>/', views.PowerFeedView.as_view(), name='powerfeed'),
path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'), path('power-feeds/<int:pk>/edit/', views.PowerFeedEditView.as_view(), name='powerfeed_edit'),
path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'), path('power-feeds/<int:pk>/delete/', views.PowerFeedDeleteView.as_view(), name='powerfeed_delete'),
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}), path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
path('power-feeds/<int:termination_a_id>/connect/<str:termination_b_type>/', views.CableCreateView.as_view(), name='powerfeed_connect', kwargs={'termination_a_type': PowerFeed}),
] ]

82
netbox/dcim/utils.py Normal file
View File

@ -0,0 +1,82 @@
from django.contrib.contenttypes.models import ContentType
from .choices import CableStatusChoices
def compile_path_node(ct_id, object_id):
return f'{ct_id}:{object_id}'
def decompile_path_node(repr):
ct_id, object_id = repr.split(':')
return int(ct_id), int(object_id)
def object_to_path_node(obj):
"""
Return a representation of an object suitable for inclusion in a CablePath path. Node representation is in the
form <ContentType ID>:<Object ID>.
"""
ct = ContentType.objects.get_for_model(obj)
return compile_path_node(ct.pk, obj.pk)
def path_node_to_object(repr):
"""
Given a path node representation, return the corresponding object.
"""
ct_id, object_id = decompile_path_node(repr)
model_class = ContentType.objects.get(pk=ct_id).model_class()
return model_class.objects.get(pk=int(object_id))
def trace_path(node):
from .models import FrontPort, RearPort
destination = None
path = []
position_stack = []
is_active = True
if node is None or node.cable is None:
return [], None, False
while node.cable is not None:
if node.cable.status != CableStatusChoices.STATUS_CONNECTED:
is_active = False
# Follow the cable to its far-end termination
path.append(object_to_path_node(node.cable))
peer_termination = node.get_cable_peer()
# Follow a FrontPort to its corresponding RearPort
if isinstance(peer_termination, FrontPort):
path.append(object_to_path_node(peer_termination))
node = peer_termination.rear_port
if node.positions > 1:
position_stack.append(peer_termination.rear_port_position)
path.append(object_to_path_node(node))
# Follow a RearPort to its corresponding FrontPort
elif isinstance(peer_termination, RearPort):
path.append(object_to_path_node(peer_termination))
if peer_termination.positions == 1:
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=1)
path.append(object_to_path_node(node))
elif position_stack:
position = position_stack.pop()
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
path.append(object_to_path_node(node))
else:
# No position indicated: path has split, so we stop at the RearPort
break
# Anything else marks the end of the path
else:
destination = peer_termination
break
if destination is None:
is_active = False
return path, destination, is_active

View File

@ -32,10 +32,10 @@ from . import filters, forms, tables
from .choices import DeviceFaceChoices from .choices import DeviceFaceChoices
from .constants import NONCONNECTABLE_IFACE_TYPES from .constants import NONCONNECTABLE_IFACE_TYPES
from .models import ( from .models import (
Cable, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate, DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort, InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site, PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
VirtualChassis, VirtualChassis,
) )
@ -1018,32 +1018,31 @@ class DeviceView(ObjectView):
# Console ports # Console ports
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
'connected_endpoint__device', 'cable', 'cable', '_path__destination',
) )
# Console server ports # Console server ports
consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter( consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter(
device=device device=device
).prefetch_related( ).prefetch_related(
'connected_endpoint__device', 'cable', 'cable', '_path__destination',
) )
# Power ports # Power ports
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
'_connected_poweroutlet__device', 'cable', 'cable', '_path__destination',
) )
# Power outlets # Power outlets
poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related( poweroutlets = PowerOutlet.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
'connected_endpoint__device', 'cable', 'power_port', 'cable', 'power_port', '_path__destination',
) )
# Interfaces # Interfaces
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related( interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable', 'lag', 'cable', '_path__destination', 'tags',
'cable__termination_a', 'cable__termination_b', 'tags'
) )
# Front ports # Front ports
@ -1118,10 +1117,8 @@ class DeviceLLDPNeighborsView(ObjectView):
def get(self, request, pk): def get(self, request, pk):
device = get_object_or_404(self.queryset, pk=pk) device = get_object_or_404(self.queryset, pk=pk)
interfaces = device.vc_interfaces.restrict(request.user, 'view').exclude( interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related('_path__destination').exclude(
type__in=NONCONNECTABLE_IFACE_TYPES type__in=NONCONNECTABLE_IFACE_TYPES
).prefetch_related(
'_connected_interface__device'
) )
return render(request, 'dcim/device_lldp_neighbors.html', { return render(request, 'dcim/device_lldp_neighbors.html', {
@ -1479,8 +1476,6 @@ class InterfaceView(ObjectView):
return render(request, 'dcim/interface.html', { return render(request, 'dcim/interface.html', {
'instance': interface, 'instance': interface,
'connected_interface': interface._connected_interface,
'connected_circuittermination': interface._connected_circuittermination,
'ipaddress_table': ipaddress_table, 'ipaddress_table': ipaddress_table,
'vlan_table': vlan_table, 'vlan_table': vlan_table,
}) })
@ -1957,9 +1952,9 @@ class CableView(ObjectView):
}) })
class CableTraceView(ObjectView): class PathTraceView(ObjectView):
""" """
Trace a cable path beginning from the given termination. Trace a cable path beginning from the given path endpoint (origin).
""" """
additional_permissions = ['dcim.view_cable'] additional_permissions = ['dcim.view_cable']
@ -1970,19 +1965,30 @@ class CableTraceView(ObjectView):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
def get(self, request, pk): def get(self, request, pk):
obj = get_object_or_404(self.queryset, pk=pk) obj = get_object_or_404(self.queryset, pk=pk)
path, split_ends, position_stack = obj.trace() related_paths = []
total_length = sum(
[entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length] # If tracing a PathEndpoint, locate the CablePath (if one exists) by its origin
) if isinstance(obj, PathEndpoint):
path = obj._path
# Otherwise, find all CablePaths which traverse the specified object
else:
related_paths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin')
# Check for specification of a particular path (when tracing pass-through ports)
try:
path_id = int(request.GET.get('cablepath_id'))
except TypeError:
path_id = None
if path_id in list(related_paths.values_list('pk', flat=True)):
path = CablePath.objects.get(pk=path_id)
else:
path = related_paths.first()
return render(request, 'dcim/cable_trace.html', { return render(request, 'dcim/cable_trace.html', {
'obj': obj, 'obj': obj,
'trace': path, 'path': path,
'split_ends': split_ends, 'related_paths': related_paths,
'position_stack': position_stack, 'total_length': path.get_total_length(),
'total_length': total_length,
}) })
@ -2077,12 +2083,8 @@ class CableBulkDeleteView(BulkDeleteView):
class ConsoleConnectionsListView(ObjectListView): class ConsoleConnectionsListView(ObjectListView):
queryset = ConsolePort.objects.prefetch_related( queryset = ConsolePort.objects.prefetch_related(
'device', 'connected_endpoint__device' 'device', '_path__destination'
).filter( ).filter(_path__isnull=False).order_by('device')
connected_endpoint__isnull=False
).order_by(
'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
)
filterset = filters.ConsoleConnectionFilterSet filterset = filters.ConsoleConnectionFilterSet
filterset_form = forms.ConsoleConnectionFilterForm filterset_form = forms.ConsoleConnectionFilterForm
table = tables.ConsoleConnectionTable table = tables.ConsoleConnectionTable
@ -2091,15 +2093,15 @@ class ConsoleConnectionsListView(ObjectListView):
def queryset_to_csv(self): def queryset_to_csv(self):
csv_data = [ csv_data = [
# Headers # Headers
','.join(['console_server', 'port', 'device', 'console_port', 'connection_status']) ','.join(['console_server', 'port', 'device', 'console_port', 'reachable'])
] ]
for obj in self.queryset: for obj in self.queryset:
csv = csv_format([ csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj._path.destination.device.identifier if obj._path.destination else None,
obj.connected_endpoint.name if obj.connected_endpoint else None, obj._path.destination.name if obj._path.destination else None,
obj.device.identifier, obj.device.identifier,
obj.name, obj.name,
obj.get_connection_status_display(), obj._path.is_active
]) ])
csv_data.append(csv) csv_data.append(csv)
@ -2108,12 +2110,8 @@ class ConsoleConnectionsListView(ObjectListView):
class PowerConnectionsListView(ObjectListView): class PowerConnectionsListView(ObjectListView):
queryset = PowerPort.objects.prefetch_related( queryset = PowerPort.objects.prefetch_related(
'device', '_connected_poweroutlet__device' 'device', '_path__destination'
).filter( ).filter(_path__isnull=False).order_by('device')
_connected_poweroutlet__isnull=False
).order_by(
'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
)
filterset = filters.PowerConnectionFilterSet filterset = filters.PowerConnectionFilterSet
filterset_form = forms.PowerConnectionFilterForm filterset_form = forms.PowerConnectionFilterForm
table = tables.PowerConnectionTable table = tables.PowerConnectionTable
@ -2122,15 +2120,15 @@ class PowerConnectionsListView(ObjectListView):
def queryset_to_csv(self): def queryset_to_csv(self):
csv_data = [ csv_data = [
# Headers # Headers
','.join(['pdu', 'outlet', 'device', 'power_port', 'connection_status']) ','.join(['pdu', 'outlet', 'device', 'power_port', 'reachable'])
] ]
for obj in self.queryset: for obj in self.queryset:
csv = csv_format([ csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj._path.destination.device.identifier if obj._path.destination else None,
obj.connected_endpoint.name if obj.connected_endpoint else None, obj._path.destination.name if obj._path.destination else None,
obj.device.identifier, obj.device.identifier,
obj.name, obj.name,
obj.get_connection_status_display(), obj._path.is_active
]) ])
csv_data.append(csv) csv_data.append(csv)
@ -2139,14 +2137,12 @@ class PowerConnectionsListView(ObjectListView):
class InterfaceConnectionsListView(ObjectListView): class InterfaceConnectionsListView(ObjectListView):
queryset = Interface.objects.prefetch_related( queryset = Interface.objects.prefetch_related(
'device', 'cable', '_connected_interface__device' 'device', '_path__destination'
).filter( ).filter(
# Avoid duplicate connections by only selecting the lower PK in a connected pair # Avoid duplicate connections by only selecting the lower PK in a connected pair
_connected_interface__isnull=False, _path__isnull=False,
pk__lt=F('_connected_interface') pk__lt=F('_path__destination_id')
).order_by( ).order_by('device')
'device'
)
filterset = filters.InterfaceConnectionFilterSet filterset = filters.InterfaceConnectionFilterSet
filterset_form = forms.InterfaceConnectionFilterForm filterset_form = forms.InterfaceConnectionFilterForm
table = tables.InterfaceConnectionTable table = tables.InterfaceConnectionTable
@ -2156,16 +2152,16 @@ class InterfaceConnectionsListView(ObjectListView):
csv_data = [ csv_data = [
# Headers # Headers
','.join([ ','.join([
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status' 'device_a', 'interface_a', 'device_b', 'interface_b', 'reachable'
]) ])
] ]
for obj in self.queryset: for obj in self.queryset:
csv = csv_format([ csv = csv_format([
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None, obj._path.destination.device.identifier if obj._path.destination else None,
obj.connected_endpoint.name if obj.connected_endpoint else None, obj._path.destination.name if obj._path.destination else None,
obj.device.identifier, obj.device.identifier,
obj.name, obj.name,
obj.get_connection_status_display(), obj._path.is_active
]) ])
csv_data.append(csv) csv_data.append(csv)

View File

@ -190,15 +190,15 @@ class HomeView(View):
def get(self, request): def get(self, request):
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').filter( connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
connected_endpoint__isnull=False _path__destination_id__isnull=False
) )
connected_powerports = PowerPort.objects.restrict(request.user, 'view').filter( connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_connected_poweroutlet__isnull=False _path__destination_id__isnull=False
) )
connected_interfaces = Interface.objects.restrict(request.user, 'view').filter( connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
_connected_interface__isnull=False, _path__destination_id__isnull=False,
pk__lt=F('_connected_interface') pk__lt=F('_path__destination_id')
) )
# Report Results # Report Results

View File

@ -7,101 +7,73 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-1 text-center"> <div class="col-md-5 col-sm-12 text-center">
<h4>Near End</h4> {% for near_end, cable, far_end in path.origin.trace %}
</div>
<div class="col-md-3 text-center"> {# Near end #}
{% if total_length %}<h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>{% endif %} {% if near_end.device %}
</div> {% include 'dcim/trace/device.html' with device=near_end.device %}
<div class="col-md-4 text-center"> {% include 'dcim/trace/termination.html' with termination=near_end %}
<h4>Far End</h4> {% elif near_end.power_panel %}
</div> {% include 'dcim/trace/powerfeed.html' with powerfeed=near_end %}
</div> {% elif near_end.circuit %}
{% for near_end, cable, far_end in trace %} {% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %}
<div class="row"> {% include 'dcim/trace/termination.html' with termination=near_end %}
<div class="col-md-1 text-right">
<h3>{{ forloop.counter }}</h3>
</div>
<div class="col-md-4">
{% include 'dcim/inc/cable_trace_end.html' with end=near_end %}
</div>
<div class="col-md-3 text-center">
{% if cable %}
<h4>
<a href="{% url 'dcim:cable' pk=cable.pk %}">
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
</a>
</h4>
<p><span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span></p>
<p>{{ cable.get_type_display|default:"" }}</p>
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
{% if cable.color %}
<span class="label color-block center-block" style="background-color: #{{ cable.color }}">&nbsp;</span>
{% endif %}
{% else %} {% else %}
<h4 class="text-muted">No Cable</h4> <h3 class="text-danger text-center">Split Paths!</h3>
{# TODO: Present the user with successive paths to choose from #}
{% endif %} {% endif %}
</div>
<div class="col-md-4"> {# Cable #}
{% if far_end %} {% if cable %}
{% include 'dcim/inc/cable_trace_end.html' with end=far_end %} <div class="row">
{% include 'dcim/trace/cable.html' %}
</div>
{% endif %} {% endif %}
</div>
{# Far end #}
{% if far_end.device %}
{% include 'dcim/trace/termination.html' with termination=far_end %}
{% if forloop.last %}
{% include 'dcim/trace/device.html' with device=far_end.device %}
{% endif %}
{% elif far_end.power_panel %}
{% include 'dcim/trace/powerfeed.html' with powerfeed=far_end %}
{% elif far_end.circuit %}
{% include 'dcim/trace/termination.html' with termination=far_end %}
{% if forloop.last %}
{% include 'dcim/trace/circuit.html' with circuit=far_end.circuit %}
{% endif %}
{% endif %}
{% if forloop.last and far_end %}
<div class="row">
<div class="text-center">
<h3 class="text-success text-center">Trace completed!</h3>
{% if total_length %}
<h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
<div class="col-md-7 col-sm-12">
<h4 class="text-center">Related Paths</h4>
<ul class="nav nav-pills nav-stacked">
{% for cablepath in related_paths %}
<li role="presentation"{% if cablepath.pk == path.pk %} class="active"{% endif %}>
<a href="?cablepath_id={{ cablepath.pk }}">
From {{ cablepath.origin }} ({{ cablepath.origin.parent }})
to {{ cablepath.destination }} ({{ cablepath.destination.parent }})
</a>
</li>
{% endfor %}
</ul>
</div> </div>
<hr />
{% endfor %}
<div class="row">
{% if split_ends %}
<div class="col-md-7 col-md-offset-3">
<div class="panel panel-warning">
<div class="panel-heading">
<strong><i class="fa fa-warning"></i> Trace Split</strong>
</div>
<div class="panel-body">
There are multiple possible paths from this point. Select a port to continue.
</div>
</div>
<div class="panel panel-default">
<table class="panel-body table">
<thead>
<tr class="table-headings">
<th>Port</th>
<th>Connected</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
{% for termination in split_ends %}
<tr>
<td><a href="{% url 'dcim:frontport_trace' pk=termination.pk %}">{{ termination }}</a></td>
<td>
{% if termination.cable %}
<i class="fa fa-check text-success" title="Yes"></i>
{% else %}
<i class="fa fa-times text-danger" title="No"></i>
{% endif %}
</td>
<td>{{ termination.get_type_display }}</td>
<td>{{ termination.description|placeholder }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% elif position_stack %}
<div class="col-md-11 col-md-offset-1">
<h3 class="text-warning text-center">
{% with last_position=position_stack|last %}
Trace completed, but there is no Front Port corresponding to
<a href="{{ last_position.device.get_absolute_url }}">{{ last_position.device }}</a> {{ last_position }}.<br>
Therefore no end-to-end connection can be established.
{% endwith %}
</h3>
</div>
{% else %}
<div class="col-md-11 col-md-offset-1">
<h3 class="text-success text-center">Trace completed!</h3>
</div>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -44,6 +44,15 @@
</div> </div>
{% if instance.cable %} {% if instance.cable %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:consoleport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if instance.connected_endpoint %} {% if instance.connected_endpoint %}
<tr> <tr>
<td>Device</td> <td>Device</td>
@ -65,26 +74,17 @@
<td>Description</td> <td>Description</td>
<td>{{ instance.connected_endpoint.description|placeholder }}</td> <td>{{ instance.connected_endpoint.description|placeholder }}</td>
</tr> </tr>
<tr>
<td>Path Status</td>
<td>
{% if instance.path.is_active %}
<span class="label label-success">Reachable</span>
{% else %}
<span class="label label-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %} {% endif %}
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:consoleport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if instance.connection_status %}
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
{% else %}
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
{% endif %}
</td>
</tr>
</table> </table>
{% else %} {% else %}
<div class="panel-body text-muted"> <div class="panel-body text-muted">

View File

@ -44,6 +44,15 @@
</div> </div>
{% if instance.cable %} {% if instance.cable %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:consoleserverport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if instance.connected_endpoint %} {% if instance.connected_endpoint %}
<tr> <tr>
<td>Device</td> <td>Device</td>
@ -65,26 +74,17 @@
<td>Description</td> <td>Description</td>
<td>{{ instance.connected_endpoint.description|placeholder }}</td> <td>{{ instance.connected_endpoint.description|placeholder }}</td>
</tr> </tr>
<tr>
<td>Path Status</td>
<td>
{% if instance.path.is_active %}
<span class="label label-success">Reachable</span>
{% else %}
<span class="label label-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %} {% endif %}
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:consoleserverport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if instance.connection_status %}
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
{% else %}
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
{% endif %}
</td>
</tr>
</table> </table>
{% else %} {% else %}
<div class="panel-body text-muted"> <div class="panel-body text-muted">

View File

@ -479,7 +479,7 @@
</div> </div>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane active" id="interfaces"> <div role="tabpanel" class="tab-pane" id="interfaces">
<form method="post"> <form method="post">
{% csrf_token %} {% csrf_token %}
<div class="panel panel-default"> <div class="panel panel-default">
@ -506,6 +506,7 @@
<th>MTU</th> <th>MTU</th>
<th>Mode</th> <th>Mode</th>
<th>Cable</th> <th>Cable</th>
<th colspan="2">Cable Termination</th>
<th colspan="2">Connection</th> <th colspan="2">Connection</th>
<th></th> <th></th>
</tr> </tr>
@ -566,7 +567,7 @@
<th>Position</th> <th>Position</th>
<th>Description</th> <th>Description</th>
<th>Cable</th> <th>Cable</th>
<th colspan="2">Connection</th> <th colspan="2">Cable Termination</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -623,7 +624,7 @@
<th>Positions</th> <th>Positions</th>
<th>Description</th> <th>Description</th>
<th>Cable</th> <th>Cable</th>
<th colspan="2">Connection</th> <th colspan="2">Cable Termination</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -679,6 +680,7 @@
<th>Type</th> <th>Type</th>
<th>Description</th> <th>Description</th>
<th>Cable</th> <th>Cable</th>
<th colspan="2">Cable Termination</th>
<th colspan="2">Connection</th> <th colspan="2">Connection</th>
<th></th> <th></th>
</tr> </tr>
@ -732,6 +734,7 @@
<th>Type</th> <th>Type</th>
<th>Description</th> <th>Description</th>
<th>Cable</th> <th>Cable</th>
<th colspan="2">Cable Termination</th>
<th colspan="2">Connection</th> <th colspan="2">Connection</th>
<th></th> <th></th>
</tr> </tr>

View File

@ -1,34 +0,0 @@
{% load helpers %}
<div class="panel panel-default">
<div class="panel-heading text-center">
{% if end.device %}
<strong><a href="{{ end.device.get_absolute_url }}">{{ end.device }}</a></strong><br/>
<small>
<a href="{{ end.device.site.get_absolute_url }}">{{ end.device.site }}</a>
{% if end.device.rack %}
/ <a href="{{ end.device.rack.get_absolute_url }}">{{ end.device.rack }}</a>
{% endif %}
</small>
{% else %}
<strong><a href="{{ end.circuit.provider.get_absolute_url }}">{{ end.circuit.provider }}</a></strong>
{% endif %}
</div>
<div class="panel-body text-center">
{% if end.device %}
{# Device component #}
{% with model=end|meta:"verbose_name" %}
<strong>{{ model|bettertitle }} {{ end }}</strong><br />
{% if model == 'interface' %}
{{ end.get_type_display }}
{% elif model == 'front port' or model == 'rear port' %}
{{ end.get_type_display }}
{% endif %}
{% endwith %}
{% else %}
{# Circuit termination #}
<strong><a href="{{ end.circuit.get_absolute_url }}">{{ end.circuit }}</a></strong><br/>
{{ end }}
{% endif %}
</div>
</div>

View File

@ -0,0 +1,14 @@
<td>
{% if termination.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ termination.parent.get_absolute_url }}">
{{ termination.parent.provider }}
{{ termination.parent }}
</a>
{% else %}
<a href="{{ termination.parent.get_absolute_url }}">{{ termination.parent }}</a>
{% endif %}
</td>
<td>
<a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
</td>

View File

@ -24,31 +24,23 @@
</td> </td>
{# Cable #} {# Cable #}
<td> {% if cp.cable %}
{% if cp.cable %} <td>
<a href="{{ cp.cable.get_absolute_url }}">{{ cp.cable }}</a> <a href="{{ cp.cable.get_absolute_url }}">{{ cp.cable }}</a>
<a href="{% url 'dcim:consoleport_trace' pk=cp.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:consoleport_trace' pk=cp.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
{% else %}
&mdash;
{% endif %}
</td>
{# Connection #}
{% if cp.connected_endpoint %}
<td>
<a href="{% url 'dcim:device' pk=cp.connected_endpoint.device.pk %}">{{ cp.connected_endpoint.device }}</a>
</td>
<td>
{{ cp.connected_endpoint }}
</td> </td>
{% include 'dcim/inc/cabletermination.html' with termination=cp.get_cable_peer %}
{% else %} {% else %}
<td colspan="2"> <td colspan="3">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
{# Connection #}
{% include 'dcim/inc/endpoint_connection.html' with path=cp.path %}
{# Actions #} {# Actions #}
<td class="text-right noprint"> <td class="text-right noprint">
{% if cp.cable %} {% if cp.cable %}

View File

@ -26,31 +26,23 @@
</td> </td>
{# Cable #} {# Cable #}
<td class="text-nowrap"> {% if csp.cable %}
{% if csp.cable %} <td>
<a href="{{ csp.cable.get_absolute_url }}">{{ csp.cable }}</a> <a href="{{ csp.cable.get_absolute_url }}">{{ csp.cable }}</a>
<a href="{% url 'dcim:consoleserverport_trace' pk=csp.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:consoleserverport_trace' pk=csp.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
{% else %}
<span class="text-muted">&mdash;</span>
{% endif %}
</td>
{# Connection #}
{% if csp.connected_endpoint %}
<td>
<a href="{% url 'dcim:device' pk=csp.connected_endpoint.device.pk %}">{{ csp.connected_endpoint.device }}</a>
</td>
<td>
{{ csp.connected_endpoint }}
</td> </td>
{% include 'dcim/inc/cabletermination.html' with termination=csp.get_cable_peer %}
{% else %} {% else %}
<td colspan="2"> <td colspan="3">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endif %}
{# Connection #}
{% include 'dcim/inc/endpoint_connection.html' with path=csp.path %}
{# Actions #} {# Actions #}
<td class="text-right noprint"> <td class="text-right noprint">
{% if csp.cable %} {% if csp.cable %}

View File

@ -0,0 +1,8 @@
{% if path.destination_id %}
{% with endpoint=path.destination %}
<td><a href="{{ endpoint.parent.get_absolute_url }}">{{ endpoint.parent }}</a></td>
<td><a href="{{ endpoint.get_absolute_url }}">{{ endpoint }}</a></td>
{% endwith %}
{% else %}
<td colspan="2" class="text-muted">Not connected</td>
{% endif %}

View File

@ -24,7 +24,7 @@
{# Description #} {# Description #}
<td>{{ frontport.description|placeholder }}</td> <td>{{ frontport.description|placeholder }}</td>
{# Cable/connection #} {# Cable #}
{% if frontport.cable %} {% if frontport.cable %}
<td> <td>
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a> <a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
@ -32,22 +32,7 @@
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
</td> </td>
{% with far_end=frontport.get_cable_peer %} {% include 'dcim/inc/cabletermination.html' with termination=frontport.get_cable_peer %}
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %} {% else %}
<td colspan="3"> <td colspan="3">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>

View File

@ -45,19 +45,19 @@
<td>{{ iface.get_mode_display|default:"&mdash;" }}</td> <td>{{ iface.get_mode_display|default:"&mdash;" }}</td>
{# Cable #} {# Cable #}
<td class="text-nowrap"> {% if iface.cable %}
{% if iface.cable %} <td>
<a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a> <a href="{{ iface.cable.get_absolute_url }}">{{ iface.cable }}</a>
{% if iface.cable.color %}
<span class="inline-color-block" style="background-color: #{{ iface.cable.color }}">&nbsp;</span>
{% endif %}
<a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:interface_trace' pk=iface.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
{% else %} </td>
<span class="text-muted">&mdash;</span> {% include 'dcim/inc/cabletermination.html' with termination=iface.get_cable_peer %}
{% endif %} {% else %}
</td> <td colspan="3">
<span class="text-muted">Not connected</span>
</td>
{% endif %}
{# Connection or type #} {# Connection or type #}
{% if iface.is_lag %} {% if iface.is_lag %}
@ -75,65 +75,8 @@
<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.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_type_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 %}
{% if peer_termination %}
{% if peer_termination.connected_endpoint %}
<td>
<a href="{% url 'dcim:device' pk=peer_termination.connected_endpoint.device.pk %}">
{{ peer_termination.connected_endpoint.device }}
</a><br/>
<small>via <i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
{{ iface.connected_endpoint.circuit.provider }}
{{ iface.connected_endpoint.circuit }}
</a>
</small>
</td>
<td>
{{ peer_termination.connected_endpoint }}
</td>
{% else %}
<td colspan="2">
<a href="{% url 'dcim:site' slug=peer_termination.site.slug %}">
{{ peer_termination.site }}
</a>
via <i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
{{ iface.connected_endpoint.circuit.provider }}
{{ iface.connected_endpoint.circuit }}
</a>
</td>
{% endif %}
{% else %}
<td colspan="2">
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ iface.connected_endpoint.circuit.get_absolute_url }}">
{{ iface.connected_endpoint.circuit.provider }}
{{ iface.connected_endpoint.circuit }}
</a>
</td>
{% endif %}
{% endwith %}
{% else %} {% else %}
<td colspan="2"> {% include 'dcim/inc/endpoint_connection.html' with path=iface.path %}
<span class="text-muted">Not connected</span>
</td>
{% endif %} {% endif %}
{# Buttons #} {# Buttons #}

View File

@ -37,39 +37,32 @@
</td> </td>
{# Cable #} {# Cable #}
<td class="text-nowrap"> {% if po.cable %}
{% if po.cable %} <td>
<a href="{{ po.cable.get_absolute_url }}">{{ po.cable }}</a> <a href="{{ po.cable.get_absolute_url }}">{{ po.cable }}</a>
<a href="{% url 'dcim:poweroutlet_trace' pk=po.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:poweroutlet_trace' pk=po.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
{% else %} </td>
<span class="text-muted">&mdash;</span> {% else %}
{% endif %} <td><span class="text-muted">Not connected</span></td>
</td> {% endif %}
{# Connection #} {# Connection #}
{% if po.connected_endpoint %} {% with path=po.path %}
{% with pp=po.connected_endpoint %} {% include 'dcim/inc/endpoint_connection.html' %}
<td> <td>
<a href="{% url 'dcim:device' pk=pp.device.pk %}">{{ pp.device }}</a> {% if paths|length == 1 %}
</td> {% with pp=paths.0.destination %}
<td> {% if pp.allocated_draw %}
{{ pp }} {{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %}
</td> {% elif pp.maximum_draw %}
<td> {{ pp.maximum_draw }}W
{% if pp.allocated_draw %} {% endif %}
{{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %} {% endwith %}
{% elif pp.maximum_draw %} {% endif %}
{{ pp.maximum_draw }}W
{% endif %}
</td>
{% endwith %}
{% else %}
<td colspan="3">
<span class="text-muted">Not connected</span>
</td> </td>
{% endif %} {% endwith %}
{# Actions #} {# Actions #}
<td class="text-right noprint"> <td class="text-right noprint">

View File

@ -33,35 +33,20 @@
</td> </td>
{# Cable #} {# Cable #}
<td> {% if pp.cable %}
{% if pp.cable %} <td>
<a href="{{ pp.cable.get_absolute_url }}">{{ pp.cable }}</a> <a href="{{ pp.cable.get_absolute_url }}">{{ pp.cable }}</a>
<a href="{% url 'dcim:powerport_trace' pk=pp.pk %}" class="btn btn-primary btn-xs" title="Trace"> <a href="{% url 'dcim:powerport_trace' pk=pp.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
{% else %}
&mdash;
{% endif %}
</td>
{# Connection #}
{% if pp.connected_endpoint.device %}
<td>
<a href="{% url 'dcim:device' pk=pp.connected_endpoint.device.pk %}">{{ pp.connected_endpoint.device }}</a>
</td>
<td>
{{ pp.connected_endpoint }}
</td>
{% elif pp.connected_endpoint %}
<td colspan="2">
<a href="{{ pp.connected_endpoint.get_absolute_url }}">{{ pp.connected_endpoint }}</a>
</td> </td>
{% else %} {% else %}
<td colspan="2"> <td><span class="text-muted">Not connected</span></td>
<span class="text-muted">Not connected</span>
</td>
{% endif %} {% endif %}
{# Connection #}
{% include 'dcim/inc/endpoint_connection.html' with path=pp.path %}
{# Actions #} {# Actions #}
<td class="text-right noprint"> <td class="text-right noprint">
{% if pp.cable %} {% if pp.cable %}

View File

@ -23,7 +23,7 @@
{# Description #} {# Description #}
<td>{{ rearport.description|placeholder }}</td> <td>{{ rearport.description|placeholder }}</td>
{# Cable/connection #} {# Cable #}
{% if rearport.cable %} {% if rearport.cable %}
<td> <td>
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a> <a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
@ -31,22 +31,7 @@
<i class="fa fa-share-alt" aria-hidden="true"></i> <i class="fa fa-share-alt" aria-hidden="true"></i>
</a> </a>
</td> </td>
{% with far_end=rearport.get_cable_peer %} {% include 'dcim/inc/cabletermination.html' with termination=rearport.get_cable_peer %}
<td>
{% if far_end.parent.provider %}
<i class="fa fa-fw fa-globe" title="Circuit"></i>
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent.provider }}
{{ far_end.parent }}
</a>
{% else %}
<a href="{{ far_end.parent.get_absolute_url }}">
{{ far_end.parent }}
</a>
{% endif %}
</td>
<td>{{ far_end }}</td>
{% endwith %}
{% else %} {% else %}
<td colspan="3"> <td colspan="3">
<span class="text-muted">Not connected</span> <span class="text-muted">Not connected</span>

View File

@ -77,61 +77,72 @@
</div> </div>
{% if instance.cable %} {% if instance.cable %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
{% if connected_interface %} <tr>
<tr> <td>Cable</td>
<td>Device</td> <td>
<td> <a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a> <a href="{% url 'dcim:interface_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
</td> <i class="fa fa-share-alt" aria-hidden="true"></i>
</tr> </a>
<tr> </td>
<td>Name</td> </tr>
<td> {% if instance.connected_endpoint.device %}
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a> {% with iface=instance.connected_endpoint %}
</td> <tr>
</tr> <td>Device</td>
<tr> <td>
<td>Type</td> <a href="{{ iface.device.get_absolute_url }}">{{ iface.device }}</a>
<td>{{ connected_interface.get_type_display }}</td> </td>
</tr> </tr>
<tr> <tr>
<td>Enabled</td> <td>Name</td>
<td> <td>
{% if connected_interface.enabled %} <a href="{{ iface.get_absolute_url }}">{{ iface.name }}</a>
<span class="text-success"><i class="fa fa-check"></i></span> </td>
{% else %} </tr>
<span class="text-danger"><i class="fa fa-close"></i></span> <tr>
{% endif %} <td>Type</td>
</td> <td>{{ iface.get_type_display }}</td>
</tr> </tr>
<tr> <tr>
<td>LAG</td> <td>Enabled</td>
<td> <td>
{% if connected_interface.lag%} {% if iface.enabled %}
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a> <span class="text-success"><i class="fa fa-check"></i></span>
{% else %} {% else %}
<span class="text-muted">None</span> <span class="text-danger"><i class="fa fa-close"></i></span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Description</td> <td>LAG</td>
<td>{{ connected_interface.description|placeholder }}</td> <td>
</tr> {% if iface.lag%}
<tr> <a href="{{ iface.lag.get_absolute_url }}">{{ iface.lag }}</a>
<td>MTU</td> {% else %}
<td>{{ connected_interface.mtu|placeholder }}</td> <span class="text-muted">None</span>
</tr> {% endif %}
<tr> </td>
<td>MAC Address</td> </tr>
<td>{{ connected_interface.mac_address|placeholder }}</td> <tr>
</tr> <td>Description</td>
<tr> <td>{{ iface.description|placeholder }}</td>
<td>802.1Q Mode</td> </tr>
<td>{{ connected_interface.get_mode_display }}</td> <tr>
</tr> <td>MTU</td>
{% elif connected_circuittermination %} <td>{{ iface.mtu|placeholder }}</td>
{% with ct=connected_circuittermination %} </tr>
<tr>
<td>MAC Address</td>
<td>{{ iface.mac_address|placeholder }}</td>
</tr>
<tr>
<td>802.1Q Mode</td>
<td>{{ iface.get_mode_display }}</td>
</tr>
{% endwith %}
{% elif instance.connected_endpoint.circuit %}
{% with ct=instance.connected_endpoint %}
<tr> <tr>
<td>Provider</td> <td>Provider</td>
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td> <td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
@ -147,21 +158,12 @@
{% endwith %} {% endwith %}
{% endif %} {% endif %}
<tr> <tr>
<td>Cable</td> <td>Path Status</td>
<td> <td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a> {% if instance.path.is_active %}
<a href="{% url 'dcim:interface_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace"> <span class="label label-success">Reachable</span>
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if instance.connection_status %}
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
{% else %} {% else %}
<span class="label label-info">{{ instance.get_connection_status_display }}</span> <span class="label label-danger">Not Reachable</span>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>

View File

@ -123,11 +123,6 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=powerfeed %}
{% include 'extras/inc/tags_panel.html' with tags=powerfeed.tags.all url='dcim:powerfeed_list' %}
{% plugin_left_page powerfeed %}
</div>
<div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Electrical Characteristics</strong> <strong>Electrical Characteristics</strong>
@ -155,6 +150,70 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=powerfeed %}
{% include 'extras/inc/tags_panel.html' with tags=powerfeed.tags.all url='dcim:powerfeed_list' %}
{% plugin_left_page powerfeed %}
</div>
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
<strong>Connection</strong>
</div>
{% if powerfeed.cable %}
<table class="table table-hover panel-body attr-table">
<tr>
<td>Cable</td>
<td>
<a href="{{ powerfeed.cable.get_absolute_url }}">{{ powerfeed.cable }}</a>
<a href="{% url 'dcim:consoleport_trace' pk=powerfeed.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if powerfeed.connected_endpoint %}
<tr>
<td>Device</td>
<td>
<a href="{{ powerfeed.connected_endpoint.device.get_absolute_url }}">{{ powerfeed.connected_endpoint.device }}</a>
</td>
</tr>
<tr>
<td>Name</td>
<td>
<a href="{{ powerfeed.connected_endpoint.get_absolute_url }}">{{ powerfeed.connected_endpoint.name }}</a>
</td>
</tr>
<tr>
<td>Type</td>
<td>{{ powerfeed.connected_endpoint.get_type_display|placeholder }}</td>
</tr>
<tr>
<td>Description</td>
<td>{{ powerfeed.connected_endpoint.description|placeholder }}</td>
</tr>
<tr>
<td>Path Status</td>
<td>
{% if powerfeed.path.is_active %}
<span class="label label-success">Reachable</span>
{% else %}
<span class="label label-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %}
</table>
{% else %}
<div class="panel-body text-muted">
{% if perms.dcim.add_cable %}
<a href="{% url 'dcim:powerfeed_connect' termination_a_id=powerfeed.pk termination_b_type='power-port' %}?return_url={{ powerfeed.get_absolute_url }}" class="btn btn-primary btn-sm pull-right">
<span class="glyphicon glyphicon-resize-small" aria-hidden="true"></span> Connect
</a>
{% endif %}
Not connected
</div>
{% endif %}
</div>
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Comments</strong> <strong>Comments</strong>

View File

@ -52,6 +52,15 @@
</div> </div>
{% if instance.cable %} {% if instance.cable %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:poweroutlet_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if instance.connected_endpoint %} {% if instance.connected_endpoint %}
<tr> <tr>
<td>Device</td> <td>Device</td>
@ -73,26 +82,17 @@
<td>Description</td> <td>Description</td>
<td>{{ instance.connected_endpoint.description|placeholder }}</td> <td>{{ instance.connected_endpoint.description|placeholder }}</td>
</tr> </tr>
<tr>
<td>Path Status</td>
<td>
{% if instance.path.is_active %}
<span class="label label-success">Reachable</span>
{% else %}
<span class="label label-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %} {% endif %}
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:poweroutlet_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if instance.connection_status %}
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
{% else %}
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
{% endif %}
</td>
</tr>
</table> </table>
{% else %} {% else %}
<div class="panel-body text-muted"> <div class="panel-body text-muted">

View File

@ -58,7 +58,7 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-md-3"> <div class="col-md-6">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<strong>Power Panel</strong> <strong>Power Panel</strong>
@ -82,17 +82,17 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=powerpanel %}
{% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %}
{% plugin_left_page powerpanel %} {% plugin_left_page powerpanel %}
</div> </div>
<div class="col-md-9"> <div class="col-md-6">
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %} {% include 'inc/custom_fields_panel.html' with obj=powerpanel %}
{% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %}
{% plugin_right_page powerpanel %} {% plugin_right_page powerpanel %}
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %}
{% plugin_full_width_page powerpanel %} {% plugin_full_width_page powerpanel %}
</div> </div>
</div> </div>

View File

@ -52,6 +52,15 @@
</div> </div>
{% if instance.cable %} {% if instance.cable %}
<table class="table table-hover panel-body attr-table"> <table class="table table-hover panel-body attr-table">
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:powerport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
{% if instance.connected_endpoint %} {% if instance.connected_endpoint %}
<tr> <tr>
<td>Device</td> <td>Device</td>
@ -73,26 +82,17 @@
<td>Description</td> <td>Description</td>
<td>{{ instance.connected_endpoint.description|placeholder }}</td> <td>{{ instance.connected_endpoint.description|placeholder }}</td>
</tr> </tr>
<tr>
<td>Path Status</td>
<td>
{% if instance.path.is_active %}
<span class="label label-success">Reachable</span>
{% else %}
<span class="label label-danger">Not Reachable</span>
{% endif %}
</td>
</tr>
{% endif %} {% endif %}
<tr>
<td>Cable</td>
<td>
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
<a href="{% url 'dcim:powerport_trace' pk=instance.pk %}" class="btn btn-primary btn-xs" title="Trace">
<i class="fa fa-share-alt" aria-hidden="true"></i>
</a>
</td>
</tr>
<tr>
<td>Connection Status</td>
<td>
{% if instance.connection_status %}
<span class="label label-success">{{ instance.get_connection_status_display }}</span>
{% else %}
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
{% endif %}
</td>
</tr>
</table> </table>
{% else %} {% else %}
<div class="panel-body text-muted"> <div class="panel-body text-muted">

View File

@ -150,10 +150,14 @@
<a href="{% url 'dcim:device_list' %}?rack_id={{ rack.id }}">{{ device_count }}</a> <a href="{% url 'dcim:device_list' %}?rack_id={{ rack.id }}">{{ device_count }}</a>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Utilization</td> <td>Space Utilization</td>
<td>{% utilization_graph rack.get_utilization %}</td> <td>{% utilization_graph rack.get_utilization %}</td>
</tr> </tr>
<tr>
<td>Power Utilization</td>
<td>{% utilization_graph rack.get_power_utilization %}</td>
</tr>
</table> </table>
</div> </div>
<div class="panel panel-default"> <div class="panel panel-default">

View File

@ -0,0 +1,15 @@
<div class="col-md-6 col-md-offset-3">
<h4><i class="fa fa-arrow-up"></i></h4>
<h4>
<a href="{% url 'dcim:cable' pk=cable.pk %}">
{% if cable.label %}<code>{{ cable.label }}</code>{% else %}Cable #{{ cable.pk }}{% endif %}
</a>
</h4>
<p><span class="label label-{{ cable.get_status_class }}">{{ cable.get_status_display }}</span></p>
<p>{{ cable.get_type_display|default:"" }}</p>
{% if cable.length %}{{ cable.length }} {{ cable.get_length_unit_display }}{% endif %}
{% if cable.color %}
<span class="label color-block center-block" style="background-color: #{{ cable.color }}">&nbsp;</span>
{% endif %}
<h4><i class="fa fa-arrow-down"></i></h4>
</div>

View File

@ -0,0 +1,6 @@
<div class="panel panel-warning">
<div class="panel-heading">
<strong>Circuit <a href="{{ circuit.get_absolute_url }}">{{ circuit }}</a></strong><br />
<a href="{{ circuit.provider.get_absolute_url }}">{{ circuit.provider }}</a>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="panel panel-info">
<div class="panel-heading">
<strong>Device <a href="{{ device.get_absolute_url }}">{{ device }}</a></strong><br />
<a href="{{ device.site.get_absolute_url }}">{{ device.site }}</a>
{% if device.rack %}
/ <a href="{{ device.rack.get_absolute_url }}">{{ device.rack }}</a>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="panel panel-danger">
<div class="panel-heading">
<strong>Power Feed <a href="{{ powerfeed.get_absolute_url }}">{{ powerfeed }}</a></strong><br />
<a href="{{ powerfeed.power_panel.get_absolute_url }}">{{ powerfeed.power_panel }}</a>
{% if powerfeed.rack %}
/ <a href="{{ powerfeed.rack.get_absolute_url }}">{{ powerfeed.rack }}</a>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,9 @@
{% load helpers %}
<div class="panel panel-default">
<div class="panel-body">
{{ termination|meta:"verbose_name"|bettertitle }} <a href="{{ termination.get_absolute_url }}">{{ termination }}</a>
{% if termination.type %}
<br/>{{ termination.get_type_display }}
{% endif %}
</div>
</div>

View File

@ -62,8 +62,8 @@ class ChoiceFieldInspector(FieldInspector):
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value) value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
if set([None] + choice_value) == {None, True, False}: if set([None] + choice_value) == {None, True, False}:
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be # DeviceType.subdevice_role and Device.face need to be differentiated since they each have
# differentiated since they each have subtly different values in their choice keys. # subtly different values in their choice keys.
# - subdevice_role and connection_status are booleans, although subdevice_role includes None # - subdevice_role and connection_status are booleans, although subdevice_role includes None
# - face is an integer set {0, 1} which is easily confused with {False, True} # - face is an integer set {0, 1} which is easily confused with {False, True}
schema_type = openapi.TYPE_STRING schema_type = openapi.TYPE_STRING

View File

@ -55,6 +55,11 @@ COMMAND="python3 netbox/manage.py migrate"
echo "Applying database migrations ($COMMAND)..." echo "Applying database migrations ($COMMAND)..."
eval $COMMAND || exit 1 eval $COMMAND || exit 1
# Trace any missing cable paths (not typically needed)
COMMAND="python3 netbox/manage.py trace_paths --no-input"
echo "Checking for missing cable paths ($COMMAND)..."
eval $COMMAND || exit 1
# Collect static files # Collect static files
COMMAND="python3 netbox/manage.py collectstatic --no-input" COMMAND="python3 netbox/manage.py collectstatic --no-input"
echo "Collecting static files ($COMMAND)..." echo "Collecting static files ($COMMAND)..."