mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #5212 from netbox-community/4900-cable-paths
#4900: New model for cable paths
This commit is contained in:
commit
6470613221
@ -34,6 +34,12 @@ http://netbox/api/dcim/sites/ \
|
||||
--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
|
||||
|
||||
* [#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
|
||||
|
||||
* 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.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.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.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.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`
|
||||
* extras.ExportTemplate: The `template_language` field has been removed
|
||||
* extras.Graph: This API endpoint has been removed (see #4349)
|
||||
|
@ -3,7 +3,7 @@ from rest_framework import serializers
|
||||
from circuits.choices import CircuitStatusChoices
|
||||
from circuits.models import Provider, Circuit, CircuitTermination, CircuitType
|
||||
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.serializers import TaggedObjectSerializer
|
||||
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')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer()
|
||||
@ -77,5 +77,6 @@ class CircuitTerminationSerializer(ConnectedEndpointSerializer):
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'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'
|
||||
]
|
||||
|
@ -3,6 +3,7 @@ from rest_framework.routers import APIRootView
|
||||
|
||||
from circuits import filters
|
||||
from circuits.models import Provider, CircuitTermination, CircuitType, Circuit
|
||||
from dcim.api.views import PathEndpointMixin
|
||||
from extras.api.views import CustomFieldModelViewSet
|
||||
from utilities.api import ModelViewSet
|
||||
from . import serializers
|
||||
@ -46,9 +47,7 @@ class CircuitTypeViewSet(ModelViewSet):
|
||||
|
||||
class CircuitViewSet(CustomFieldModelViewSet):
|
||||
queryset = Circuit.objects.prefetch_related(
|
||||
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related(
|
||||
'site', 'connected_endpoint__device'
|
||||
)),
|
||||
Prefetch('terminations', queryset=CircuitTermination.objects.prefetch_related('site')),
|
||||
'type', 'tenant', 'provider',
|
||||
).prefetch_related('tags')
|
||||
serializer_class = serializers.CircuitSerializer
|
||||
@ -59,9 +58,9 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
# Circuit Terminations
|
||||
#
|
||||
|
||||
class CircuitTerminationViewSet(ModelViewSet):
|
||||
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', 'connected_endpoint__device', 'cable'
|
||||
'circuit', 'site', '_path__destination', 'cable'
|
||||
)
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filters.CircuitTerminationFilterSet
|
||||
|
@ -1,6 +1,7 @@
|
||||
import django_filters
|
||||
from django.db.models import Q
|
||||
|
||||
from dcim.filters import CableTerminationFilterSet, PathEndpointFilterSet
|
||||
from dcim.models import Region, Site
|
||||
from extras.filters import CustomFieldFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
@ -144,7 +145,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldFilterSet, TenancyFilterSet, Cr
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilterSet(BaseFilterSet):
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
49
netbox/circuits/migrations/0021_cache_cable_peer.py
Normal file
49
netbox/circuits/migrations/0021_cache_cable_peer.py
Normal 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
|
||||
),
|
||||
]
|
26
netbox/circuits/migrations/0022_cablepath.py
Normal file
26
netbox/circuits/migrations/0022_cablepath.py
Normal 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',
|
||||
),
|
||||
]
|
@ -2,9 +2,8 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
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.utils import extras_features
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
@ -232,7 +231,7 @@ class Circuit(ChangeLoggedModel, CustomFieldModel):
|
||||
return self._get_termination('Z')
|
||||
|
||||
|
||||
class CircuitTermination(CableTermination):
|
||||
class CircuitTermination(PathEndpoint, CableTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
@ -248,18 +247,6 @@ class CircuitTermination(CableTermination):
|
||||
on_delete=models.PROTECT,
|
||||
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(
|
||||
verbose_name='Port speed (Kbps)'
|
||||
)
|
||||
|
@ -3,7 +3,7 @@ from django.test import TestCase
|
||||
from circuits.choices import *
|
||||
from circuits.filters import *
|
||||
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
|
||||
|
||||
|
||||
@ -286,6 +286,8 @@ class CircuitTerminationTestCase(TestCase):
|
||||
))
|
||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||
|
||||
Cable(termination_a=circuit_terminations[0], termination_b=circuit_terminations[1]).save()
|
||||
|
||||
def test_term_side(self):
|
||||
params = {'term_side': 'A'}
|
||||
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)
|
||||
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||
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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from dcim.views import CableCreateView, CableTraceView
|
||||
from dcim.views import CableCreateView, PathTraceView
|
||||
from extras.views import ObjectChangeLogView
|
||||
from . import views
|
||||
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>/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: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}),
|
||||
|
||||
]
|
||||
|
@ -131,7 +131,7 @@ class CircuitView(ObjectView):
|
||||
circuit = get_object_or_404(self.queryset, pk=pk)
|
||||
|
||||
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'site__region', 'connected_endpoint__device'
|
||||
'site__region'
|
||||
).filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).first()
|
||||
@ -139,7 +139,7 @@ class CircuitView(ObjectView):
|
||||
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
||||
|
||||
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
'site__region', 'connected_endpoint__device'
|
||||
'site__region'
|
||||
).filter(
|
||||
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).first()
|
||||
|
@ -1,8 +1,7 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.constants import CONNECTION_STATUS_CHOICES
|
||||
from dcim import models
|
||||
from utilities.api import ChoiceField, WritableNestedSerializer
|
||||
from utilities.api import WritableNestedSerializer
|
||||
|
||||
__all__ = [
|
||||
'NestedCableSerializer',
|
||||
@ -228,51 +227,46 @@ class NestedDeviceSerializer(WritableNestedSerializer):
|
||||
class NestedConsoleServerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsoleServerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedConsolePortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ConsolePort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedPowerOutletSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerOutlet
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedPowerPortSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.PowerPort
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedInterfaceSerializer(WritableNestedSerializer):
|
||||
device = NestedDeviceSerializer(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||
connection_status = ChoiceField(choices=CONNECTION_STATUS_CHOICES, read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.Interface
|
||||
fields = ['id', 'url', 'device', 'name', 'cable', 'connection_status']
|
||||
fields = ['id', 'url', 'device', 'name', 'cable']
|
||||
|
||||
|
||||
class NestedRearPortSerializer(WritableNestedSerializer):
|
||||
|
@ -7,12 +7,13 @@ from rest_framework.validators import UniqueTogetherValidator
|
||||
from dcim.choices import *
|
||||
from dcim.constants 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,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
from dcim.utils import decompile_path_node
|
||||
from extras.api.customfields import CustomFieldModelSerializer
|
||||
from extras.api.serializers import TaggedObjectSerializer
|
||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
||||
@ -27,17 +28,35 @@ from virtualization.api.nested_serializers import NestedClusterSerializer
|
||||
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):
|
||||
connected_endpoint_type = 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):
|
||||
if hasattr(obj, 'connected_endpoint') and obj.connected_endpoint is not None:
|
||||
return '{}.{}'.format(
|
||||
obj.connected_endpoint._meta.app_label,
|
||||
obj.connected_endpoint._meta.model_name
|
||||
)
|
||||
if obj._path is not None and obj._path.destination is not None:
|
||||
return f'{obj._path.destination._meta.app_label}.{obj._path.destination._meta.model_name}'
|
||||
return None
|
||||
|
||||
@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.
|
||||
"""
|
||||
if getattr(obj, 'connected_endpoint', None) is None:
|
||||
return None
|
||||
if obj._path is not None and obj._path.destination is not 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')
|
||||
context = {'request': self.context['request']}
|
||||
data = serializer(obj.connected_endpoint, context=context).data
|
||||
|
||||
return 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
|
||||
|
||||
|
||||
#
|
||||
@ -452,7 +474,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
||||
method = serializers.DictField()
|
||||
|
||||
|
||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -465,12 +487,12 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSeria
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -483,12 +505,12 @@ class ConsolePortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer)
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'connected_endpoint_type',
|
||||
'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -511,12 +533,13 @@ class PowerOutletSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer)
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description',
|
||||
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(
|
||||
@ -529,12 +552,13 @@ class PowerPortSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description',
|
||||
'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'tags',
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable',
|
||||
'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')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=InterfaceTypeChoices)
|
||||
@ -554,8 +578,9 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
model = Interface
|
||||
fields = [
|
||||
'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only',
|
||||
'description', 'connected_endpoint_type', 'connected_endpoint', 'connection_status', 'cable', 'mode',
|
||||
'untagged_vlan', 'tagged_vlans', 'tags', 'count_ipaddresses',
|
||||
'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type',
|
||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
||||
'count_ipaddresses',
|
||||
]
|
||||
|
||||
# TODO: This validation should be handled by Interface.clean()
|
||||
@ -579,7 +604,7 @@ class InterfaceSerializer(TaggedObjectSerializer, ConnectedEndpointSerializer):
|
||||
return super().validate(data)
|
||||
|
||||
|
||||
class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@ -587,7 +612,10 @@ class RearPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
@ -601,7 +629,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'name', 'label']
|
||||
|
||||
|
||||
class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
type = ChoiceField(choices=PortTypeChoices)
|
||||
@ -612,7 +640,7 @@ class FrontPortSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
|
||||
model = FrontPort
|
||||
fields = [
|
||||
'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
|
||||
#
|
||||
@ -715,17 +787,23 @@ class TracedCableSerializer(serializers.ModelSerializer):
|
||||
class InterfaceConnectionSerializer(ValidatedModelSerializer):
|
||||
interface_a = serializers.SerializerMethodField()
|
||||
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:
|
||||
model = Interface
|
||||
fields = ['interface_a', 'interface_b', 'connection_status']
|
||||
fields = ['interface_a', 'interface_b', 'connected_endpoint_reachable']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=NestedInterfaceSerializer)
|
||||
def get_interface_a(self, obj):
|
||||
context = {'request': self.context['request']}
|
||||
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
|
||||
@ -760,7 +838,12 @@ class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
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')
|
||||
power_panel = NestedPowerPanelSerializer()
|
||||
rack = NestedRackSerializer(
|
||||
@ -790,5 +873,7 @@ class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
|
||||
model = PowerFeed
|
||||
fields = [
|
||||
'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',
|
||||
]
|
||||
|
@ -17,7 +17,7 @@ from rest_framework.viewsets import GenericViewSet, ViewSet
|
||||
from circuits.models import Circuit
|
||||
from dcim import filters
|
||||
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,
|
||||
Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
@ -45,7 +45,7 @@ class DCIMRootView(APIRootView):
|
||||
|
||||
# Mixins
|
||||
|
||||
class CableTraceMixin(object):
|
||||
class PathEndpointMixin(object):
|
||||
|
||||
@action(detail=True, url_path='trace')
|
||||
def trace(self, request, pk):
|
||||
@ -57,7 +57,10 @@ class CableTraceMixin(object):
|
||||
# Initialize the path array
|
||||
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
|
||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
||||
@ -77,6 +80,20 @@ class CableTraceMixin(object):
|
||||
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
|
||||
#
|
||||
@ -469,49 +486,47 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
||||
# Device components
|
||||
#
|
||||
|
||||
class ConsolePortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
|
||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filters.ConsolePortFilterSet
|
||||
|
||||
|
||||
class ConsoleServerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
|
||||
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.ConsoleServerPortSerializer
|
||||
filterset_class = filters.ConsoleServerPortFilterSet
|
||||
|
||||
|
||||
class PowerPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related(
|
||||
'device', '_connected_poweroutlet__device', '_connected_powerfeed', 'cable', 'tags'
|
||||
)
|
||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filters.PowerPortFilterSet
|
||||
|
||||
|
||||
class PowerOutletViewSet(CableTraceMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', 'connected_endpoint__device', 'cable', 'tags')
|
||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
|
||||
serializer_class = serializers.PowerOutletSerializer
|
||||
filterset_class = filters.PowerOutletFilterSet
|
||||
|
||||
|
||||
class InterfaceViewSet(CableTraceMixin, ModelViewSet):
|
||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', '_connected_interface', '_connected_circuittermination', 'cable', 'ip_addresses', 'tags'
|
||||
).filter(
|
||||
device__isnull=False
|
||||
'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
|
||||
)
|
||||
serializer_class = serializers.InterfaceSerializer
|
||||
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')
|
||||
serializer_class = serializers.FrontPortSerializer
|
||||
filterset_class = filters.FrontPortFilterSet
|
||||
|
||||
|
||||
class RearPortViewSet(CableTraceMixin, ModelViewSet):
|
||||
class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
|
||||
queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
|
||||
serializer_class = serializers.RearPortSerializer
|
||||
filterset_class = filters.RearPortFilterSet
|
||||
@ -534,32 +549,26 @@ class InventoryItemViewSet(ModelViewSet):
|
||||
#
|
||||
|
||||
class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = ConsolePort.objects.prefetch_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
queryset = ConsolePort.objects.prefetch_related('device', '_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.ConsolePortSerializer
|
||||
filterset_class = filters.ConsoleConnectionFilterSet
|
||||
|
||||
|
||||
class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = PowerPort.objects.prefetch_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
_connected_poweroutlet__isnull=False
|
||||
queryset = PowerPort.objects.prefetch_related('device', '_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
serializer_class = serializers.PowerPortSerializer
|
||||
filterset_class = filters.PowerConnectionFilterSet
|
||||
|
||||
|
||||
class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', '_connected_interface__device'
|
||||
).filter(
|
||||
queryset = Interface.objects.prefetch_related('device', '_path').filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
_path__destination_id__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
serializer_class = serializers.InterfaceConnectionSerializer
|
||||
filterset_class = filters.InterfaceConnectionFilterSet
|
||||
@ -608,8 +617,10 @@ class PowerPanelViewSet(ModelViewSet):
|
||||
# Power feeds
|
||||
#
|
||||
|
||||
class PowerFeedViewSet(CustomFieldModelViewSet):
|
||||
queryset = PowerFeed.objects.prefetch_related('power_panel', 'rack', 'tags')
|
||||
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
||||
queryset = PowerFeed.objects.prefetch_related(
|
||||
'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
|
||||
)
|
||||
serializer_class = serializers.PowerFeedSerializer
|
||||
filterset_class = filters.PowerFeedFilterSet
|
||||
|
||||
@ -664,7 +675,7 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
device__name=peer_device_name,
|
||||
name=peer_interface_name
|
||||
)
|
||||
local_interface = peer_interface._connected_interface
|
||||
local_interface = peer_interface.connected_endpoint
|
||||
|
||||
if local_interface is None:
|
||||
return Response()
|
||||
|
@ -59,12 +59,6 @@ POWERFEED_MAX_UTILIZATION_DEFAULT = 80 # Percentage
|
||||
# Cabling and connections
|
||||
#
|
||||
|
||||
# Console/power/interface connection statuses
|
||||
CONNECTION_STATUS_CHOICES = [
|
||||
[False, 'Not Connected'],
|
||||
[True, 'Connected'],
|
||||
]
|
||||
|
||||
# Cable endpoint types
|
||||
CABLE_TERMINATION_MODELS = Q(
|
||||
Q(app_label='circuits', model__in=(
|
||||
|
@ -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
|
@ -1,9 +1,11 @@
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.db import models
|
||||
from netaddr import AddrFormatError, EUI, mac_unix_expanded
|
||||
|
||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
||||
from .lookups import PathContains
|
||||
|
||||
|
||||
class ASNField(models.BigIntegerField):
|
||||
@ -50,3 +52,15 @@ class MACAddressField(models.Field):
|
||||
if not value:
|
||||
return None
|
||||
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)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import Count
|
||||
|
||||
from extras.filters import CustomFieldFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet
|
||||
from tenancy.filters import TenancyFilterSet
|
||||
@ -23,6 +24,7 @@ from .models import (
|
||||
|
||||
__all__ = (
|
||||
'CableFilterSet',
|
||||
'CableTerminationFilterSet',
|
||||
'ConsoleConnectionFilterSet',
|
||||
'ConsolePortFilterSet',
|
||||
'ConsolePortTemplateFilterSet',
|
||||
@ -40,6 +42,7 @@ __all__ = (
|
||||
'InterfaceTemplateFilterSet',
|
||||
'InventoryItemFilterSet',
|
||||
'ManufacturerFilterSet',
|
||||
'PathEndpointFilterSet',
|
||||
'PlatformFilterSet',
|
||||
'PowerConnectionFilterSet',
|
||||
'PowerFeedFilterSet',
|
||||
@ -752,71 +755,76 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
||||
)
|
||||
|
||||
|
||||
class ConsolePortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
type = django_filters.MultipleChoiceFilter(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
class CableTerminationFilterSet(django_filters.FilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
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:
|
||||
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(
|
||||
choices=ConsolePortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
choices=PowerPortTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
choices=PowerOutletTypeChoices,
|
||||
null_value=None
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
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(
|
||||
method='search',
|
||||
label='Search',
|
||||
@ -833,11 +841,6 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
field_name='pk',
|
||||
label='Device (ID)',
|
||||
)
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
kind = django_filters.CharFilter(
|
||||
method='filter_kind',
|
||||
label='Kind of interface',
|
||||
@ -864,7 +867,7 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
try:
|
||||
@ -914,24 +917,14 @@ class InterfaceFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
}.get(value, queryset.none())
|
||||
|
||||
|
||||
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
class FrontPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = FrontPort
|
||||
fields = ['id', 'name', 'type', 'description']
|
||||
|
||||
|
||||
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet):
|
||||
cabled = django_filters.BooleanFilter(
|
||||
field_name='cable',
|
||||
lookup_expr='isnull',
|
||||
exclude=True
|
||||
)
|
||||
class RearPortFilterSet(BaseFilterSet, DeviceComponentFilterSet, CableTerminationFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = RearPort
|
||||
@ -1139,7 +1132,20 @@ class CableFilterSet(BaseFilterSet):
|
||||
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(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@ -1154,23 +1160,10 @@ class ConsoleConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ConsolePort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
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})
|
||||
)
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class PowerConnectionFilterSet(BaseFilterSet):
|
||||
class PowerConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@ -1185,23 +1178,10 @@ class PowerConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = ['name', 'connection_status']
|
||||
|
||||
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})
|
||||
)
|
||||
fields = ['name']
|
||||
|
||||
|
||||
class InterfaceConnectionFilterSet(BaseFilterSet):
|
||||
class InterfaceConnectionFilterSet(ConnectionFilterSet, BaseFilterSet):
|
||||
site = django_filters.CharFilter(
|
||||
method='filter_site',
|
||||
label='Site (slug)',
|
||||
@ -1216,23 +1196,7 @@ class InterfaceConnectionFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['connection_status']
|
||||
|
||||
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})
|
||||
)
|
||||
fields = []
|
||||
|
||||
|
||||
class PowerPanelFilterSet(BaseFilterSet):
|
||||
@ -1284,7 +1248,13 @@ class PowerPanelFilterSet(BaseFilterSet):
|
||||
return queryset.filter(qs_filter)
|
||||
|
||||
|
||||
class PowerFeedFilterSet(BaseFilterSet, CustomFieldFilterSet, CreatedUpdatedFilterSet):
|
||||
class PowerFeedFilterSet(
|
||||
BaseFilterSet,
|
||||
CableTerminationFilterSet,
|
||||
PathEndpointFilterSet,
|
||||
CustomFieldFilterSet,
|
||||
CreatedUpdatedFilterSet
|
||||
):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
label='Search',
|
||||
|
10
netbox/dcim/lookups.py
Normal file
10
netbox/dcim/lookups.py
Normal 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()
|
0
netbox/dcim/management/__init__.py
Normal file
0
netbox/dcim/management/__init__.py
Normal file
0
netbox/dcim/management/commands/__init__.py
Normal file
0
netbox/dcim/management/commands/__init__.py
Normal file
81
netbox/dcim/management/commands/trace_paths.py
Normal file
81
netbox/dcim/management/commands/trace_paths.py
Normal 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.'))
|
141
netbox/dcim/migrations/0120_cache_cable_peer.py
Normal file
141
netbox/dcim/migrations/0120_cache_cable_peer.py
Normal 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
|
||||
),
|
||||
]
|
107
netbox/dcim/migrations/0121_cablepath.py
Normal file
107
netbox/dcim/migrations/0121_cablepath.py
Normal 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',
|
||||
),
|
||||
]
|
@ -8,6 +8,7 @@ from .sites import *
|
||||
__all__ = (
|
||||
'BaseInterface',
|
||||
'Cable',
|
||||
'CablePath',
|
||||
'CableTermination',
|
||||
'ConsolePort',
|
||||
'ConsolePortTemplate',
|
||||
|
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
from django.db import models
|
||||
@ -11,8 +10,8 @@ from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices import *
|
||||
from dcim.constants import *
|
||||
from dcim.exceptions import CableTraceSplit
|
||||
from dcim.fields import MACAddressField
|
||||
from dcim.utils import path_node_to_object
|
||||
from extras.models import ObjectChange, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.fields import NaturalOrderingField
|
||||
@ -32,6 +31,7 @@ __all__ = (
|
||||
'FrontPort',
|
||||
'Interface',
|
||||
'InventoryItem',
|
||||
'PathEndpoint',
|
||||
'PowerOutlet',
|
||||
'PowerPort',
|
||||
'RearPort',
|
||||
@ -39,6 +39,9 @@ __all__ = (
|
||||
|
||||
|
||||
class ComponentModel(models.Model):
|
||||
"""
|
||||
An abstract model inherited by any model which has a parent Device.
|
||||
"""
|
||||
device = models.ForeignKey(
|
||||
to='dcim.Device',
|
||||
on_delete=models.CASCADE,
|
||||
@ -93,6 +96,14 @@ class ComponentModel(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(
|
||||
to='dcim.Cable',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -100,6 +111,21 @@ class CableTermination(models.Model):
|
||||
blank=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.
|
||||
_cabled_as_a = GenericRelation(
|
||||
@ -116,138 +142,57 @@ class CableTermination(models.Model):
|
||||
class Meta:
|
||||
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):
|
||||
if self.cable is None:
|
||||
return None
|
||||
if self._cabled_as_a.exists():
|
||||
return self.cable.termination_b
|
||||
if self._cabled_as_b.exists():
|
||||
return self.cable.termination_a
|
||||
return self._cable_peer
|
||||
|
||||
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 = []
|
||||
|
||||
# Get the far end of the last path segment
|
||||
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
|
||||
if not hasattr(self, '_connected_endpoint'):
|
||||
self._connected_endpoint = self._path.destination if self._path else None
|
||||
return self._connected_endpoint
|
||||
|
||||
|
||||
#
|
||||
@ -255,7 +200,7 @@ class CableTermination(models.Model):
|
||||
#
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -265,18 +210,6 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
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)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'description']
|
||||
@ -303,7 +236,7 @@ class ConsolePort(CableTermination, ComponentModel):
|
||||
#
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -313,11 +246,6 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
help_text='Physical port type'
|
||||
)
|
||||
connection_status = models.BooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'description']
|
||||
@ -344,7 +272,7 @@ class ConsoleServerPort(CableTermination, ComponentModel):
|
||||
#
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -366,25 +294,6 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
validators=[MinValueValidator(1)],
|
||||
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)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description']
|
||||
@ -407,51 +316,18 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
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):
|
||||
"""
|
||||
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
|
||||
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)
|
||||
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'),
|
||||
allocated_draw_total=Sum('allocated_draw'),
|
||||
)
|
||||
@ -463,10 +339,13 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
}
|
||||
|
||||
# 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:
|
||||
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'),
|
||||
allocated_draw_total=Sum('allocated_draw'),
|
||||
)
|
||||
@ -493,7 +372,7 @@ class PowerPort(CableTermination, ComponentModel):
|
||||
#
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -516,11 +395,6 @@ class PowerOutlet(CableTermination, ComponentModel):
|
||||
blank=True,
|
||||
help_text="Phase (for three-phase feeds)"
|
||||
)
|
||||
connection_status = models.BooleanField(
|
||||
choices=CONNECTION_STATUS_CHOICES,
|
||||
blank=True,
|
||||
null=True
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
csv_headers = ['device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description']
|
||||
@ -585,7 +459,7 @@ class BaseInterface(models.Model):
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -596,25 +470,6 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
|
||||
max_length=100,
|
||||
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(
|
||||
to='self',
|
||||
on_delete=models.SET_NULL,
|
||||
@ -730,42 +585,6 @@ class Interface(CableTermination, ComponentModel, BaseInterface):
|
||||
|
||||
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
|
||||
def parent(self):
|
||||
return self.device
|
||||
|
@ -7,13 +7,15 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
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.utils.safestring import mark_safe
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from dcim.choices 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.utils import extras_features
|
||||
from utilities.choices import ColorChoices
|
||||
@ -25,6 +27,7 @@ from .device_components import *
|
||||
|
||||
__all__ = (
|
||||
'Cable',
|
||||
'CablePath',
|
||||
'Device',
|
||||
'DeviceRole',
|
||||
'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
|
||||
self._pk = self.pk
|
||||
|
||||
# Cache the original status so we can check later if it's been changed
|
||||
self._orig_status = self.status
|
||||
|
||||
@classmethod
|
||||
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]
|
||||
|
||||
|
||||
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
|
||||
#
|
||||
|
@ -10,7 +10,7 @@ from extras.models import ChangeLoggedModel, CustomFieldModel, TaggedItem
|
||||
from extras.utils import extras_features
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.validators import ExclusionValidator
|
||||
from .device_components import CableTermination
|
||||
from .device_components import CableTermination, PathEndpoint
|
||||
|
||||
__all__ = (
|
||||
'PowerFeed',
|
||||
@ -73,7 +73,7 @@ class PowerPanel(ChangeLoggedModel, CustomFieldModel):
|
||||
|
||||
|
||||
@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.
|
||||
"""
|
||||
@ -88,18 +88,6 @@ class PowerFeed(ChangeLoggedModel, CableTermination, CustomFieldModel):
|
||||
blank=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(
|
||||
max_length=50
|
||||
)
|
||||
|
@ -3,6 +3,7 @@ from collections import OrderedDict
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||
@ -22,6 +23,7 @@ from utilities.fields import ColorField, NaturalOrderingField
|
||||
from utilities.querysets import RestrictedQuerySet
|
||||
from utilities.mptt import TreeManager
|
||||
from utilities.utils import array_to_string, serialize_object
|
||||
from .device_components import PowerOutlet, PowerPort
|
||||
from .devices import Device
|
||||
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.
|
||||
"""
|
||||
power_stats = PowerFeed.objects.filter(
|
||||
rack=self
|
||||
).annotate(
|
||||
allocated_draw_total=Sum('connected_endpoint__poweroutlets__connected_endpoint__allocated_draw'),
|
||||
).values(
|
||||
'allocated_draw_total',
|
||||
'available_power'
|
||||
)
|
||||
powerfeeds = PowerFeed.objects.filter(rack=self)
|
||||
available_power_total = sum(pf.available_power for pf in powerfeeds)
|
||||
if not available_power_total:
|
||||
return 0
|
||||
|
||||
if power_stats:
|
||||
allocated_draw_total = sum(x['allocated_draw_total'] or 0 for x in power_stats)
|
||||
available_power_total = sum(x['available_power'] for x in power_stats)
|
||||
return int(allocated_draw_total / available_power_total * 100) or 0
|
||||
return 0
|
||||
pf_powerports = PowerPort.objects.filter(
|
||||
_cable_peer_type=ContentType.objects.get_for_model(PowerFeed),
|
||||
_cable_peer_id__in=powerfeeds.values_list('id', flat=True)
|
||||
)
|
||||
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')
|
||||
|
@ -1,10 +1,35 @@
|
||||
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 .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)
|
||||
@ -32,7 +57,7 @@ def clear_virtualchassis_members(instance, **kwargs):
|
||||
|
||||
|
||||
@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
|
||||
"""
|
||||
@ -40,63 +65,61 @@ def update_connected_endpoints(instance, **kwargs):
|
||||
|
||||
# Cache the Cable on its two termination points
|
||||
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_peer = instance.termination_b
|
||||
instance.termination_a.save()
|
||||
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_peer = instance.termination_a
|
||||
instance.termination_b.save()
|
||||
|
||||
# Update any endpoints for this Cable.
|
||||
endpoints = instance.termination_a.get_path_endpoints() + instance.termination_b.get_path_endpoints()
|
||||
for endpoint in endpoints:
|
||||
path, split_ends, position_stack = endpoint.trace()
|
||||
# Determine overall path status (connected or planned)
|
||||
path_status = True
|
||||
for segment in path:
|
||||
if segment[1] is None or segment[1].status != CableStatusChoices.STATUS_CONNECTED:
|
||||
path_status = False
|
||||
break
|
||||
|
||||
endpoint_a = path[0][0]
|
||||
endpoint_b = path[-1][2] if not split_ends and not position_stack else None
|
||||
|
||||
# Patch panel ports are not connected endpoints, all other cable terminations are
|
||||
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()
|
||||
# Create/update cable paths
|
||||
if created:
|
||||
for termination in (instance.termination_a, instance.termination_b):
|
||||
if isinstance(termination, PathEndpoint):
|
||||
create_cablepath(termination)
|
||||
else:
|
||||
rebuild_paths(termination)
|
||||
elif instance.status != instance._orig_status:
|
||||
# We currently don't support modifying either termination of an existing Cable. (This
|
||||
# may change in the future.) However, we do need to capture status changes and update
|
||||
# any CablePaths accordingly.
|
||||
if instance.status != CableStatusChoices.STATUS_CONNECTED:
|
||||
CablePath.objects.filter(path__contains=instance).update(is_active=False)
|
||||
else:
|
||||
rebuild_paths(instance)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Cable)
|
||||
@receiver(post_delete, sender=Cable)
|
||||
def nullify_connected_endpoints(instance, **kwargs):
|
||||
"""
|
||||
When a Cable is deleted, check for and update its two connected endpoints
|
||||
"""
|
||||
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
|
||||
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_peer = None
|
||||
instance.termination_a.save()
|
||||
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_peer = None
|
||||
instance.termination_b.save()
|
||||
|
||||
# If this Cable was part of any complete end-to-end paths, tear them down.
|
||||
for endpoint in endpoints:
|
||||
logger.debug(f"Removing path information for {endpoint}")
|
||||
if hasattr(endpoint, 'connected_endpoint'):
|
||||
endpoint.connected_endpoint = None
|
||||
endpoint.connection_status = None
|
||||
endpoint.save()
|
||||
# Delete and retrace any dependent cable paths
|
||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
||||
path, destination, is_active = trace_path(cablepath.origin)
|
||||
if path:
|
||||
CablePath.objects.filter(pk=cablepath.pk).update(
|
||||
path=path,
|
||||
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()
|
||||
|
@ -67,8 +67,17 @@ INTERFACE_TAGGED_VLANS = """
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
CONNECTION_STATUS = """
|
||||
<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span>
|
||||
POWERFEED_CABLE = """
|
||||
<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):
|
||||
console_server = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('connected_endpoint__device'),
|
||||
args=[Accessor('connected_endpoint__device__pk')],
|
||||
console_server = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Console Server'
|
||||
)
|
||||
connected_endpoint = tables.Column(
|
||||
console_server_port = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Port'
|
||||
)
|
||||
@ -829,26 +840,28 @@ class ConsoleConnectionTable(BaseTable):
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
reachable = BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
add_prefetch = False
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('console_server', 'connected_endpoint', 'device', 'name', 'connection_status')
|
||||
fields = ('device', 'name', 'console_server', 'console_server_port', 'reachable')
|
||||
|
||||
|
||||
class PowerConnectionTable(BaseTable):
|
||||
pdu = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('connected_endpoint__device'),
|
||||
args=[Accessor('connected_endpoint__device__pk')],
|
||||
order_by='_connected_poweroutlet__device',
|
||||
pdu = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='PDU'
|
||||
)
|
||||
outlet = tables.Column(
|
||||
accessor=Accessor('_connected_poweroutlet'),
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Outlet'
|
||||
)
|
||||
@ -859,51 +872,51 @@ class PowerConnectionTable(BaseTable):
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
reachable = BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
add_prefetch = False
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('pdu', 'outlet', 'device', 'name', 'connection_status')
|
||||
fields = ('device', 'name', 'pdu', 'outlet', 'reachable')
|
||||
|
||||
|
||||
class InterfaceConnectionTable(BaseTable):
|
||||
device_a = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
device_a = tables.Column(
|
||||
accessor=Accessor('device'),
|
||||
args=[Accessor('device__pk')],
|
||||
linkify=True,
|
||||
verbose_name='Device A'
|
||||
)
|
||||
interface_a = tables.LinkColumn(
|
||||
viewname='dcim:interface',
|
||||
interface_a = tables.Column(
|
||||
accessor=Accessor('name'),
|
||||
args=[Accessor('pk')],
|
||||
linkify=True,
|
||||
verbose_name='Interface A'
|
||||
)
|
||||
device_b = tables.LinkColumn(
|
||||
viewname='dcim:device',
|
||||
accessor=Accessor('_connected_interface__device'),
|
||||
args=[Accessor('_connected_interface__device__pk')],
|
||||
device_b = tables.Column(
|
||||
accessor=Accessor('_path__destination__device'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Device B'
|
||||
)
|
||||
interface_b = tables.LinkColumn(
|
||||
viewname='dcim:interface',
|
||||
accessor=Accessor('_connected_interface'),
|
||||
args=[Accessor('_connected_interface__pk')],
|
||||
interface_b = tables.Column(
|
||||
accessor=Accessor('_path__destination'),
|
||||
orderable=False,
|
||||
linkify=True,
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
reachable = BooleanColumn(
|
||||
accessor=Accessor('_path__is_active'),
|
||||
verbose_name='Reachable'
|
||||
)
|
||||
|
||||
add_prefetch = False
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'device_a', 'interface_a', 'device_b', 'interface_b', 'connection_status',
|
||||
)
|
||||
fields = ('device_a', 'interface_a', 'device_b', 'interface_b', 'reachable')
|
||||
|
||||
|
||||
#
|
||||
@ -977,6 +990,15 @@ class PowerFeedTable(BaseTable):
|
||||
max_utilization = tables.TemplateColumn(
|
||||
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(
|
||||
verbose_name='Available power (VA)'
|
||||
)
|
||||
@ -988,8 +1010,9 @@ class PowerFeedTable(BaseTable):
|
||||
model = PowerFeed
|
||||
fields = (
|
||||
'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 = (
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
|
||||
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',
|
||||
'connection',
|
||||
)
|
||||
|
@ -977,7 +977,7 @@ class DeviceTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsolePort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1016,7 +1016,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = ConsoleServerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1055,7 +1055,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView
|
||||
|
||||
class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerPort
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1094,7 +1094,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase
|
||||
|
||||
class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = PowerOutlet
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
@ -1133,7 +1133,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa
|
||||
|
||||
class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase):
|
||||
model = Interface
|
||||
brief_fields = ['cable', 'connection_status', 'device', 'id', 'name', 'url']
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'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
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
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
|
||||
brief_fields = ['cable', 'device', 'id', 'name', 'url']
|
||||
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):
|
||||
|
||||
def setUp(self):
|
||||
|
901
netbox/dcim/tests/test_cablepaths.py
Normal file
901
netbox/dcim/tests/test_cablepaths.py
Normal 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)
|
@ -1514,10 +1514,11 @@ class ConsolePortTestCase(TestCase):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
# TODO: Fix boolean value
|
||||
def test_connection_status(self):
|
||||
params = {'connection_status': 'True'}
|
||||
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)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
@ -1609,10 +1610,11 @@ class ConsoleServerPortTestCase(TestCase):
|
||||
params = {'description': ['First', 'Second']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
# TODO: Fix boolean value
|
||||
def test_connection_status(self):
|
||||
params = {'connection_status': 'True'}
|
||||
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)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
@ -1712,10 +1714,11 @@ class PowerPortTestCase(TestCase):
|
||||
params = {'allocated_draw': [50, 100]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
# TODO: Fix boolean value
|
||||
def test_connection_status(self):
|
||||
params = {'connection_status': 'True'}
|
||||
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)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
@ -1812,10 +1815,11 @@ class PowerOutletTestCase(TestCase):
|
||||
params = {'feed_leg': PowerOutletFeedLegChoices.FEED_LEG_A}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
# TODO: Fix boolean value
|
||||
def test_connection_status(self):
|
||||
params = {'connection_status': 'True'}
|
||||
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)
|
||||
|
||||
def test_region(self):
|
||||
regions = Region.objects.all()[:2]
|
||||
@ -1900,10 +1904,11 @@ class InterfaceTestCase(TestCase):
|
||||
params = {'name': ['Interface 1', 'Interface 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
# TODO: Fix boolean value
|
||||
def test_connection_status(self):
|
||||
params = {'connection_status': 'True'}
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
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):
|
||||
params = {'enabled': 'true'}
|
||||
@ -2662,6 +2667,18 @@ class PowerFeedTestCase(TestCase):
|
||||
)
|
||||
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):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[: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]}
|
||||
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
|
||||
|
@ -398,9 +398,11 @@ class CableTestCase(TestCase):
|
||||
When a new Cable is created, it must be cached on either termination point.
|
||||
"""
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertEqual(self.cable.termination_a, interface1)
|
||||
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(interface2._cable_peer, interface1)
|
||||
|
||||
def test_cable_deletion(self):
|
||||
"""
|
||||
@ -412,8 +414,10 @@ class CableTestCase(TestCase):
|
||||
self.assertNotEqual(str(self.cable), '#None')
|
||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||
self.assertIsNone(interface1.cable)
|
||||
self.assertIsNone(interface1._cable_peer)
|
||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||
self.assertIsNone(interface2.cable)
|
||||
self.assertIsNone(interface2._cable_peer)
|
||||
|
||||
def test_cabletermination_deletion(self):
|
||||
"""
|
||||
@ -561,628 +565,3 @@ class CableTestCase(TestCase):
|
||||
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
|
||||
with self.assertRaises(ValidationError):
|
||||
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)
|
||||
|
@ -1714,11 +1714,6 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'max_utilization': 50,
|
||||
'comments': 'New comments',
|
||||
'tags': [t.pk for t in tags],
|
||||
|
||||
# Connection
|
||||
'cable': None,
|
||||
'connected_endpoint': None,
|
||||
'connection_status': None,
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
|
@ -207,7 +207,7 @@ urlpatterns = [
|
||||
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>/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('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>/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>/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('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>/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>/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('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>/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>/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('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>/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>/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('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>/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>/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('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>/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>/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('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>/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>/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: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
82
netbox/dcim/utils.py
Normal 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
|
@ -32,10 +32,10 @@ from . import filters, forms, tables
|
||||
from .choices import DeviceFaceChoices
|
||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||
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,
|
||||
InventoryItem, Manufacturer, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
|
||||
PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
InventoryItem, Manufacturer, PathEndpoint, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel,
|
||||
PowerPort, PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
|
||||
VirtualChassis,
|
||||
)
|
||||
|
||||
@ -1018,32 +1018,31 @@ class DeviceView(ObjectView):
|
||||
|
||||
# Console ports
|
||||
consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'connected_endpoint__device', 'cable',
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
|
||||
# Console server ports
|
||||
consoleserverports = ConsoleServerPort.objects.restrict(request.user, 'view').filter(
|
||||
device=device
|
||||
).prefetch_related(
|
||||
'connected_endpoint__device', 'cable',
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
|
||||
# Power ports
|
||||
powerports = PowerPort.objects.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
'_connected_poweroutlet__device', 'cable',
|
||||
'cable', '_path__destination',
|
||||
)
|
||||
|
||||
# Power outlets
|
||||
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 = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
||||
'cable__termination_a', 'cable__termination_b', 'tags'
|
||||
'lag', 'cable', '_path__destination', 'tags',
|
||||
)
|
||||
|
||||
# Front ports
|
||||
@ -1118,10 +1117,8 @@ class DeviceLLDPNeighborsView(ObjectView):
|
||||
def get(self, request, 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
|
||||
).prefetch_related(
|
||||
'_connected_interface__device'
|
||||
)
|
||||
|
||||
return render(request, 'dcim/device_lldp_neighbors.html', {
|
||||
@ -1479,8 +1476,6 @@ class InterfaceView(ObjectView):
|
||||
|
||||
return render(request, 'dcim/interface.html', {
|
||||
'instance': interface,
|
||||
'connected_interface': interface._connected_interface,
|
||||
'connected_circuittermination': interface._connected_circuittermination,
|
||||
'ipaddress_table': ipaddress_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']
|
||||
|
||||
@ -1970,19 +1965,30 @@ class CableTraceView(ObjectView):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
obj = get_object_or_404(self.queryset, pk=pk)
|
||||
path, split_ends, position_stack = obj.trace()
|
||||
total_length = sum(
|
||||
[entry[1]._abs_length for entry in path if entry[1] and entry[1]._abs_length]
|
||||
)
|
||||
related_paths = []
|
||||
|
||||
# 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', {
|
||||
'obj': obj,
|
||||
'trace': path,
|
||||
'split_ends': split_ends,
|
||||
'position_stack': position_stack,
|
||||
'total_length': total_length,
|
||||
'path': path,
|
||||
'related_paths': related_paths,
|
||||
'total_length': path.get_total_length(),
|
||||
})
|
||||
|
||||
|
||||
@ -2077,12 +2083,8 @@ class CableBulkDeleteView(BulkDeleteView):
|
||||
|
||||
class ConsoleConnectionsListView(ObjectListView):
|
||||
queryset = ConsolePort.objects.prefetch_related(
|
||||
'device', 'connected_endpoint__device'
|
||||
).filter(
|
||||
connected_endpoint__isnull=False
|
||||
).order_by(
|
||||
'cable', 'connected_endpoint__device__name', 'connected_endpoint__name'
|
||||
)
|
||||
'device', '_path__destination'
|
||||
).filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.ConsoleConnectionFilterSet
|
||||
filterset_form = forms.ConsoleConnectionFilterForm
|
||||
table = tables.ConsoleConnectionTable
|
||||
@ -2091,15 +2093,15 @@ class ConsoleConnectionsListView(ObjectListView):
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['console_server', 'port', 'device', 'console_port', 'connection_status'])
|
||||
','.join(['console_server', 'port', 'device', 'console_port', 'reachable'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.get_connection_status_display(),
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
@ -2108,12 +2110,8 @@ class ConsoleConnectionsListView(ObjectListView):
|
||||
|
||||
class PowerConnectionsListView(ObjectListView):
|
||||
queryset = PowerPort.objects.prefetch_related(
|
||||
'device', '_connected_poweroutlet__device'
|
||||
).filter(
|
||||
_connected_poweroutlet__isnull=False
|
||||
).order_by(
|
||||
'cable', '_connected_poweroutlet__device__name', '_connected_poweroutlet__name'
|
||||
)
|
||||
'device', '_path__destination'
|
||||
).filter(_path__isnull=False).order_by('device')
|
||||
filterset = filters.PowerConnectionFilterSet
|
||||
filterset_form = forms.PowerConnectionFilterForm
|
||||
table = tables.PowerConnectionTable
|
||||
@ -2122,15 +2120,15 @@ class PowerConnectionsListView(ObjectListView):
|
||||
def queryset_to_csv(self):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.join(['pdu', 'outlet', 'device', 'power_port', 'connection_status'])
|
||||
','.join(['pdu', 'outlet', 'device', 'power_port', 'reachable'])
|
||||
]
|
||||
for obj in self.queryset:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.get_connection_status_display(),
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
@ -2139,14 +2137,12 @@ class PowerConnectionsListView(ObjectListView):
|
||||
|
||||
class InterfaceConnectionsListView(ObjectListView):
|
||||
queryset = Interface.objects.prefetch_related(
|
||||
'device', 'cable', '_connected_interface__device'
|
||||
'device', '_path__destination'
|
||||
).filter(
|
||||
# Avoid duplicate connections by only selecting the lower PK in a connected pair
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
).order_by(
|
||||
'device'
|
||||
)
|
||||
_path__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
).order_by('device')
|
||||
filterset = filters.InterfaceConnectionFilterSet
|
||||
filterset_form = forms.InterfaceConnectionFilterForm
|
||||
table = tables.InterfaceConnectionTable
|
||||
@ -2156,16 +2152,16 @@ class InterfaceConnectionsListView(ObjectListView):
|
||||
csv_data = [
|
||||
# Headers
|
||||
','.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:
|
||||
csv = csv_format([
|
||||
obj.connected_endpoint.device.identifier if obj.connected_endpoint else None,
|
||||
obj.connected_endpoint.name if obj.connected_endpoint else None,
|
||||
obj._path.destination.device.identifier if obj._path.destination else None,
|
||||
obj._path.destination.name if obj._path.destination else None,
|
||||
obj.device.identifier,
|
||||
obj.name,
|
||||
obj.get_connection_status_display(),
|
||||
obj._path.is_active
|
||||
])
|
||||
csv_data.append(csv)
|
||||
|
||||
|
@ -190,15 +190,15 @@ class HomeView(View):
|
||||
|
||||
def get(self, request):
|
||||
|
||||
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').filter(
|
||||
connected_endpoint__isnull=False
|
||||
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
connected_powerports = PowerPort.objects.restrict(request.user, 'view').filter(
|
||||
_connected_poweroutlet__isnull=False
|
||||
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||
_path__destination_id__isnull=False
|
||||
)
|
||||
connected_interfaces = Interface.objects.restrict(request.user, 'view').filter(
|
||||
_connected_interface__isnull=False,
|
||||
pk__lt=F('_connected_interface')
|
||||
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||
_path__destination_id__isnull=False,
|
||||
pk__lt=F('_path__destination_id')
|
||||
)
|
||||
|
||||
# Report Results
|
||||
|
@ -7,101 +7,73 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-md-offset-1 text-center">
|
||||
<h4>Near End</h4>
|
||||
</div>
|
||||
<div class="col-md-3 text-center">
|
||||
{% if total_length %}<h5>Total length: {{ total_length|floatformat:"-2" }} Meters<h5>{% endif %}
|
||||
</div>
|
||||
<div class="col-md-4 text-center">
|
||||
<h4>Far End</h4>
|
||||
</div>
|
||||
</div>
|
||||
{% for near_end, cable, far_end in trace %}
|
||||
<div class="row">
|
||||
<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 }}"> </span>
|
||||
{% endif %}
|
||||
<div class="col-md-5 col-sm-12 text-center">
|
||||
{% for near_end, cable, far_end in path.origin.trace %}
|
||||
|
||||
{# Near end #}
|
||||
{% if near_end.device %}
|
||||
{% include 'dcim/trace/device.html' with device=near_end.device %}
|
||||
{% include 'dcim/trace/termination.html' with termination=near_end %}
|
||||
{% elif near_end.power_panel %}
|
||||
{% include 'dcim/trace/powerfeed.html' with powerfeed=near_end %}
|
||||
{% elif near_end.circuit %}
|
||||
{% include 'dcim/trace/circuit.html' with circuit=near_end.circuit %}
|
||||
{% include 'dcim/trace/termination.html' with termination=near_end %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{% if far_end %}
|
||||
{% include 'dcim/inc/cable_trace_end.html' with end=far_end %}
|
||||
|
||||
{# Cable #}
|
||||
{% if cable %}
|
||||
<div class="row">
|
||||
{% include 'dcim/trace/cable.html' %}
|
||||
</div>
|
||||
{% 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>
|
||||
<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>
|
||||
{% endblock %}
|
||||
|
@ -44,6 +44,15 @@
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<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 %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
@ -65,26 +74,17 @@
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</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 %}
|
||||
<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>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
|
@ -44,6 +44,15 @@
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<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 %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
@ -65,26 +74,17 @@
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</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 %}
|
||||
<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>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
|
@ -479,7 +479,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane active" id="interfaces">
|
||||
<div role="tabpanel" class="tab-pane" id="interfaces">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-default">
|
||||
@ -506,6 +506,7 @@
|
||||
<th>MTU</th>
|
||||
<th>Mode</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Cable Termination</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@ -566,7 +567,7 @@
|
||||
<th>Position</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th colspan="2">Cable Termination</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -623,7 +624,7 @@
|
||||
<th>Positions</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th colspan="2">Cable Termination</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -679,6 +680,7 @@
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Cable Termination</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@ -732,6 +734,7 @@
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th>Cable</th>
|
||||
<th colspan="2">Cable Termination</th>
|
||||
<th colspan="2">Connection</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
@ -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>
|
14
netbox/templates/dcim/inc/cabletermination.html
Normal file
14
netbox/templates/dcim/inc/cabletermination.html
Normal 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>
|
@ -24,31 +24,23 @@
|
||||
</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if cp.cable %}
|
||||
{% if cp.cable %}
|
||||
<td>
|
||||
<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">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
—
|
||||
{% 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>
|
||||
{% include 'dcim/inc/cabletermination.html' with termination=cp.get_cable_peer %}
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Connection #}
|
||||
{% include 'dcim/inc/endpoint_connection.html' with path=cp.path %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right noprint">
|
||||
{% if cp.cable %}
|
||||
|
@ -26,31 +26,23 @@
|
||||
</td>
|
||||
|
||||
{# Cable #}
|
||||
<td class="text-nowrap">
|
||||
{% if csp.cable %}
|
||||
{% if csp.cable %}
|
||||
<td>
|
||||
<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">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</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>
|
||||
{% include 'dcim/inc/cabletermination.html' with termination=csp.get_cable_peer %}
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Connection #}
|
||||
{% include 'dcim/inc/endpoint_connection.html' with path=csp.path %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right noprint">
|
||||
{% if csp.cable %}
|
||||
|
8
netbox/templates/dcim/inc/endpoint_connection.html
Normal file
8
netbox/templates/dcim/inc/endpoint_connection.html
Normal 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 %}
|
@ -24,7 +24,7 @@
|
||||
{# Description #}
|
||||
<td>{{ frontport.description|placeholder }}</td>
|
||||
|
||||
{# Cable/connection #}
|
||||
{# Cable #}
|
||||
{% if frontport.cable %}
|
||||
<td>
|
||||
<a href="{{ frontport.cable.get_absolute_url }}">{{ frontport.cable }}</a>
|
||||
@ -32,22 +32,7 @@
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
{% with far_end=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 %}
|
||||
{% include 'dcim/inc/cabletermination.html' with termination=frontport.get_cable_peer %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
|
@ -45,19 +45,19 @@
|
||||
<td>{{ iface.get_mode_display|default:"—" }}</td>
|
||||
|
||||
{# Cable #}
|
||||
<td class="text-nowrap">
|
||||
{% if iface.cable %}
|
||||
{% if iface.cable %}
|
||||
<td>
|
||||
<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 }}"> </span>
|
||||
{% endif %}
|
||||
<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>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
{% include 'dcim/inc/cabletermination.html' with termination=iface.get_cable_peer %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
{# Connection or type #}
|
||||
{% if iface.is_lag %}
|
||||
@ -75,65 +75,8 @@
|
||||
<td colspan="2" class="text-muted">Virtual interface</td>
|
||||
{% elif iface.is_wireless %}
|
||||
<td colspan="2" class="text-muted">Wireless interface</td>
|
||||
{% elif iface.connected_endpoint.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 %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
{% include 'dcim/inc/endpoint_connection.html' with path=iface.path %}
|
||||
{% endif %}
|
||||
|
||||
{# Buttons #}
|
||||
|
@ -37,39 +37,32 @@
|
||||
</td>
|
||||
|
||||
{# Cable #}
|
||||
<td class="text-nowrap">
|
||||
{% if po.cable %}
|
||||
{% if po.cable %}
|
||||
<td>
|
||||
<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">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</td>
|
||||
{% else %}
|
||||
<td><span class="text-muted">Not connected</span></td>
|
||||
{% endif %}
|
||||
|
||||
{# Connection #}
|
||||
{% if po.connected_endpoint %}
|
||||
{% with pp=po.connected_endpoint %}
|
||||
<td>
|
||||
<a href="{% url 'dcim:device' pk=pp.device.pk %}">{{ pp.device }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{{ pp }}
|
||||
</td>
|
||||
<td>
|
||||
{% if pp.allocated_draw %}
|
||||
{{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %}
|
||||
{% elif pp.maximum_draw %}
|
||||
{{ pp.maximum_draw }}W
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
{% with path=po.path %}
|
||||
{% include 'dcim/inc/endpoint_connection.html' %}
|
||||
<td>
|
||||
{% if paths|length == 1 %}
|
||||
{% with pp=paths.0.destination %}
|
||||
{% if pp.allocated_draw %}
|
||||
{{ pp.allocated_draw }}W{% if pp.maximum_draw %} ({{ pp.maximum_draw }}W max){% endif %}
|
||||
{% elif pp.maximum_draw %}
|
||||
{{ pp.maximum_draw }}W
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right noprint">
|
||||
|
@ -33,35 +33,20 @@
|
||||
</td>
|
||||
|
||||
{# Cable #}
|
||||
<td>
|
||||
{% if pp.cable %}
|
||||
{% if pp.cable %}
|
||||
<td>
|
||||
<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">
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
—
|
||||
{% 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>
|
||||
{% else %}
|
||||
<td colspan="2">
|
||||
<span class="text-muted">Not connected</span>
|
||||
</td>
|
||||
<td><span class="text-muted">Not connected</span></td>
|
||||
{% endif %}
|
||||
|
||||
{# Connection #}
|
||||
{% include 'dcim/inc/endpoint_connection.html' with path=pp.path %}
|
||||
|
||||
{# Actions #}
|
||||
<td class="text-right noprint">
|
||||
{% if pp.cable %}
|
||||
|
@ -23,7 +23,7 @@
|
||||
{# Description #}
|
||||
<td>{{ rearport.description|placeholder }}</td>
|
||||
|
||||
{# Cable/connection #}
|
||||
{# Cable #}
|
||||
{% if rearport.cable %}
|
||||
<td>
|
||||
<a href="{{ rearport.cable.get_absolute_url }}">{{ rearport.cable }}</a>
|
||||
@ -31,22 +31,7 @@
|
||||
<i class="fa fa-share-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
</td>
|
||||
{% with far_end=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 %}
|
||||
{% include 'dcim/inc/cabletermination.html' with termination=rearport.get_cable_peer %}
|
||||
{% else %}
|
||||
<td colspan="3">
|
||||
<span class="text-muted">Not connected</span>
|
||||
|
@ -77,61 +77,72 @@
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<table class="table table-hover panel-body attr-table">
|
||||
{% if connected_interface %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.device.get_absolute_url }}">{{ connected_interface.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ connected_interface.get_absolute_url }}">{{ connected_interface.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ connected_interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if connected_interface.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if connected_interface.lag%}
|
||||
<a href="{{ connected_interface.lag.get_absolute_url }}">{{ connected_interface.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ connected_interface.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ connected_interface.mtu|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MAC Address</td>
|
||||
<td>{{ connected_interface.mac_address|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>802.1Q Mode</td>
|
||||
<td>{{ connected_interface.get_mode_display }}</td>
|
||||
</tr>
|
||||
{% elif connected_circuittermination %}
|
||||
{% with ct=connected_circuittermination %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_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.device %}
|
||||
{% with iface=instance.connected_endpoint %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
<td>
|
||||
<a href="{{ iface.device.get_absolute_url }}">{{ iface.device }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
<a href="{{ iface.get_absolute_url }}">{{ iface.name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{{ iface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Enabled</td>
|
||||
<td>
|
||||
{% if iface.enabled %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% else %}
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LAG</td>
|
||||
<td>
|
||||
{% if iface.lag%}
|
||||
<a href="{{ iface.lag.get_absolute_url }}">{{ iface.lag }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Description</td>
|
||||
<td>{{ iface.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MTU</td>
|
||||
<td>{{ iface.mtu|placeholder }}</td>
|
||||
</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>
|
||||
<td>Provider</td>
|
||||
<td><a href="{{ ct.circuit.provider.get_absolute_url }}">{{ ct.circuit.provider }}</a></td>
|
||||
@ -147,21 +158,12 @@
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td>Cable</td>
|
||||
<td>Path Status</td>
|
||||
<td>
|
||||
<a href="{{ instance.cable.get_absolute_url }}">{{ instance.cable }}</a>
|
||||
<a href="{% url 'dcim:interface_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>
|
||||
{% if instance.path.is_active %}
|
||||
<span class="label label-success">Reachable</span>
|
||||
{% else %}
|
||||
<span class="label label-info">{{ instance.get_connection_status_display }}</span>
|
||||
<span class="label label-danger">Not Reachable</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -123,11 +123,6 @@
|
||||
</tr>
|
||||
</table>
|
||||
</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>Electrical Characteristics</strong>
|
||||
@ -155,6 +150,70 @@
|
||||
</tr>
|
||||
</table>
|
||||
</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-heading">
|
||||
<strong>Comments</strong>
|
||||
|
@ -52,6 +52,15 @@
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<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 %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
@ -73,26 +82,17 @@
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</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 %}
|
||||
<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>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
|
@ -58,7 +58,7 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<strong>Power Panel</strong>
|
||||
@ -82,17 +82,17 @@
|
||||
</tr>
|
||||
</table>
|
||||
</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 %}
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %}
|
||||
<div class="col-md-6">
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'panel_table.html' with table=powerfeed_table heading='Connected Feeds' %}
|
||||
{% plugin_full_width_page powerpanel %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,6 +52,15 @@
|
||||
</div>
|
||||
{% if instance.cable %}
|
||||
<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 %}
|
||||
<tr>
|
||||
<td>Device</td>
|
||||
@ -73,26 +82,17 @@
|
||||
<td>Description</td>
|
||||
<td>{{ instance.connected_endpoint.description|placeholder }}</td>
|
||||
</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 %}
|
||||
<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>
|
||||
{% else %}
|
||||
<div class="panel-body text-muted">
|
||||
|
@ -150,10 +150,14 @@
|
||||
<a href="{% url 'dcim:device_list' %}?rack_id={{ rack.id }}">{{ device_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Utilization</td>
|
||||
<td>{% utilization_graph rack.get_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Space Utilization</td>
|
||||
<td>{% utilization_graph rack.get_utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Power Utilization</td>
|
||||
<td>{% utilization_graph rack.get_power_utilization %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
|
15
netbox/templates/dcim/trace/cable.html
Normal file
15
netbox/templates/dcim/trace/cable.html
Normal 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 }}"> </span>
|
||||
{% endif %}
|
||||
<h4><i class="fa fa-arrow-down"></i></h4>
|
||||
</div>
|
6
netbox/templates/dcim/trace/circuit.html
Normal file
6
netbox/templates/dcim/trace/circuit.html
Normal 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>
|
9
netbox/templates/dcim/trace/device.html
Normal file
9
netbox/templates/dcim/trace/device.html
Normal 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>
|
9
netbox/templates/dcim/trace/powerfeed.html
Normal file
9
netbox/templates/dcim/trace/powerfeed.html
Normal 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>
|
9
netbox/templates/dcim/trace/termination.html
Normal file
9
netbox/templates/dcim/trace/termination.html
Normal 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>
|
@ -62,8 +62,8 @@ class ChoiceFieldInspector(FieldInspector):
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
|
||||
|
||||
if set([None] + choice_value) == {None, True, False}:
|
||||
# DeviceType.subdevice_role, Device.face and InterfaceConnection.connection_status all need to be
|
||||
# differentiated since they each have subtly different values in their choice keys.
|
||||
# DeviceType.subdevice_role and Device.face need to be differentiated since they each have
|
||||
# subtly different values in their choice keys.
|
||||
# - 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}
|
||||
schema_type = openapi.TYPE_STRING
|
||||
|
@ -55,6 +55,11 @@ COMMAND="python3 netbox/manage.py migrate"
|
||||
echo "Applying database migrations ($COMMAND)..."
|
||||
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
|
||||
COMMAND="python3 netbox/manage.py collectstatic --no-input"
|
||||
echo "Collecting static files ($COMMAND)..."
|
||||
|
Loading…
Reference in New Issue
Block a user