mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
Merge pull request #9615 from netbox-community/9102-cabling
Closes #9102: Add support for multi-termination cable ends
This commit is contained in:
commit
6c9f2734a2
@ -6,6 +6,28 @@
|
|||||||
|
|
||||||
* Device position and rack unit values are now reported as decimals (e.g. `1.0` or `1.5`) to support modeling half-height rack units.
|
* Device position and rack unit values are now reported as decimals (e.g. `1.0` or `1.5`) to support modeling half-height rack units.
|
||||||
* The `nat_outside` relation on the IP address model now returns a list of zero or more related IP addresses, rather than a single instance (or None).
|
* The `nat_outside` relation on the IP address model now returns a list of zero or more related IP addresses, rather than a single instance (or None).
|
||||||
|
* Several fields on the cable API serializers have been altered to support multiple-object cable terminations:
|
||||||
|
|
||||||
|
| Old Name | Old Type | New Name | New Type |
|
||||||
|
|----------------------|----------|-----------------------|----------|
|
||||||
|
| `termination_a_type` | string | `a_terminations_type` | string |
|
||||||
|
| `termination_b_type` | string | `b_terminations_type` | string |
|
||||||
|
| `termination_a_id` | integer | _Removed_ | - |
|
||||||
|
| `termination_b_id` | integer | _Removed_ | - |
|
||||||
|
| `termination_a` | object | `a_terminations` | list |
|
||||||
|
| `termination_b` | object | `b_terminations` | list |
|
||||||
|
|
||||||
|
* As with the cable model, several API fields on all objects to which cables can be connected (interfaces, circuit terminations, etc.) have been changed:
|
||||||
|
|
||||||
|
| Old Name | Old Type | New Name | New Type |
|
||||||
|
|--------------------------------|----------|---------------------------------|----------|
|
||||||
|
| `link_peer` | object | `link_peers` | list |
|
||||||
|
| `link_peer_type` | string | `link_peers_type` | string |
|
||||||
|
| `connected_endpoint` | object | `connected_endpoints` | list |
|
||||||
|
| `connected_endpoint_type` | string | `connected_endpoints_type` | string |
|
||||||
|
| `connected_endpoint_reachable` | boolean | `connected_endpoints_reachable` | boolean |
|
||||||
|
|
||||||
|
* The cable path serialization returned by the `/paths/` endpoint for pass-through ports has been simplified, and the following fields removed: `origin_type`, `origin`, `destination_type`, `destination`. (Additionally, `is_complete` has been added.)
|
||||||
|
|
||||||
### New Features
|
### New Features
|
||||||
|
|
||||||
@ -19,6 +41,8 @@
|
|||||||
|
|
||||||
#### Reference User in Permission Constraints ([#9074](https://github.com/netbox-community/netbox/issues/9074))
|
#### Reference User in Permission Constraints ([#9074](https://github.com/netbox-community/netbox/issues/9074))
|
||||||
|
|
||||||
|
#### Multi-object Cable Terminations ([#9102](https://github.com/netbox-community/netbox/issues/9102))
|
||||||
|
|
||||||
### Enhancements
|
### Enhancements
|
||||||
|
|
||||||
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
|
||||||
@ -55,23 +79,83 @@
|
|||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
* Added the following endpoints:
|
* Added the following endpoints:
|
||||||
|
* `/api/dcim/cable-terminations/`
|
||||||
* `/api/ipam/l2vpns/`
|
* `/api/ipam/l2vpns/`
|
||||||
* `/api/ipam/l2vpn-terminations/`
|
* `/api/ipam/l2vpn-terminations/`
|
||||||
* circuits.Circuit
|
* circuits.Circuit
|
||||||
* Added optional `termination_date` field
|
* Added optional `termination_date` field
|
||||||
* circuits.CircuitTermination
|
* circuits.CircuitTermination
|
||||||
* Added 'custom_fields' and 'tags' fields
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
|
* Added `custom_fields` and `tags` fields
|
||||||
|
* dcim.Cable
|
||||||
|
* `termination_a_type` has been renamed to `a_terminations_type`
|
||||||
|
* `termination_b_type` has been renamed to `b_terminations_type`
|
||||||
|
* `termination_a` renamed to `a_terminations` and now returns a list of objects
|
||||||
|
* `termination_b` renamed to `b_terminations` and now returns a list of objects
|
||||||
|
* `termination_a_id` has been removed
|
||||||
|
* `termination_b_id` has been removed
|
||||||
|
* dcim.ConsolePort
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
|
* dcim.ConsoleServerPort
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
* dcim.Device
|
* dcim.Device
|
||||||
* The `position` field has been changed from an integer to a decimal
|
* The `position` field has been changed from an integer to a decimal
|
||||||
* dcim.DeviceType
|
* dcim.DeviceType
|
||||||
* The `u_height` field has been changed from an integer to a decimal
|
* The `u_height` field has been changed from an integer to a decimal
|
||||||
|
* dcim.FrontPort
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
* dcim.Interface
|
* dcim.Interface
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
* Added the optional `poe_mode` and `poe_type` fields
|
* Added the optional `poe_mode` and `poe_type` fields
|
||||||
* Added the `l2vpn_termination` read-only field
|
* Added the `l2vpn_termination` read-only field
|
||||||
* dcim.Location
|
* dcim.Location
|
||||||
* Added required `status` field (default value: `active`)
|
* Added required `status` field (default value: `active`)
|
||||||
|
* dcim.PowerOutlet
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
|
* dcim.PowerFeed
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
|
* dcim.PowerPort
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
* dcim.Rack
|
* dcim.Rack
|
||||||
* The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
|
* The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
|
||||||
|
* dcim.RearPort
|
||||||
|
* `link_peer` has been renamed to `link_peers` and now returns a list of objects
|
||||||
|
* `link_peer_type` has been renamed to `link_peers_type`
|
||||||
|
* `connected_endpoint` has been renamed to `connected_endpoints` and now returns a list of objects
|
||||||
|
* `connected_endpoint_type` has been renamed to `connected_endpoints_type`
|
||||||
|
* `connected_endpoint_reachable` has been renamed to `connected_endpoints_reachable`
|
||||||
* extras.ConfigContext
|
* extras.ConfigContext
|
||||||
* Added the `locations` many-to-many field to track the assignment of ConfigContexts to Locations
|
* Added the `locations` many-to-many field to track the assignment of ConfigContexts to Locations
|
||||||
* extras.CustomField
|
* extras.CustomField
|
||||||
|
@ -3,11 +3,11 @@ from rest_framework import serializers
|
|||||||
from circuits.choices import CircuitStatusChoices
|
from circuits.choices import CircuitStatusChoices
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import LinkTerminationSerializer
|
from dcim.api.serializers import CabledObjectSerializer
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from ipam.api.nested_serializers import NestedASNSerializer
|
from ipam.api.nested_serializers import NestedASNSerializer
|
||||||
from netbox.api import ChoiceField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
|
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -98,17 +98,16 @@ class CircuitSerializer(NetBoxModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
circuit = NestedCircuitSerializer()
|
circuit = NestedCircuitSerializer()
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed',
|
||||||
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
'_occupied', 'tags', 'custom_fields', 'created', 'last_updated',
|
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
@ -58,7 +58,7 @@ class CircuitViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = CircuitTermination.objects.prefetch_related(
|
queryset = CircuitTermination.objects.prefetch_related(
|
||||||
'circuit', 'site', 'provider_network', 'cable'
|
'circuit', 'site', 'provider_network', 'cable__terminations'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.CircuitTerminationSerializer
|
serializer_class = serializers.CircuitTerminationSerializer
|
||||||
filterset_class = filtersets.CircuitTerminationFilterSet
|
filterset_class = filtersets.CircuitTerminationFilterSet
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.filtersets import CableTerminationFilterSet
|
from dcim.filtersets import CabledObjectFilterSet
|
||||||
from dcim.models import Region, Site, SiteGroup
|
from dcim.models import Region, Site, SiteGroup
|
||||||
from ipam.models import ASN
|
from ipam.models import ASN
|
||||||
from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
|
from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
|
||||||
@ -198,7 +198,7 @@ class CircuitFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet):
|
class CircuitTerminationFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
@ -224,7 +224,7 @@ class CircuitTerminationFilterSet(NetBoxModelFilterSet, CableTerminationFilterSe
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description']
|
fields = ['id', 'term_side', 'port_speed', 'upstream_speed', 'xconnect_id', 'description', 'cable_end']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from circuits import filtersets, models
|
from circuits import filtersets, models
|
||||||
|
from dcim.graphql.mixins import CabledObjectMixin
|
||||||
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
|
||||||
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, ObjectType):
|
class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.CircuitTermination
|
model = models.CircuitTermination
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 4.0.5 on 2022-06-22 18:51
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('circuits', '0035_provider_asns'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='circuit',
|
|
||||||
name='termination_date',
|
|
||||||
field=models.DateField(blank=True, null=True),
|
|
||||||
),
|
|
||||||
]
|
|
@ -6,11 +6,15 @@ import taggit.managers
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('extras', '0076_configcontext_locations'),
|
('circuits', '0035_provider_asns'),
|
||||||
('circuits', '0036_circuit_termination_date'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuit',
|
||||||
|
name='termination_date',
|
||||||
|
field=models.DateField(blank=True, null=True),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='circuittermination',
|
model_name='circuittermination',
|
||||||
name='custom_field_data',
|
name='custom_field_data',
|
16
netbox/circuits/migrations/0037_new_cabling_models.py
Normal file
16
netbox/circuits/migrations/0037_new_cabling_models.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0036_circuit_termination_date_tags_custom_fields'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
]
|
20
netbox/circuits/migrations/0038_cabling_cleanup.py
Normal file
20
netbox/circuits/migrations/0038_cabling_cleanup.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0037_new_cabling_models'),
|
||||||
|
('dcim', '0160_populate_cable_ends'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
]
|
@ -4,7 +4,7 @@ from django.db import models
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from circuits.choices import *
|
from circuits.choices import *
|
||||||
from dcim.models import LinkTermination
|
from dcim.models import CabledObjectModel
|
||||||
from netbox.models import (
|
from netbox.models import (
|
||||||
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin,
|
ChangeLoggedModel, CustomFieldsMixin, CustomLinksMixin, OrganizationalModel, NetBoxModel, TagsMixin,
|
||||||
)
|
)
|
||||||
@ -149,7 +149,7 @@ class CircuitTermination(
|
|||||||
TagsMixin,
|
TagsMixin,
|
||||||
WebhooksMixin,
|
WebhooksMixin,
|
||||||
ChangeLoggedModel,
|
ChangeLoggedModel,
|
||||||
LinkTermination
|
CabledObjectModel
|
||||||
):
|
):
|
||||||
circuit = models.ForeignKey(
|
circuit = models.ForeignKey(
|
||||||
to='circuits.Circuit',
|
to='circuits.Circuit',
|
||||||
|
@ -24,4 +24,4 @@ def rebuild_cablepaths(instance, raw=False, **kwargs):
|
|||||||
if not raw:
|
if not raw:
|
||||||
peer_termination = instance.get_peer_termination()
|
peer_termination = instance.get_peer_termination()
|
||||||
if peer_termination:
|
if peer_termination:
|
||||||
rebuild_paths(peer_termination)
|
rebuild_paths([peer_termination])
|
||||||
|
@ -360,7 +360,7 @@ class CircuitTerminationTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
))
|
))
|
||||||
CircuitTermination.objects.bulk_create(circuit_terminations)
|
CircuitTermination.objects.bulk_create(circuit_terminations)
|
||||||
|
|
||||||
Cable(termination_a=circuit_terminations[0], termination_b=circuit_terminations[1]).save()
|
Cable(a_terminations=[circuit_terminations[0]], b_terminations=[circuit_terminations[1]]).save()
|
||||||
|
|
||||||
def test_term_side(self):
|
def test_term_side(self):
|
||||||
params = {'term_side': 'A'}
|
params = {'term_side': 'A'}
|
||||||
|
@ -246,7 +246,7 @@ class CircuitTerminationTestCase(
|
|||||||
device=device,
|
device=device,
|
||||||
name='Interface 1'
|
name='Interface 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=circuittermination, termination_b=interface).save()
|
Cable(a_terminations=[circuittermination], b_terminations=[interface]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
response = self.client.get(reverse('circuits:circuittermination_trace', kwargs={'pk': circuittermination.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from dcim.views import CableCreateView, PathTraceView
|
from dcim.views import PathTraceView
|
||||||
from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
|
from netbox.views.generic import ObjectChangeLogView, ObjectJournalView
|
||||||
from . import views
|
from . import views
|
||||||
from .models import *
|
from .models import *
|
||||||
@ -60,7 +60,6 @@ urlpatterns = [
|
|||||||
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
path('circuit-terminations/add/', views.CircuitTerminationEditView.as_view(), name='circuittermination_add'),
|
||||||
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
path('circuit-terminations/<int:pk>/edit/', views.CircuitTerminationEditView.as_view(), name='circuittermination_edit'),
|
||||||
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
path('circuit-terminations/<int:pk>/delete/', views.CircuitTerminationDeleteView.as_view(), name='circuittermination_delete'),
|
||||||
path('circuit-terminations/<int:termination_a_id>/connect/<str:termination_b_type>/', CableCreateView.as_view(), name='circuittermination_connect', kwargs={'termination_a_type': CircuitTermination}),
|
|
||||||
path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
path('circuit-terminations/<int:pk>/trace/', PathTraceView.as_view(), name='circuittermination_trace', kwargs={'model': CircuitTermination}),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -28,58 +28,68 @@ from wireless.models import WirelessLAN
|
|||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
|
|
||||||
class LinkTerminationSerializer(serializers.ModelSerializer):
|
class CabledObjectSerializer(serializers.ModelSerializer):
|
||||||
link_peer_type = serializers.SerializerMethodField(read_only=True)
|
cable = NestedCableSerializer(read_only=True)
|
||||||
link_peer = serializers.SerializerMethodField(read_only=True)
|
cable_end = serializers.CharField(read_only=True)
|
||||||
|
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
||||||
|
link_peers = serializers.SerializerMethodField(read_only=True)
|
||||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def get_link_peer_type(self, obj):
|
def get_link_peers_type(self, obj):
|
||||||
if obj._link_peer is not None:
|
"""
|
||||||
return f'{obj._link_peer._meta.app_label}.{obj._link_peer._meta.model_name}'
|
Return the type of the peer link terminations, or None.
|
||||||
|
"""
|
||||||
|
if not obj.cable:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if obj.link_peers:
|
||||||
|
return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}'
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||||
def get_link_peer(self, obj):
|
def get_link_peers(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the link termination model.
|
Return the appropriate serializer for the link termination model.
|
||||||
"""
|
"""
|
||||||
if obj._link_peer is not None:
|
if not obj.link_peers:
|
||||||
serializer = get_serializer_for_model(obj._link_peer, prefix='Nested')
|
return []
|
||||||
context = {'request': self.context['request']}
|
|
||||||
return serializer(obj._link_peer, context=context).data
|
# Return serialized peer termination objects
|
||||||
return None
|
serializer = get_serializer_for_model(obj.link_peers[0], prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.link_peers, context=context, many=True).data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||||
def get__occupied(self, obj):
|
def get__occupied(self, obj):
|
||||||
return obj._occupied
|
return obj._occupied
|
||||||
|
|
||||||
|
|
||||||
class ConnectedEndpointSerializer(serializers.ModelSerializer):
|
class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||||
connected_endpoint_type = serializers.SerializerMethodField(read_only=True)
|
"""
|
||||||
connected_endpoint = serializers.SerializerMethodField(read_only=True)
|
Legacy serializer for pre-v3.3 connections
|
||||||
connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True)
|
"""
|
||||||
|
connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
||||||
|
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
def get_connected_endpoint_type(self, obj):
|
def get_connected_endpoints_type(self, obj):
|
||||||
if obj._path is not None and obj._path.destination is not None:
|
if endpoints := obj.connected_endpoints:
|
||||||
return f'{obj._path.destination._meta.app_label}.{obj._path.destination._meta.model_name}'
|
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||||
return None
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||||
def get_connected_endpoint(self, obj):
|
def get_connected_endpoints(self, obj):
|
||||||
"""
|
"""
|
||||||
Return the appropriate serializer for the type of connected object.
|
Return the appropriate serializer for the type of connected object.
|
||||||
"""
|
"""
|
||||||
if obj._path is not None and obj._path.destination is not None:
|
if endpoints := obj.connected_endpoints:
|
||||||
serializer = get_serializer_for_model(obj._path.destination, prefix='Nested')
|
serializer = get_serializer_for_model(endpoints[0], prefix='Nested')
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj._path.destination, context=context).data
|
return serializer(endpoints, many=True, context=context).data
|
||||||
return None
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||||
def get_connected_endpoint_reachable(self, obj):
|
def get_connected_endpoints_reachable(self, obj):
|
||||||
if obj._path is not None:
|
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||||
return obj._path.is_active
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -684,7 +694,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -701,18 +711,18 @@ class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializ
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -729,18 +739,18 @@ class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
|
||||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint', 'connected_endpoint_type',
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
|
||||||
'connected_endpoint_reachable', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
|
||||||
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -761,21 +771,18 @@ class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Co
|
|||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(
|
|
||||||
read_only=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
|
||||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
|
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
'last_updated', '_occupied',
|
'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -787,19 +794,18 @@ class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
|||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
|
||||||
'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'connected_endpoint',
|
'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
|
||||||
'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'created',
|
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
|
||||||
'last_updated', '_occupied',
|
'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -825,7 +831,6 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
|||||||
)
|
)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
||||||
wireless_lans = SerializedPKRelatedField(
|
wireless_lans = SerializedPKRelatedField(
|
||||||
queryset=WirelessLAN.objects.all(),
|
queryset=WirelessLAN.objects.all(),
|
||||||
@ -842,9 +847,10 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
|||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', 'lag',
|
||||||
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', 'rf_channel',
|
||||||
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan',
|
||||||
'tagged_vlans', 'mark_connected', 'cable', 'wireless_link', 'link_peer', 'link_peer_type', 'wireless_lans',
|
'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', 'link_peers_type',
|
||||||
'vrf', 'l2vpn_termination', 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags',
|
'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', 'connected_endpoints_type',
|
||||||
'custom_fields', 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
|
'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', 'last_updated', 'count_ipaddresses',
|
||||||
|
'count_fhrp_groups', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
@ -861,7 +867,7 @@ class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
|||||||
return super().validate(data)
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -869,13 +875,12 @@ class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
|||||||
allow_null=True
|
allow_null=True
|
||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
|
||||||
'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags', 'custom_fields', 'created',
|
'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
|
||||||
'last_updated', '_occupied',
|
'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -891,7 +896,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'label']
|
fields = ['id', 'url', 'display', 'name', 'label']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -900,14 +905,13 @@ class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
|||||||
)
|
)
|
||||||
type = ChoiceField(choices=PortTypeChoices)
|
type = ChoiceField(choices=PortTypeChoices)
|
||||||
rear_port = FrontPortRearPortSerializer()
|
rear_port = FrontPortRearPortSerializer()
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
|
||||||
'rear_port_position', 'description', 'mark_connected', 'cable', 'link_peer', 'link_peer_type', 'tags',
|
'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
'custom_fields', 'created', 'last_updated', '_occupied',
|
'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -990,14 +994,10 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class CableSerializer(NetBoxModelSerializer):
|
class CableSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
termination_a_type = ContentTypeField(
|
a_terminations_type = serializers.SerializerMethodField(read_only=True)
|
||||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
b_terminations_type = serializers.SerializerMethodField(read_only=True)
|
||||||
)
|
a_terminations = serializers.SerializerMethodField(read_only=True)
|
||||||
termination_b_type = ContentTypeField(
|
b_terminations = serializers.SerializerMethodField(read_only=True)
|
||||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
|
||||||
)
|
|
||||||
termination_a = serializers.SerializerMethodField(read_only=True)
|
|
||||||
termination_b = serializers.SerializerMethodField(read_only=True)
|
|
||||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||||
@ -1005,33 +1005,46 @@ class CableSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type',
|
'id', 'url', 'display', 'type', 'a_terminations_type', 'a_terminations', 'b_terminations_type',
|
||||||
'termination_b_id', 'termination_b', 'type', 'status', 'tenant', 'label', 'color', 'length', 'length_unit',
|
'b_terminations', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'custom_fields',
|
||||||
'tags', 'custom_fields', 'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_termination(self, obj, side):
|
def _get_terminations_type(self, obj, side):
|
||||||
"""
|
assert side in CableEndChoices.values()
|
||||||
Serialize a nested representation of a termination.
|
terms = getattr(obj, f'get_{side.lower()}_terminations')()
|
||||||
"""
|
if terms:
|
||||||
if side.lower() not in ['a', 'b']:
|
ct = ContentType.objects.get_for_model(terms[0])
|
||||||
raise ValueError("Termination side must be either A or B.")
|
return f"{ct.app_label}.{ct.model}"
|
||||||
termination = getattr(obj, 'termination_{}'.format(side.lower()))
|
|
||||||
if termination is None:
|
def _get_terminations(self, obj, side):
|
||||||
return None
|
assert side in CableEndChoices.values()
|
||||||
serializer = get_serializer_for_model(termination, prefix='Nested')
|
terms = getattr(obj, f'get_{side.lower()}_terminations')()
|
||||||
|
if not terms:
|
||||||
|
return []
|
||||||
|
|
||||||
|
termination_type = ContentType.objects.get_for_model(terms[0])
|
||||||
|
serializer = get_serializer_for_model(termination_type.model_class(), prefix='Nested')
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
data = serializer(termination, context=context).data
|
data = serializer(terms, context=context, many=True).data
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
||||||
def get_termination_a(self, obj):
|
def get_a_terminations_type(self, obj):
|
||||||
return self._get_termination(obj, 'a')
|
return self._get_terminations_type(obj, CableEndChoices.SIDE_A)
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
||||||
|
def get_b_terminations_type(self, obj):
|
||||||
|
return self._get_terminations_type(obj, CableEndChoices.SIDE_B)
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
def get_termination_b(self, obj):
|
def get_a_terminations(self, obj):
|
||||||
return self._get_termination(obj, 'b')
|
return self._get_terminations(obj, CableEndChoices.SIDE_A)
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_b_terminations(self, obj):
|
||||||
|
return self._get_terminations(obj, CableEndChoices.SIDE_B)
|
||||||
|
|
||||||
|
|
||||||
class TracedCableSerializer(serializers.ModelSerializer):
|
class TracedCableSerializer(serializers.ModelSerializer):
|
||||||
@ -1047,46 +1060,40 @@ class TracedCableSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CableTerminationSerializer(NetBoxModelSerializer):
|
||||||
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail')
|
||||||
|
termination_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||||
|
)
|
||||||
|
termination = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CableTermination
|
||||||
|
fields = [
|
||||||
|
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
||||||
|
]
|
||||||
|
|
||||||
|
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
||||||
|
def get_termination(self, obj):
|
||||||
|
serializer = get_serializer_for_model(obj.termination, prefix='Nested')
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.termination, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class CablePathSerializer(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)
|
path = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CablePath
|
model = CablePath
|
||||||
fields = [
|
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
|
||||||
'id', 'origin_type', 'origin', 'destination_type', 'destination', 'path', 'is_active', 'is_split',
|
|
||||||
]
|
|
||||||
|
|
||||||
@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)
|
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||||
def get_path(self, obj):
|
def get_path(self, obj):
|
||||||
ret = []
|
ret = []
|
||||||
for node in obj.get_path():
|
for nodes in obj.path_objects:
|
||||||
serializer = get_serializer_for_model(node, prefix='Nested')
|
serializer = get_serializer_for_model(nodes[0], prefix='Nested')
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
ret.append(serializer(node, context=context).data)
|
ret.append(serializer(nodes, context=context, many=True).data)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@ -1129,7 +1136,7 @@ class PowerPanelSerializer(NetBoxModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
power_panel = NestedPowerPanelSerializer()
|
power_panel = NestedPowerPanelSerializer()
|
||||||
rack = NestedRackSerializer(
|
rack = NestedRackSerializer(
|
||||||
@ -1153,13 +1160,12 @@ class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, Conn
|
|||||||
choices=PowerFeedPhaseChoices,
|
choices=PowerFeedPhaseChoices,
|
||||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
||||||
)
|
)
|
||||||
cable = NestedCableSerializer(read_only=True)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
|
||||||
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'link_peer', 'link_peer_type',
|
'amperage', 'max_utilization', 'comments', 'mark_connected', 'cable', 'cable_end', 'link_peers',
|
||||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields',
|
'link_peers_type', 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable',
|
||||||
'created', 'last_updated', '_occupied',
|
'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
|
||||||
]
|
]
|
||||||
|
@ -56,6 +56,7 @@ router.register('inventory-item-roles', views.InventoryItemRoleViewSet)
|
|||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
router.register('cables', views.CableViewSet)
|
router.register('cables', views.CableViewSet)
|
||||||
|
router.register('cable-terminations', views.CableTerminationViewSet)
|
||||||
|
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
router.register('virtual-chassis', views.VirtualChassisViewSet)
|
router.register('virtual-chassis', views.VirtualChassisViewSet)
|
||||||
|
@ -13,7 +13,9 @@ from rest_framework.viewsets import ViewSet
|
|||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim import filtersets
|
from dcim import filtersets
|
||||||
|
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
|
from dcim.svg import CableTraceSVG
|
||||||
from extras.api.views import ConfigContextQuerySetMixin
|
from extras.api.views import ConfigContextQuerySetMixin
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
@ -51,37 +53,30 @@ class PathEndpointMixin(object):
|
|||||||
# Initialize the path array
|
# Initialize the path array
|
||||||
path = []
|
path = []
|
||||||
|
|
||||||
|
# Render SVG image if requested
|
||||||
if request.GET.get('render', None) == 'svg':
|
if request.GET.get('render', None) == 'svg':
|
||||||
# Render SVG
|
|
||||||
try:
|
try:
|
||||||
width = min(int(request.GET.get('width')), 1600)
|
width = int(request.GET.get('width', CABLE_TRACE_SVG_DEFAULT_WIDTH))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
width = None
|
width = CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
drawing = obj.get_trace_svg(
|
drawing = CableTraceSVG(obj, base_url=request.build_absolute_uri('/'), width=width)
|
||||||
base_url=request.build_absolute_uri('/'),
|
return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml')
|
||||||
width=width
|
|
||||||
)
|
|
||||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
|
||||||
|
|
||||||
|
# Serialize path objects, iterating over each three-tuple in the path
|
||||||
for near_end, cable, far_end in obj.trace():
|
for near_end, cable, far_end in obj.trace():
|
||||||
if near_end is None:
|
if near_end is not None:
|
||||||
# Split paths
|
serializer_a = get_serializer_for_model(near_end[0], prefix='Nested')
|
||||||
|
near_end = serializer_a(near_end, many=True, context={'request': request}).data
|
||||||
|
else:
|
||||||
|
# Path is split; stop here
|
||||||
break
|
break
|
||||||
|
|
||||||
# Serialize each object
|
|
||||||
serializer_a = get_serializer_for_model(near_end, prefix='Nested')
|
|
||||||
x = serializer_a(near_end, context={'request': request}).data
|
|
||||||
if cable is not None:
|
if cable is not None:
|
||||||
y = serializers.TracedCableSerializer(cable, context={'request': request}).data
|
cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
|
||||||
else:
|
|
||||||
y = None
|
|
||||||
if far_end is not None:
|
if far_end is not None:
|
||||||
serializer_b = get_serializer_for_model(far_end, prefix='Nested')
|
serializer_b = get_serializer_for_model(far_end[0], prefix='Nested')
|
||||||
z = serializer_b(far_end, context={'request': request}).data
|
far_end = serializer_b(far_end, many=True, context={'request': request}).data
|
||||||
else:
|
|
||||||
z = None
|
|
||||||
|
|
||||||
path.append((x, y, z))
|
path.append((near_end, cable, far_end))
|
||||||
|
|
||||||
return Response(path)
|
return Response(path)
|
||||||
|
|
||||||
@ -94,7 +89,7 @@ class PassThroughPortMixin(object):
|
|||||||
Return all CablePaths which traverse a given pass-through port.
|
Return all CablePaths which traverse a given pass-through port.
|
||||||
"""
|
"""
|
||||||
obj = get_object_or_404(self.queryset, pk=pk)
|
obj = get_object_or_404(self.queryset, pk=pk)
|
||||||
cablepaths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin', 'destination')
|
cablepaths = CablePath.objects.filter(_nodes__contains=obj)
|
||||||
serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
|
serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
@ -557,7 +552,7 @@ class ModuleViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsolePort.objects.prefetch_related(
|
queryset = ConsolePort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ConsolePortSerializer
|
serializer_class = serializers.ConsolePortSerializer
|
||||||
filterset_class = filtersets.ConsolePortFilterSet
|
filterset_class = filtersets.ConsolePortFilterSet
|
||||||
@ -566,7 +561,7 @@ class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ConsoleServerPortSerializer
|
serializer_class = serializers.ConsoleServerPortSerializer
|
||||||
filterset_class = filtersets.ConsoleServerPortFilterSet
|
filterset_class = filtersets.ConsoleServerPortFilterSet
|
||||||
@ -575,7 +570,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerPort.objects.prefetch_related(
|
queryset = PowerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerPortSerializer
|
serializer_class = serializers.PowerPortSerializer
|
||||||
filterset_class = filtersets.PowerPortFilterSet
|
filterset_class = filtersets.PowerPortFilterSet
|
||||||
@ -584,7 +579,7 @@ class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerOutlet.objects.prefetch_related(
|
queryset = PowerOutlet.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerOutletSerializer
|
serializer_class = serializers.PowerOutletSerializer
|
||||||
filterset_class = filtersets.PowerOutletFilterSet
|
filterset_class = filtersets.PowerOutletFilterSet
|
||||||
@ -593,8 +588,8 @@ class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
|
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path', 'cable__terminations', 'wireless_lans',
|
||||||
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.InterfaceSerializer
|
serializer_class = serializers.InterfaceSerializer
|
||||||
filterset_class = filtersets.InterfaceFilterSet
|
filterset_class = filtersets.InterfaceFilterSet
|
||||||
@ -603,7 +598,7 @@ class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = FrontPort.objects.prefetch_related(
|
queryset = FrontPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
|
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.FrontPortSerializer
|
serializer_class = serializers.FrontPortSerializer
|
||||||
filterset_class = filtersets.FrontPortFilterSet
|
filterset_class = filtersets.FrontPortFilterSet
|
||||||
@ -612,7 +607,7 @@ class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
|||||||
|
|
||||||
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = RearPort.objects.prefetch_related(
|
queryset = RearPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
|
'device__device_type__manufacturer', 'module__module_bay', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.RearPortSerializer
|
serializer_class = serializers.RearPortSerializer
|
||||||
filterset_class = filtersets.RearPortFilterSet
|
filterset_class = filtersets.RearPortFilterSet
|
||||||
@ -657,14 +652,18 @@ class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CableViewSet(NetBoxModelViewSet):
|
class CableViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
queryset = Cable.objects.prefetch_related('terminations__termination')
|
||||||
queryset = Cable.objects.prefetch_related(
|
|
||||||
'termination_a', 'termination_b'
|
|
||||||
)
|
|
||||||
serializer_class = serializers.CableSerializer
|
serializer_class = serializers.CableSerializer
|
||||||
filterset_class = filtersets.CableFilterSet
|
filterset_class = filtersets.CableFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class CableTerminationViewSet(NetBoxModelViewSet):
|
||||||
|
metadata_class = ContentTypeMetadata
|
||||||
|
queryset = CableTermination.objects.prefetch_related('cable', 'termination')
|
||||||
|
serializer_class = serializers.CableTerminationSerializer
|
||||||
|
filterset_class = filtersets.CableTerminationFilterSet
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
@ -698,7 +697,7 @@ class PowerPanelViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerFeed.objects.prefetch_related(
|
queryset = PowerFeed.objects.prefetch_related(
|
||||||
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
'power_panel', 'rack', '_path', 'cable__terminations', 'tags'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.PowerFeedSerializer
|
serializer_class = serializers.PowerFeedSerializer
|
||||||
filterset_class = filtersets.PowerFeedFilterSet
|
filterset_class = filtersets.PowerFeedFilterSet
|
||||||
@ -758,13 +757,13 @@ class ConnectedDeviceViewSet(ViewSet):
|
|||||||
device=peer_device,
|
device=peer_device,
|
||||||
name=peer_interface_name
|
name=peer_interface_name
|
||||||
)
|
)
|
||||||
endpoint = peer_interface.connected_endpoint
|
endpoints = peer_interface.connected_endpoints
|
||||||
|
|
||||||
# If an Interface, return the parent device
|
# If an Interface, return the parent device
|
||||||
if type(endpoint) is Interface:
|
if endpoints and type(endpoints[0]) is Interface:
|
||||||
device = get_object_or_404(
|
device = get_object_or_404(
|
||||||
Device.objects.restrict(request.user, 'view'),
|
Device.objects.restrict(request.user, 'view'),
|
||||||
pk=endpoint.device_id
|
pk=endpoints[0].device_id
|
||||||
)
|
)
|
||||||
return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
|
return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
|
||||||
|
|
||||||
|
@ -1282,6 +1282,22 @@ class CableLengthUnitChoices(ChoiceSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# CableTerminations
|
||||||
|
#
|
||||||
|
|
||||||
|
class CableEndChoices(ChoiceSet):
|
||||||
|
|
||||||
|
SIDE_A = 'A'
|
||||||
|
SIDE_B = 'B'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(SIDE_A, 'A'),
|
||||||
|
(SIDE_B, 'B'),
|
||||||
|
# ('', ''),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# PowerFeeds
|
# PowerFeeds
|
||||||
#
|
#
|
||||||
|
@ -85,6 +85,8 @@ MODULAR_COMPONENT_MODELS = Q(
|
|||||||
# Cabling and connections
|
# Cabling and connections
|
||||||
#
|
#
|
||||||
|
|
||||||
|
CABLE_TRACE_SVG_DEFAULT_WIDTH = 400
|
||||||
|
|
||||||
# Cable endpoint types
|
# Cable endpoint types
|
||||||
CABLE_TERMINATION_MODELS = Q(
|
CABLE_TERMINATION_MODELS = Q(
|
||||||
Q(app_label='circuits', model__in=(
|
Q(app_label='circuits', model__in=(
|
||||||
|
@ -21,6 +21,7 @@ from .models import *
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableFilterSet',
|
'CableFilterSet',
|
||||||
|
'CabledObjectFilterSet',
|
||||||
'CableTerminationFilterSet',
|
'CableTerminationFilterSet',
|
||||||
'ConsoleConnectionFilterSet',
|
'ConsoleConnectionFilterSet',
|
||||||
'ConsolePortFilterSet',
|
'ConsolePortFilterSet',
|
||||||
@ -1117,7 +1118,7 @@ class ModularDeviceComponentFilterSet(DeviceComponentFilterSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class CableTerminationFilterSet(django_filters.FilterSet):
|
class CabledObjectFilterSet(django_filters.FilterSet):
|
||||||
cabled = django_filters.BooleanFilter(
|
cabled = django_filters.BooleanFilter(
|
||||||
field_name='cable',
|
field_name='cable',
|
||||||
lookup_expr='isnull',
|
lookup_expr='isnull',
|
||||||
@ -1140,7 +1141,7 @@ class PathEndpointFilterSet(django_filters.FilterSet):
|
|||||||
class ConsolePortFilterSet(
|
class ConsolePortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
@ -1150,13 +1151,13 @@ class ConsolePortFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePort
|
model = ConsolePort
|
||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortFilterSet(
|
class ConsoleServerPortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
@ -1166,13 +1167,13 @@ class ConsoleServerPortFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPort
|
model = ConsoleServerPort
|
||||||
fields = ['id', 'name', 'label', 'description']
|
fields = ['id', 'name', 'label', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortFilterSet(
|
class PowerPortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
@ -1182,13 +1183,13 @@ class PowerPortFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPort
|
model = PowerPort
|
||||||
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description']
|
fields = ['id', 'name', 'label', 'maximum_draw', 'allocated_draw', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletFilterSet(
|
class PowerOutletFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
@ -1202,13 +1203,13 @@ class PowerOutletFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutlet
|
model = PowerOutlet
|
||||||
fields = ['id', 'name', 'label', 'feed_leg', 'description']
|
fields = ['id', 'name', 'label', 'feed_leg', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceFilterSet(
|
class InterfaceFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet,
|
CabledObjectFilterSet,
|
||||||
PathEndpointFilterSet
|
PathEndpointFilterSet
|
||||||
):
|
):
|
||||||
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
# Override device and device_id filters from DeviceComponentFilterSet to match against any peer virtual chassis
|
||||||
@ -1288,7 +1289,7 @@ class InterfaceFilterSet(
|
|||||||
model = Interface
|
model = Interface
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
'id', 'name', 'label', 'type', 'enabled', 'mtu', 'mgmt_only', 'poe_mode', 'poe_type', 'mode', 'rf_role',
|
||||||
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description',
|
'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'description', 'cable_end',
|
||||||
]
|
]
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_device(self, queryset, name, value):
|
||||||
@ -1342,7 +1343,7 @@ class InterfaceFilterSet(
|
|||||||
class FrontPortFilterSet(
|
class FrontPortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet
|
CabledObjectFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@ -1351,13 +1352,13 @@ class FrontPortFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPort
|
model = FrontPort
|
||||||
fields = ['id', 'name', 'label', 'type', 'color', 'description']
|
fields = ['id', 'name', 'label', 'type', 'color', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class RearPortFilterSet(
|
class RearPortFilterSet(
|
||||||
ModularDeviceComponentFilterSet,
|
ModularDeviceComponentFilterSet,
|
||||||
NetBoxModelFilterSet,
|
NetBoxModelFilterSet,
|
||||||
CableTerminationFilterSet
|
CabledObjectFilterSet
|
||||||
):
|
):
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=PortTypeChoices,
|
choices=PortTypeChoices,
|
||||||
@ -1366,7 +1367,7 @@ class RearPortFilterSet(
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPort
|
model = RearPort
|
||||||
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description']
|
fields = ['id', 'name', 'label', 'type', 'color', 'positions', 'description', 'cable_end']
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
class ModuleBayFilterSet(DeviceComponentFilterSet, NetBoxModelFilterSet):
|
||||||
@ -1514,10 +1515,18 @@ class VirtualChassisFilterSet(NetBoxModelFilterSet):
|
|||||||
|
|
||||||
|
|
||||||
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
||||||
termination_a_type = ContentTypeFilter()
|
termination_a_type = ContentTypeFilter(
|
||||||
termination_a_id = MultiValueNumberFilter()
|
field_name='terminations__termination_type'
|
||||||
termination_b_type = ContentTypeFilter()
|
)
|
||||||
termination_b_id = MultiValueNumberFilter()
|
termination_a_id = MultiValueNumberFilter(
|
||||||
|
field_name='terminations__termination_id'
|
||||||
|
)
|
||||||
|
termination_b_type = ContentTypeFilter(
|
||||||
|
field_name='terminations__termination_type'
|
||||||
|
)
|
||||||
|
termination_b_id = MultiValueNumberFilter(
|
||||||
|
field_name='terminations__termination_id'
|
||||||
|
)
|
||||||
type = django_filters.MultipleChoiceFilter(
|
type = django_filters.MultipleChoiceFilter(
|
||||||
choices=CableTypeChoices
|
choices=CableTypeChoices
|
||||||
)
|
)
|
||||||
@ -1528,44 +1537,57 @@ class CableFilterSet(TenancyFilterSet, NetBoxModelFilterSet):
|
|||||||
choices=ColorChoices
|
choices=ColorChoices
|
||||||
)
|
)
|
||||||
device_id = MultiValueNumberFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
method='filter_device'
|
method='filter_by_termination'
|
||||||
)
|
)
|
||||||
device = MultiValueCharFilter(
|
device = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_by_termination',
|
||||||
field_name='device__name'
|
field_name='device__name'
|
||||||
)
|
)
|
||||||
rack_id = MultiValueNumberFilter(
|
rack_id = MultiValueNumberFilter(
|
||||||
method='filter_device',
|
method='filter_by_termination',
|
||||||
field_name='device__rack_id'
|
field_name='rack_id'
|
||||||
)
|
)
|
||||||
rack = MultiValueCharFilter(
|
rack = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_by_termination',
|
||||||
field_name='device__rack__name'
|
field_name='rack__name'
|
||||||
|
)
|
||||||
|
location_id = MultiValueNumberFilter(
|
||||||
|
method='filter_by_termination',
|
||||||
|
field_name='location_id'
|
||||||
|
)
|
||||||
|
location = MultiValueCharFilter(
|
||||||
|
method='filter_by_termination',
|
||||||
|
field_name='location__name'
|
||||||
)
|
)
|
||||||
site_id = MultiValueNumberFilter(
|
site_id = MultiValueNumberFilter(
|
||||||
method='filter_device',
|
method='filter_by_termination',
|
||||||
field_name='device__site_id'
|
field_name='site_id'
|
||||||
)
|
)
|
||||||
site = MultiValueCharFilter(
|
site = MultiValueCharFilter(
|
||||||
method='filter_device',
|
method='filter_by_termination',
|
||||||
field_name='device__site__slug'
|
field_name='site__slug'
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = ['id', 'label', 'length', 'length_unit', 'termination_a_id', 'termination_b_id']
|
fields = ['id', 'label', 'length', 'length_unit']
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(label__icontains=value)
|
return queryset.filter(label__icontains=value)
|
||||||
|
|
||||||
def filter_device(self, queryset, name, value):
|
def filter_by_termination(self, queryset, name, value):
|
||||||
queryset = queryset.filter(
|
# Filter by a related object cached on CableTermination. Note the underscore preceding the field name.
|
||||||
Q(**{'_termination_a_{}__in'.format(name): value}) |
|
# Supported objects: device, rack, location, site
|
||||||
Q(**{'_termination_b_{}__in'.format(name): value})
|
return queryset.filter(**{f'terminations___{name}__in': value}).distinct()
|
||||||
)
|
|
||||||
return queryset
|
|
||||||
|
class CableTerminationFilterSet(BaseFilterSet):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = CableTermination
|
||||||
|
fields = ['id', 'cable', 'cable_end', 'termination_type', 'termination_id']
|
||||||
|
|
||||||
|
|
||||||
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
||||||
@ -1625,7 +1647,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
|
|||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
queryset=Region.objects.all(),
|
||||||
field_name='power_panel__site__region',
|
field_name='power_panel__site__region',
|
||||||
@ -1679,7 +1701,9 @@ class PowerFeedFilterSet(NetBoxModelFilterSet, CableTerminationFilterSet, PathEn
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerFeed
|
model = PowerFeed
|
||||||
fields = ['id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization']
|
fields = [
|
||||||
|
'id', 'name', 'status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'cable_end',
|
||||||
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -1,279 +1,171 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
from circuits.models import Circuit, CircuitTermination, Provider
|
from circuits.models import Circuit, CircuitTermination, Provider
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.models import Tag
|
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField
|
||||||
from netbox.forms import NetBoxModelForm
|
from .models import CableForm
|
||||||
from tenancy.forms import TenancyForm
|
|
||||||
from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect
|
|
||||||
|
|
||||||
__all__ = (
|
|
||||||
'ConnectCableToCircuitTerminationForm',
|
|
||||||
'ConnectCableToConsolePortForm',
|
|
||||||
'ConnectCableToConsoleServerPortForm',
|
|
||||||
'ConnectCableToFrontPortForm',
|
|
||||||
'ConnectCableToInterfaceForm',
|
|
||||||
'ConnectCableToPowerFeedForm',
|
|
||||||
'ConnectCableToPowerPortForm',
|
|
||||||
'ConnectCableToPowerOutletForm',
|
|
||||||
'ConnectCableToRearPortForm',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm):
|
def get_cable_form(a_type, b_type):
|
||||||
"""
|
|
||||||
Base form for connecting a Cable to a Device component
|
|
||||||
"""
|
|
||||||
termination_b_region = DynamicModelChoiceField(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
label='Region',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_sitegroup = DynamicModelChoiceField(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
label='Site group',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_site = DynamicModelChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
label='Site',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'region_id': '$termination_b_region',
|
|
||||||
'group_id': '$termination_b_sitegroup',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_location = DynamicModelChoiceField(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
label='Location',
|
|
||||||
required=False,
|
|
||||||
null_option='None',
|
|
||||||
query_params={
|
|
||||||
'site_id': '$termination_b_site'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_rack = DynamicModelChoiceField(
|
|
||||||
queryset=Rack.objects.all(),
|
|
||||||
label='Rack',
|
|
||||||
required=False,
|
|
||||||
null_option='None',
|
|
||||||
query_params={
|
|
||||||
'site_id': '$termination_b_site',
|
|
||||||
'location_id': '$termination_b_location',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_device = DynamicModelChoiceField(
|
|
||||||
queryset=Device.objects.all(),
|
|
||||||
label='Device',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'site_id': '$termination_b_site',
|
|
||||||
'location_id': '$termination_b_location',
|
|
||||||
'rack_id': '$termination_b_rack',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class FormMetaclass(forms.models.ModelFormMetaclass):
|
||||||
model = Cable
|
|
||||||
fields = [
|
|
||||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_rack',
|
|
||||||
'termination_b_device', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
|
||||||
'length', 'length_unit', 'tags',
|
|
||||||
]
|
|
||||||
widgets = {
|
|
||||||
'status': StaticSelect,
|
|
||||||
'type': StaticSelect,
|
|
||||||
'length_unit': StaticSelect,
|
|
||||||
}
|
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
def __new__(mcs, name, bases, attrs):
|
||||||
# Return the PK rather than the object
|
|
||||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
|
||||||
|
|
||||||
|
for cable_end, term_cls in (('a', a_type), ('b', b_type)):
|
||||||
|
|
||||||
class ConnectCableToConsolePortForm(ConnectCableToDeviceForm):
|
attrs[f'termination_{cable_end}_region'] = DynamicModelChoiceField(
|
||||||
termination_b_id = DynamicModelChoiceField(
|
queryset=Region.objects.all(),
|
||||||
queryset=ConsolePort.objects.all(),
|
label='Region',
|
||||||
label='Name',
|
required=False,
|
||||||
disabled_indicator='_occupied',
|
initial_params={
|
||||||
query_params={
|
'sites': f'$termination_{cable_end}_site'
|
||||||
'device_id': '$termination_b_device'
|
}
|
||||||
}
|
)
|
||||||
)
|
attrs[f'termination_{cable_end}_sitegroup'] = DynamicModelChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
label='Site group',
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
'sites': f'$termination_{cable_end}_site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'termination_{cable_end}_site'] = DynamicModelChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
label='Site',
|
||||||
|
required=False,
|
||||||
|
query_params={
|
||||||
|
'region_id': f'$termination_{cable_end}_region',
|
||||||
|
'group_id': f'$termination_{cable_end}_sitegroup',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'termination_{cable_end}_location'] = DynamicModelChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
label='Location',
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'site_id': f'$termination_{cable_end}_site'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Device component
|
||||||
|
if hasattr(term_cls, 'device'):
|
||||||
|
|
||||||
class ConnectCableToConsoleServerPortForm(ConnectCableToDeviceForm):
|
attrs[f'termination_{cable_end}_rack'] = DynamicModelChoiceField(
|
||||||
termination_b_id = DynamicModelChoiceField(
|
queryset=Rack.objects.all(),
|
||||||
queryset=ConsoleServerPort.objects.all(),
|
label='Rack',
|
||||||
label='Name',
|
required=False,
|
||||||
disabled_indicator='_occupied',
|
null_option='None',
|
||||||
query_params={
|
initial_params={
|
||||||
'device_id': '$termination_b_device'
|
'devices': f'$termination_{cable_end}_device'
|
||||||
}
|
},
|
||||||
)
|
query_params={
|
||||||
|
'site_id': f'$termination_{cable_end}_site',
|
||||||
|
'location_id': f'$termination_{cable_end}_location',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'termination_{cable_end}_device'] = DynamicModelChoiceField(
|
||||||
|
queryset=Device.objects.all(),
|
||||||
|
label='Device',
|
||||||
|
required=False,
|
||||||
|
initial_params={
|
||||||
|
f'{term_cls._meta.model_name}s__in': f'${cable_end}_terminations'
|
||||||
|
},
|
||||||
|
query_params={
|
||||||
|
'site_id': f'$termination_{cable_end}_site',
|
||||||
|
'location_id': f'$termination_{cable_end}_location',
|
||||||
|
'rack_id': f'$termination_{cable_end}_rack',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=term_cls.objects.all(),
|
||||||
|
label=term_cls._meta.verbose_name.title(),
|
||||||
|
disabled_indicator='_occupied',
|
||||||
|
query_params={
|
||||||
|
'device_id': f'$termination_{cable_end}_device',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# PowerFeed
|
||||||
|
elif term_cls == PowerFeed:
|
||||||
|
|
||||||
class ConnectCableToPowerPortForm(ConnectCableToDeviceForm):
|
attrs[f'termination_{cable_end}_powerpanel'] = DynamicModelChoiceField(
|
||||||
termination_b_id = DynamicModelChoiceField(
|
queryset=PowerPanel.objects.all(),
|
||||||
queryset=PowerPort.objects.all(),
|
label='Power Panel',
|
||||||
label='Name',
|
required=False,
|
||||||
disabled_indicator='_occupied',
|
initial_params={
|
||||||
query_params={
|
'powerfeeds__in': f'${cable_end}_terminations'
|
||||||
'device_id': '$termination_b_device'
|
},
|
||||||
}
|
query_params={
|
||||||
)
|
'site_id': f'$termination_{cable_end}_site',
|
||||||
|
'location_id': f'$termination_{cable_end}_location',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=term_cls.objects.all(),
|
||||||
|
label='Power Feed',
|
||||||
|
disabled_indicator='_occupied',
|
||||||
|
query_params={
|
||||||
|
'powerpanel_id': f'$termination_{cable_end}_powerpanel',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# CircuitTermination
|
||||||
|
elif term_cls == CircuitTermination:
|
||||||
|
|
||||||
class ConnectCableToPowerOutletForm(ConnectCableToDeviceForm):
|
attrs[f'termination_{cable_end}_provider'] = DynamicModelChoiceField(
|
||||||
termination_b_id = DynamicModelChoiceField(
|
queryset=Provider.objects.all(),
|
||||||
queryset=PowerOutlet.objects.all(),
|
label='Provider',
|
||||||
label='Name',
|
initial_params={
|
||||||
disabled_indicator='_occupied',
|
'circuits': f'$termination_{cable_end}_circuit'
|
||||||
query_params={
|
},
|
||||||
'device_id': '$termination_b_device'
|
required=False
|
||||||
}
|
)
|
||||||
)
|
attrs[f'termination_{cable_end}_circuit'] = DynamicModelChoiceField(
|
||||||
|
queryset=Circuit.objects.all(),
|
||||||
|
label='Circuit',
|
||||||
|
initial_params={
|
||||||
|
'terminations__in': f'${cable_end}_terminations'
|
||||||
|
},
|
||||||
|
query_params={
|
||||||
|
'provider_id': f'$termination_{cable_end}_provider',
|
||||||
|
'site_id': f'$termination_{cable_end}_site',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
attrs[f'{cable_end}_terminations'] = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=term_cls.objects.all(),
|
||||||
|
label='Side',
|
||||||
|
disabled_indicator='_occupied',
|
||||||
|
query_params={
|
||||||
|
'circuit_id': f'termination_{cable_end}_circuit',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
class ConnectCableToInterfaceForm(ConnectCableToDeviceForm):
|
class _CableForm(CableForm, metaclass=FormMetaclass):
|
||||||
termination_b_id = DynamicModelChoiceField(
|
|
||||||
queryset=Interface.objects.all(),
|
|
||||||
label='Name',
|
|
||||||
disabled_indicator='_occupied',
|
|
||||||
query_params={
|
|
||||||
'device_id': '$termination_b_device',
|
|
||||||
'kind': 'physical',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
class ConnectCableToFrontPortForm(ConnectCableToDeviceForm):
|
# TODO: Temporary hack to work around list handling limitations with utils.normalize_querydict()
|
||||||
termination_b_id = DynamicModelChoiceField(
|
for field_name in ('a_terminations', 'b_terminations'):
|
||||||
queryset=FrontPort.objects.all(),
|
if field_name in kwargs.get('initial', {}) and type(kwargs['initial'][field_name]) is not list:
|
||||||
label='Name',
|
kwargs['initial'][field_name] = [kwargs['initial'][field_name]]
|
||||||
disabled_indicator='_occupied',
|
|
||||||
query_params={
|
|
||||||
'device_id': '$termination_b_device'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
class ConnectCableToRearPortForm(ConnectCableToDeviceForm):
|
if self.instance and self.instance.pk:
|
||||||
termination_b_id = DynamicModelChoiceField(
|
# Initialize A/B terminations when modifying an existing Cable instance
|
||||||
queryset=RearPort.objects.all(),
|
self.initial['a_terminations'] = self.instance.get_a_terminations()
|
||||||
label='Name',
|
self.initial['b_terminations'] = self.instance.get_b_terminations()
|
||||||
disabled_indicator='_occupied',
|
|
||||||
query_params={
|
|
||||||
'device_id': '$termination_b_device'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm):
|
# Set the A/B terminations on the Cable instance
|
||||||
termination_b_provider = DynamicModelChoiceField(
|
self.instance.a_terminations = self.cleaned_data['a_terminations']
|
||||||
queryset=Provider.objects.all(),
|
self.instance.b_terminations = self.cleaned_data['b_terminations']
|
||||||
label='Provider',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_region = DynamicModelChoiceField(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
label='Region',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_sitegroup = DynamicModelChoiceField(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
label='Site group',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_site = DynamicModelChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
label='Site',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'region_id': '$termination_b_region',
|
|
||||||
'group_id': '$termination_b_sitegroup',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_circuit = DynamicModelChoiceField(
|
|
||||||
queryset=Circuit.objects.all(),
|
|
||||||
label='Circuit',
|
|
||||||
query_params={
|
|
||||||
'provider_id': '$termination_b_provider',
|
|
||||||
'site_id': '$termination_b_site',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_id = DynamicModelChoiceField(
|
|
||||||
queryset=CircuitTermination.objects.all(),
|
|
||||||
label='Side',
|
|
||||||
disabled_indicator='_occupied',
|
|
||||||
query_params={
|
|
||||||
'circuit_id': '$termination_b_circuit'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(ConnectCableToDeviceForm.Meta):
|
return super().save(*args, **kwargs)
|
||||||
fields = [
|
|
||||||
'termination_b_provider', 'termination_b_region', 'termination_b_sitegroup', 'termination_b_site',
|
|
||||||
'termination_b_circuit', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label', 'color',
|
|
||||||
'length', 'length_unit', 'tags',
|
|
||||||
]
|
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
return _CableForm
|
||||||
# Return the PK rather than the object
|
|
||||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm):
|
|
||||||
termination_b_region = DynamicModelChoiceField(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
label='Region',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_sitegroup = DynamicModelChoiceField(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
label='Site group',
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
termination_b_site = DynamicModelChoiceField(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
label='Site',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'region_id': '$termination_b_region',
|
|
||||||
'group_id': '$termination_b_sitegroup',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_location = DynamicModelChoiceField(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
label='Location',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'site_id': '$termination_b_site'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_powerpanel = DynamicModelChoiceField(
|
|
||||||
queryset=PowerPanel.objects.all(),
|
|
||||||
label='Power Panel',
|
|
||||||
required=False,
|
|
||||||
query_params={
|
|
||||||
'site_id': '$termination_b_site',
|
|
||||||
'location_id': '$termination_b_location',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
termination_b_id = DynamicModelChoiceField(
|
|
||||||
queryset=PowerFeed.objects.all(),
|
|
||||||
label='Name',
|
|
||||||
disabled_indicator='_occupied',
|
|
||||||
query_params={
|
|
||||||
'power_panel_id': '$termination_b_powerpanel'
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(ConnectCableToDeviceForm.Meta):
|
|
||||||
fields = [
|
|
||||||
'termination_b_region', 'termination_b_sitegroup', 'termination_b_site', 'termination_b_location',
|
|
||||||
'termination_b_powerpanel', 'termination_b_id', 'type', 'status', 'tenant_group', 'tenant', 'label',
|
|
||||||
'color', 'length', 'length_unit', 'tags',
|
|
||||||
]
|
|
||||||
|
|
||||||
def clean_termination_b_id(self):
|
|
||||||
# Return the PK rather than the object
|
|
||||||
return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
|
|
||||||
|
@ -730,7 +730,7 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
model = Cable
|
model = Cable
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
(None, ('q', 'tag')),
|
(None, ('q', 'tag')),
|
||||||
('Location', ('site_id', 'rack_id', 'device_id')),
|
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
|
||||||
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
|
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
|
||||||
('Tenant', ('tenant_group_id', 'tenant_id')),
|
('Tenant', ('tenant_group_id', 'tenant_id')),
|
||||||
)
|
)
|
||||||
@ -747,13 +747,23 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
},
|
},
|
||||||
label=_('Site')
|
label=_('Site')
|
||||||
)
|
)
|
||||||
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Location'),
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'site_id': '$site_id'
|
||||||
|
}
|
||||||
|
)
|
||||||
rack_id = DynamicModelMultipleChoiceField(
|
rack_id = DynamicModelMultipleChoiceField(
|
||||||
queryset=Rack.objects.all(),
|
queryset=Rack.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('Rack'),
|
label=_('Rack'),
|
||||||
null_option='None',
|
null_option='None',
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site_id'
|
'site_id': '$site_id',
|
||||||
|
'location_id': '$location_id',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
device_id = DynamicModelMultipleChoiceField(
|
device_id = DynamicModelMultipleChoiceField(
|
||||||
@ -761,8 +771,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
required=False,
|
required=False,
|
||||||
query_params={
|
query_params={
|
||||||
'site_id': '$site_id',
|
'site_id': '$site_id',
|
||||||
'tenant_id': '$tenant_id',
|
'location_id': '$location_id',
|
||||||
'rack_id': '$rack_id',
|
'rack_id': '$rack_id',
|
||||||
|
'tenant_id': '$tenant_id',
|
||||||
},
|
},
|
||||||
label=_('Device')
|
label=_('Device')
|
||||||
)
|
)
|
||||||
|
5
netbox/dcim/graphql/mixins.py
Normal file
5
netbox/dcim/graphql/mixins.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class CabledObjectMixin:
|
||||||
|
|
||||||
|
def resolve_cable_end(self, info):
|
||||||
|
# Handle empty values
|
||||||
|
return self.cable_end or None
|
@ -7,6 +7,7 @@ from extras.graphql.mixins import (
|
|||||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||||
from netbox.graphql.scalars import BigInt
|
from netbox.graphql.scalars import BigInt
|
||||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
|
||||||
|
from .mixins import CabledObjectMixin
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableType',
|
'CableType',
|
||||||
@ -99,7 +100,15 @@ class CableType(NetBoxObjectType):
|
|||||||
return self.length_unit or None
|
return self.length_unit or None
|
||||||
|
|
||||||
|
|
||||||
class ConsolePortType(ComponentObjectType):
|
class CableTerminationType(NetBoxObjectType):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = models.CableTermination
|
||||||
|
fields = '__all__'
|
||||||
|
filterset_class = filtersets.CableTerminationFilterSet
|
||||||
|
|
||||||
|
|
||||||
|
class ConsolePortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ConsolePort
|
model = models.ConsolePort
|
||||||
@ -121,7 +130,7 @@ class ConsolePortTemplateType(ComponentTemplateObjectType):
|
|||||||
return self.type or None
|
return self.type or None
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortType(ComponentObjectType):
|
class ConsoleServerPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.ConsoleServerPort
|
model = models.ConsoleServerPort
|
||||||
@ -203,7 +212,7 @@ class DeviceTypeType(NetBoxObjectType):
|
|||||||
return self.airflow or None
|
return self.airflow or None
|
||||||
|
|
||||||
|
|
||||||
class FrontPortType(ComponentObjectType):
|
class FrontPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.FrontPort
|
model = models.FrontPort
|
||||||
@ -219,7 +228,7 @@ class FrontPortTemplateType(ComponentTemplateObjectType):
|
|||||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class InterfaceType(IPAddressesMixin, ComponentObjectType):
|
class InterfaceType(IPAddressesMixin, ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Interface
|
model = models.Interface
|
||||||
@ -322,7 +331,7 @@ class PlatformType(OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.PlatformFilterSet
|
filterset_class = filtersets.PlatformFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerFeedType(NetBoxObjectType):
|
class PowerFeedType(NetBoxObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerFeed
|
model = models.PowerFeed
|
||||||
@ -330,7 +339,7 @@ class PowerFeedType(NetBoxObjectType):
|
|||||||
filterset_class = filtersets.PowerFeedFilterSet
|
filterset_class = filtersets.PowerFeedFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletType(ComponentObjectType):
|
class PowerOutletType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerOutlet
|
model = models.PowerOutlet
|
||||||
@ -366,7 +375,7 @@ class PowerPanelType(NetBoxObjectType):
|
|||||||
filterset_class = filtersets.PowerPanelFilterSet
|
filterset_class = filtersets.PowerPanelFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerPortType(ComponentObjectType):
|
class PowerPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.PowerPort
|
model = models.PowerPort
|
||||||
@ -418,7 +427,7 @@ class RackRoleType(OrganizationalObjectType):
|
|||||||
filterset_class = filtersets.RackRoleFilterSet
|
filterset_class = filtersets.RackRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RearPortType(ComponentObjectType):
|
class RearPortType(ComponentObjectType, CabledObjectMixin):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.RearPort
|
model = models.RearPort
|
||||||
|
@ -81,7 +81,7 @@ class Command(BaseCommand):
|
|||||||
self.stdout.write(f'Retracing {origins_count} cabled {model._meta.verbose_name_plural}...')
|
self.stdout.write(f'Retracing {origins_count} cabled {model._meta.verbose_name_plural}...')
|
||||||
i = 0
|
i = 0
|
||||||
for i, obj in enumerate(origins, start=1):
|
for i, obj in enumerate(origins, start=1):
|
||||||
create_cablepath(obj)
|
create_cablepath([obj])
|
||||||
if not i % 100:
|
if not i % 100:
|
||||||
self.draw_progress_bar(i * 100 / origins_count)
|
self.draw_progress_bar(i * 100 / origins_count)
|
||||||
self.draw_progress_bar(100)
|
self.draw_progress_bar(100)
|
||||||
|
95
netbox/dcim/migrations/0157_new_cabling_models.py
Normal file
95
netbox/dcim/migrations/0157_new_cabling_models.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('dcim', '0156_location_status'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
|
||||||
|
# Create CableTermination model
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='CableTermination',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||||
|
('cable_end', models.CharField(max_length=1)),
|
||||||
|
('termination_id', models.PositiveBigIntegerField()),
|
||||||
|
('cable', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='terminations', to='dcim.cable')),
|
||||||
|
('termination_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(models.Q(('app_label', 'circuits'), ('model__in', ('circuittermination',))), models.Q(('app_label', 'dcim'), ('model__in', ('consoleport', 'consoleserverport', 'frontport', 'interface', 'powerfeed', 'poweroutlet', 'powerport', 'rearport'))), _connector='OR')), on_delete=django.db.models.deletion.PROTECT, related_name='+', to='contenttypes.contenttype')),
|
||||||
|
('_device', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.device')),
|
||||||
|
('_rack', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.rack')),
|
||||||
|
('_location', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location')),
|
||||||
|
('_site', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ('cable', 'cable_end', 'pk'),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='cabletermination',
|
||||||
|
constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cable_termination_unique_termination'),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Update CablePath model
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='cablepath',
|
||||||
|
old_name='path',
|
||||||
|
new_name='_nodes',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='path',
|
||||||
|
field=models.JSONField(default=list),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='is_complete',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Add cable_end field to cable termination models
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='interface',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='cable_end',
|
||||||
|
field=models.CharField(blank=True, max_length=1),
|
||||||
|
),
|
||||||
|
]
|
76
netbox/dcim/migrations/0158_populate_cable_terminations.py
Normal file
76
netbox/dcim/migrations/0158_populate_cable_terminations.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def cache_related_objects(termination):
|
||||||
|
"""
|
||||||
|
Replicate caching logic from CableTermination.cache_related_objects()
|
||||||
|
"""
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
|
# Device components
|
||||||
|
if getattr(termination, 'device', None):
|
||||||
|
attrs['_device'] = termination.device
|
||||||
|
attrs['_rack'] = termination.device.rack
|
||||||
|
attrs['_location'] = termination.device.location
|
||||||
|
attrs['_site'] = termination.device.site
|
||||||
|
|
||||||
|
# Power feeds
|
||||||
|
elif getattr(termination, 'rack', None):
|
||||||
|
attrs['_rack'] = termination.rack
|
||||||
|
attrs['_location'] = termination.rack.location
|
||||||
|
attrs['_site'] = termination.rack.site
|
||||||
|
|
||||||
|
# Circuit terminations
|
||||||
|
elif getattr(termination, 'site', None):
|
||||||
|
attrs['_site'] = termination.site
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
def populate_cable_terminations(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Replicate terminations from the Cable model into CableTermination instances.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Cable = apps.get_model('dcim', 'Cable')
|
||||||
|
CableTermination = apps.get_model('dcim', 'CableTermination')
|
||||||
|
|
||||||
|
# Retrieve the necessary data from Cable objects
|
||||||
|
cables = Cable.objects.values(
|
||||||
|
'id', 'termination_a_type', 'termination_a_id', 'termination_b_type', 'termination_b_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Queue CableTerminations to be created
|
||||||
|
cable_terminations = []
|
||||||
|
for i, cable in enumerate(cables, start=1):
|
||||||
|
for cable_end in ('a', 'b'):
|
||||||
|
# We must manually instantiate the termination object, because GFK fields are not
|
||||||
|
# supported within migrations.
|
||||||
|
termination_ct = ContentType.objects.get(pk=cable[f'termination_{cable_end}_type'])
|
||||||
|
termination_model = apps.get_model(termination_ct.app_label, termination_ct.model)
|
||||||
|
termination = termination_model.objects.get(pk=cable[f'termination_{cable_end}_id'])
|
||||||
|
|
||||||
|
cable_terminations.append(CableTermination(
|
||||||
|
cable_id=cable['id'],
|
||||||
|
cable_end=cable_end.upper(),
|
||||||
|
termination_type_id=cable[f'termination_{cable_end}_type'],
|
||||||
|
termination_id=cable[f'termination_{cable_end}_id'],
|
||||||
|
**cache_related_objects(termination)
|
||||||
|
))
|
||||||
|
|
||||||
|
# Bulk create the termination objects
|
||||||
|
CableTermination.objects.bulk_create(cable_terminations, batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0157_new_cabling_models'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_cable_terminations,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
50
netbox/dcim/migrations/0159_populate_cable_paths.py
Normal file
50
netbox/dcim/migrations/0159_populate_cable_paths.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from dcim.utils import compile_path_node
|
||||||
|
|
||||||
|
|
||||||
|
def populate_cable_paths(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Replicate terminations from the Cable model into CableTermination instances.
|
||||||
|
"""
|
||||||
|
CablePath = apps.get_model('dcim', 'CablePath')
|
||||||
|
|
||||||
|
# Construct the new two-dimensional path, and add the origin & destination objects to the nodes list
|
||||||
|
cable_paths = []
|
||||||
|
for cablepath in CablePath.objects.all():
|
||||||
|
|
||||||
|
# Origin
|
||||||
|
origin = compile_path_node(cablepath.origin_type_id, cablepath.origin_id)
|
||||||
|
cablepath.path.append([origin])
|
||||||
|
cablepath._nodes.insert(0, origin)
|
||||||
|
|
||||||
|
# Transit nodes
|
||||||
|
cablepath.path.extend([
|
||||||
|
[node] for node in cablepath._nodes[1:]
|
||||||
|
])
|
||||||
|
|
||||||
|
# Destination
|
||||||
|
if cablepath.destination_id:
|
||||||
|
destination = compile_path_node(cablepath.destination_type_id, cablepath.destination_id)
|
||||||
|
cablepath.path.append([destination])
|
||||||
|
cablepath._nodes.append(destination)
|
||||||
|
cablepath.is_complete = True
|
||||||
|
|
||||||
|
cable_paths.append(cablepath)
|
||||||
|
|
||||||
|
# Bulk update all CableTerminations
|
||||||
|
CablePath.objects.bulk_update(cable_paths, fields=('path', '_nodes', 'is_complete'), batch_size=100)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0158_populate_cable_terminations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_cable_paths,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
42
netbox/dcim/migrations/0160_populate_cable_ends.py
Normal file
42
netbox/dcim/migrations/0160_populate_cable_ends.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def populate_cable_terminations(apps, schema_editor):
|
||||||
|
Cable = apps.get_model('dcim', 'Cable')
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
|
||||||
|
cable_termination_models = (
|
||||||
|
apps.get_model('dcim', 'ConsolePort'),
|
||||||
|
apps.get_model('dcim', 'ConsoleServerPort'),
|
||||||
|
apps.get_model('dcim', 'PowerPort'),
|
||||||
|
apps.get_model('dcim', 'PowerOutlet'),
|
||||||
|
apps.get_model('dcim', 'Interface'),
|
||||||
|
apps.get_model('dcim', 'FrontPort'),
|
||||||
|
apps.get_model('dcim', 'RearPort'),
|
||||||
|
apps.get_model('dcim', 'PowerFeed'),
|
||||||
|
apps.get_model('circuits', 'CircuitTermination'),
|
||||||
|
)
|
||||||
|
|
||||||
|
for model in cable_termination_models:
|
||||||
|
ct = ContentType.objects.get_for_model(model)
|
||||||
|
model.objects.filter(
|
||||||
|
id__in=Cable.objects.filter(termination_a_type=ct).values_list('termination_a_id', flat=True)
|
||||||
|
).update(cable_end='A')
|
||||||
|
model.objects.filter(
|
||||||
|
id__in=Cable.objects.filter(termination_b_type=ct).values_list('termination_b_id', flat=True)
|
||||||
|
).update(cable_end='B')
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0037_new_cabling_models'),
|
||||||
|
('dcim', '0159_populate_cable_paths'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_cable_terminations,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
134
netbox/dcim/migrations/0161_cabling_cleanup.py
Normal file
134
netbox/dcim/migrations/0161_cabling_cleanup.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0160_populate_cable_ends'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
|
||||||
|
# Remove old fields from Cable
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='cable',
|
||||||
|
options={'ordering': ('pk',)},
|
||||||
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='cable',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_a_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_a_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_b_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='termination_b_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='_termination_a_device',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cable',
|
||||||
|
name='_termination_b_device',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Remove old fields from CablePath
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='cablepath',
|
||||||
|
unique_together=set(),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='destination_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='destination_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='origin_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cablepath',
|
||||||
|
name='origin_type',
|
||||||
|
),
|
||||||
|
|
||||||
|
# Remove link peer type/ID fields from cable termination models
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='interface',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='interface',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='_link_peer_id',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='_link_peer_type',
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
@ -1,10 +1,12 @@
|
|||||||
|
import itertools
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
|
from django.dispatch import Signal
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
@ -13,17 +15,21 @@ from dcim.fields import PathField
|
|||||||
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
|
||||||
from netbox.models import NetBoxModel
|
from netbox.models import NetBoxModel
|
||||||
from utilities.fields import ColorField
|
from utilities.fields import ColorField
|
||||||
|
from utilities.querysets import RestrictedQuerySet
|
||||||
from utilities.utils import to_meters
|
from utilities.utils import to_meters
|
||||||
from .devices import Device
|
from wireless.models import WirelessLink
|
||||||
from .device_components import FrontPort, RearPort
|
from .device_components import FrontPort, RearPort
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Cable',
|
'Cable',
|
||||||
'CablePath',
|
'CablePath',
|
||||||
|
'CableTermination',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
trace_paths = Signal()
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
@ -32,28 +38,6 @@ class Cable(NetBoxModel):
|
|||||||
"""
|
"""
|
||||||
A physical connection between two endpoints.
|
A physical connection between two endpoints.
|
||||||
"""
|
"""
|
||||||
termination_a_type = models.ForeignKey(
|
|
||||||
to=ContentType,
|
|
||||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='+'
|
|
||||||
)
|
|
||||||
termination_a_id = models.PositiveBigIntegerField()
|
|
||||||
termination_a = GenericForeignKey(
|
|
||||||
ct_field='termination_a_type',
|
|
||||||
fk_field='termination_a_id'
|
|
||||||
)
|
|
||||||
termination_b_type = models.ForeignKey(
|
|
||||||
to=ContentType,
|
|
||||||
limit_choices_to=CABLE_TERMINATION_MODELS,
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
related_name='+'
|
|
||||||
)
|
|
||||||
termination_b_id = models.PositiveBigIntegerField()
|
|
||||||
termination_b = GenericForeignKey(
|
|
||||||
ct_field='termination_b_type',
|
|
||||||
fk_field='termination_b_id'
|
|
||||||
)
|
|
||||||
type = models.CharField(
|
type = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CableTypeChoices,
|
choices=CableTypeChoices,
|
||||||
@ -96,31 +80,11 @@ class Cable(NetBoxModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
# Cache the associated device (where applicable) for the A and B terminations. This enables filtering of Cables by
|
|
||||||
# their associated Devices.
|
|
||||||
_termination_a_device = models.ForeignKey(
|
|
||||||
to=Device,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
_termination_b_device = models.ForeignKey(
|
|
||||||
to=Device,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['pk']
|
ordering = ('pk',)
|
||||||
unique_together = (
|
|
||||||
('termination_a_type', 'termination_a_id'),
|
|
||||||
('termination_b_type', 'termination_b_id'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, a_terminations=None, b_terminations=None, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# A copy of the PK to be used by __str__ in case the object is deleted
|
# A copy of the PK to be used by __str__ in case the object is deleted
|
||||||
@ -129,19 +93,12 @@ class Cable(NetBoxModel):
|
|||||||
# Cache the original status so we can check later if it's been changed
|
# Cache the original status so we can check later if it's been changed
|
||||||
self._orig_status = self.status
|
self._orig_status = self.status
|
||||||
|
|
||||||
@classmethod
|
# Assign any *new* CableTerminations for the instance. These will replace any existing
|
||||||
def from_db(cls, db, field_names, values):
|
# terminations on save().
|
||||||
"""
|
if a_terminations is not None:
|
||||||
Cache the original A and B terminations of existing Cable instances for later reference inside clean().
|
self.a_terminations = a_terminations
|
||||||
"""
|
if b_terminations is not None:
|
||||||
instance = super().from_db(db, field_names, values)
|
self.b_terminations = b_terminations
|
||||||
|
|
||||||
instance._orig_termination_a_type_id = instance.termination_a_type_id
|
|
||||||
instance._orig_termination_a_id = instance.termination_a_id
|
|
||||||
instance._orig_termination_b_type_id = instance.termination_b_type_id
|
|
||||||
instance._orig_termination_b_id = instance.termination_b_id
|
|
||||||
|
|
||||||
return instance
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
pk = self.pk or self._pk
|
pk = self.pk or self._pk
|
||||||
@ -151,123 +108,41 @@ class Cable(NetBoxModel):
|
|||||||
return reverse('dcim:cable', args=[self.pk])
|
return reverse('dcim:cable', args=[self.pk])
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
from circuits.models import CircuitTermination
|
|
||||||
|
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Validate that termination A exists
|
|
||||||
if not hasattr(self, 'termination_a_type'):
|
|
||||||
raise ValidationError('Termination A type has not been specified')
|
|
||||||
try:
|
|
||||||
self.termination_a_type.model_class().objects.get(pk=self.termination_a_id)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_a': 'Invalid ID for type {}'.format(self.termination_a_type)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Validate that termination B exists
|
|
||||||
if not hasattr(self, 'termination_b_type'):
|
|
||||||
raise ValidationError('Termination B type has not been specified')
|
|
||||||
try:
|
|
||||||
self.termination_b_type.model_class().objects.get(pk=self.termination_b_id)
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_b': 'Invalid ID for type {}'.format(self.termination_b_type)
|
|
||||||
})
|
|
||||||
|
|
||||||
# If editing an existing Cable instance, check that neither termination has been modified.
|
|
||||||
if self.pk:
|
|
||||||
err_msg = 'Cable termination points may not be modified. Delete and recreate the cable instead.'
|
|
||||||
if (
|
|
||||||
self.termination_a_type_id != self._orig_termination_a_type_id or
|
|
||||||
self.termination_a_id != self._orig_termination_a_id
|
|
||||||
):
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_a': err_msg
|
|
||||||
})
|
|
||||||
if (
|
|
||||||
self.termination_b_type_id != self._orig_termination_b_type_id or
|
|
||||||
self.termination_b_id != self._orig_termination_b_id
|
|
||||||
):
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_b': err_msg
|
|
||||||
})
|
|
||||||
|
|
||||||
type_a = self.termination_a_type.model
|
|
||||||
type_b = self.termination_b_type.model
|
|
||||||
|
|
||||||
# Validate interface types
|
|
||||||
if type_a == 'interface' and self.termination_a.type in NONCONNECTABLE_IFACE_TYPES:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_a_id': 'Cables cannot be terminated to {} interfaces'.format(
|
|
||||||
self.termination_a.get_type_display()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
if type_b == 'interface' and self.termination_b.type in NONCONNECTABLE_IFACE_TYPES:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_b_id': 'Cables cannot be terminated to {} interfaces'.format(
|
|
||||||
self.termination_b.get_type_display()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
# Check that termination types are compatible
|
|
||||||
if type_b not in COMPATIBLE_TERMINATION_TYPES.get(type_a):
|
|
||||||
raise ValidationError(
|
|
||||||
f"Incompatible termination types: {self.termination_a_type} and {self.termination_b_type}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that two connected RearPorts have the same number of positions (if both are >1)
|
|
||||||
if isinstance(self.termination_a, RearPort) and isinstance(self.termination_b, RearPort):
|
|
||||||
if self.termination_a.positions > 1 and self.termination_b.positions > 1:
|
|
||||||
if self.termination_a.positions != self.termination_b.positions:
|
|
||||||
raise ValidationError(
|
|
||||||
f"{self.termination_a} has {self.termination_a.positions} position(s) but "
|
|
||||||
f"{self.termination_b} has {self.termination_b.positions}. "
|
|
||||||
f"Both terminations must have the same number of positions (if greater than one)."
|
|
||||||
)
|
|
||||||
|
|
||||||
# A termination point cannot be connected to itself
|
|
||||||
if self.termination_a == self.termination_b:
|
|
||||||
raise ValidationError(f"Cannot connect {self.termination_a_type} to itself")
|
|
||||||
|
|
||||||
# A front port cannot be connected to its corresponding rear port
|
|
||||||
if (
|
|
||||||
type_a in ['frontport', 'rearport'] and
|
|
||||||
type_b in ['frontport', 'rearport'] and
|
|
||||||
(
|
|
||||||
getattr(self.termination_a, 'rear_port', None) == self.termination_b or
|
|
||||||
getattr(self.termination_b, 'rear_port', None) == self.termination_a
|
|
||||||
)
|
|
||||||
):
|
|
||||||
raise ValidationError("A front port cannot be connected to it corresponding rear port")
|
|
||||||
|
|
||||||
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
|
||||||
if isinstance(self.termination_a, CircuitTermination) and self.termination_a.provider_network is not None:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_a_id': "Circuit terminations attached to a provider network may not be cabled."
|
|
||||||
})
|
|
||||||
if isinstance(self.termination_b, CircuitTermination) and self.termination_b.provider_network is not None:
|
|
||||||
raise ValidationError({
|
|
||||||
'termination_b_id': "Circuit terminations attached to a provider network may not be cabled."
|
|
||||||
})
|
|
||||||
|
|
||||||
# Check for an existing Cable connected to either termination object
|
|
||||||
if self.termination_a.cable not in (None, self):
|
|
||||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
|
||||||
self.termination_a, self.termination_a.cable_id
|
|
||||||
))
|
|
||||||
if self.termination_b.cable not in (None, self):
|
|
||||||
raise ValidationError("{} already has a cable attached (#{})".format(
|
|
||||||
self.termination_b, self.termination_b.cable_id
|
|
||||||
))
|
|
||||||
|
|
||||||
# Validate length and length_unit
|
# Validate length and length_unit
|
||||||
if self.length is not None and not self.length_unit:
|
if self.length is not None and not self.length_unit:
|
||||||
raise ValidationError("Must specify a unit when setting a cable length")
|
raise ValidationError("Must specify a unit when setting a cable length")
|
||||||
elif self.length is None:
|
elif self.length is None:
|
||||||
self.length_unit = ''
|
self.length_unit = ''
|
||||||
|
|
||||||
|
a_terminations = [
|
||||||
|
CableTermination(cable=self, cable_end='A', termination=t)
|
||||||
|
for t in getattr(self, 'a_terminations', [])
|
||||||
|
]
|
||||||
|
b_terminations = [
|
||||||
|
CableTermination(cable=self, cable_end='B', termination=t)
|
||||||
|
for t in getattr(self, 'b_terminations', [])
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check that all termination objects for either end are of the same type
|
||||||
|
for terms in (a_terminations, b_terminations):
|
||||||
|
if len(terms) > 1 and not all(t.termination_type == terms[0].termination_type for t in terms[1:]):
|
||||||
|
raise ValidationError("Cannot connect different termination types to same end of cable.")
|
||||||
|
|
||||||
|
# Check that termination types are compatible
|
||||||
|
if a_terminations and b_terminations:
|
||||||
|
a_type = a_terminations[0].termination_type.model
|
||||||
|
b_type = b_terminations[0].termination_type.model
|
||||||
|
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
||||||
|
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
||||||
|
|
||||||
|
# Run clean() on any new CableTerminations
|
||||||
|
for cabletermination in [*a_terminations, *b_terminations]:
|
||||||
|
cabletermination.clean()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
_created = self.pk is None
|
||||||
|
|
||||||
# Store the given length (if any) in meters for use in database ordering
|
# Store the given length (if any) in meters for use in database ordering
|
||||||
if self.length and self.length_unit:
|
if self.length and self.length_unit:
|
||||||
@ -275,199 +150,454 @@ class Cable(NetBoxModel):
|
|||||||
else:
|
else:
|
||||||
self._abs_length = None
|
self._abs_length = None
|
||||||
|
|
||||||
# Store the parent Device for the A and B terminations (if applicable) to enable filtering
|
|
||||||
if hasattr(self.termination_a, 'device'):
|
|
||||||
self._termination_a_device = self.termination_a.device
|
|
||||||
if hasattr(self.termination_b, 'device'):
|
|
||||||
self._termination_b_device = self.termination_b.device
|
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
|
# Update the private pk used in __str__ in case this is a new object (i.e. just got its pk)
|
||||||
self._pk = self.pk
|
self._pk = self.pk
|
||||||
|
|
||||||
|
# Retrieve existing A/B terminations for the Cable
|
||||||
|
a_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='A')}
|
||||||
|
b_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='B')}
|
||||||
|
|
||||||
|
# Delete stale CableTerminations
|
||||||
|
if hasattr(self, 'a_terminations'):
|
||||||
|
for termination, ct in a_terminations.items():
|
||||||
|
if termination not in self.a_terminations:
|
||||||
|
ct.delete()
|
||||||
|
if hasattr(self, 'b_terminations'):
|
||||||
|
for termination, ct in b_terminations.items():
|
||||||
|
if termination not in self.b_terminations:
|
||||||
|
ct.delete()
|
||||||
|
|
||||||
|
# Save new CableTerminations (if any)
|
||||||
|
if hasattr(self, 'a_terminations'):
|
||||||
|
for termination in self.a_terminations:
|
||||||
|
if termination not in a_terminations:
|
||||||
|
CableTermination(cable=self, cable_end='A', termination=termination).save()
|
||||||
|
if hasattr(self, 'b_terminations'):
|
||||||
|
for termination in self.b_terminations:
|
||||||
|
if termination not in b_terminations:
|
||||||
|
CableTermination(cable=self, cable_end='B', termination=termination).save()
|
||||||
|
|
||||||
|
trace_paths.send(Cable, instance=self, created=_created)
|
||||||
|
|
||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return LinkStatusChoices.colors.get(self.status)
|
return LinkStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
def get_compatible_types(self):
|
def get_a_terminations(self):
|
||||||
|
# Query self.terminations.all() to leverage cached results
|
||||||
|
return [
|
||||||
|
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_b_terminations(self):
|
||||||
|
# Query self.terminations.all() to leverage cached results
|
||||||
|
return [
|
||||||
|
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class CableTermination(models.Model):
|
||||||
|
"""
|
||||||
|
A mapping between side A or B of a Cable and a terminating object (e.g. an Interface or CircuitTermination).
|
||||||
|
"""
|
||||||
|
cable = models.ForeignKey(
|
||||||
|
to='dcim.Cable',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='terminations'
|
||||||
|
)
|
||||||
|
cable_end = models.CharField(
|
||||||
|
max_length=1,
|
||||||
|
choices=CableEndChoices,
|
||||||
|
verbose_name='End'
|
||||||
|
)
|
||||||
|
termination_type = models.ForeignKey(
|
||||||
|
to=ContentType,
|
||||||
|
limit_choices_to=CABLE_TERMINATION_MODELS,
|
||||||
|
on_delete=models.PROTECT,
|
||||||
|
related_name='+'
|
||||||
|
)
|
||||||
|
termination_id = models.PositiveBigIntegerField()
|
||||||
|
termination = GenericForeignKey(
|
||||||
|
ct_field='termination_type',
|
||||||
|
fk_field='termination_id'
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cached associations to enable efficient filtering
|
||||||
|
_device = models.ForeignKey(
|
||||||
|
to='dcim.Device',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_rack = models.ForeignKey(
|
||||||
|
to='dcim.Rack',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_location = models.ForeignKey(
|
||||||
|
to='dcim.Location',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = RestrictedQuerySet.as_manager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('cable', 'cable_end', 'pk')
|
||||||
|
constraints = (
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=('termination_type', 'termination_id'),
|
||||||
|
name='dcim_cable_termination_unique_termination'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Cable {self.cable} to {self.termination}'
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
super().clean()
|
||||||
|
|
||||||
|
# Validate interface type (if applicable)
|
||||||
|
if self.termination_type.model == 'interface' and self.termination.type in NONCONNECTABLE_IFACE_TYPES:
|
||||||
|
raise ValidationError({
|
||||||
|
'termination': f'Cables cannot be terminated to {self.termination.get_type_display()} interfaces'
|
||||||
|
})
|
||||||
|
|
||||||
|
# A CircuitTermination attached to a ProviderNetwork cannot have a Cable
|
||||||
|
if self.termination_type.model == 'circuittermination' and self.termination.provider_network is not None:
|
||||||
|
raise ValidationError({
|
||||||
|
'termination': "Circuit terminations attached to a provider network may not be cabled."
|
||||||
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Cache objects associated with the terminating object (for filtering)
|
||||||
|
self.cache_related_objects()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
# Set the cable on the terminating object
|
||||||
|
termination_model = self.termination._meta.model
|
||||||
|
termination_model.objects.filter(pk=self.termination_id).update(
|
||||||
|
cable=self.cable,
|
||||||
|
cable_end=self.cable_end
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Delete the cable association on the terminating object
|
||||||
|
termination_model = self.termination._meta.model
|
||||||
|
termination_model.objects.filter(pk=self.termination_id).update(
|
||||||
|
cable=None,
|
||||||
|
cable_end=''
|
||||||
|
)
|
||||||
|
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def cache_related_objects(self):
|
||||||
"""
|
"""
|
||||||
Return all termination types compatible with termination A.
|
Cache objects related to the termination (e.g. device, rack, site) directly on the object to
|
||||||
|
enable efficient filtering.
|
||||||
"""
|
"""
|
||||||
if self.termination_a is None:
|
assert self.termination is not None
|
||||||
return
|
|
||||||
return COMPATIBLE_TERMINATION_TYPES[self.termination_a._meta.model_name]
|
# Device components
|
||||||
|
if getattr(self.termination, 'device', None):
|
||||||
|
self._device = self.termination.device
|
||||||
|
self._rack = self.termination.device.rack
|
||||||
|
self._location = self.termination.device.location
|
||||||
|
self._site = self.termination.device.site
|
||||||
|
|
||||||
|
# Power feeds
|
||||||
|
elif getattr(self.termination, 'rack', None):
|
||||||
|
self._rack = self.termination.rack
|
||||||
|
self._location = self.termination.rack.location
|
||||||
|
self._site = self.termination.rack.site
|
||||||
|
|
||||||
|
# Circuit terminations
|
||||||
|
elif getattr(self.termination, 'site', None):
|
||||||
|
self._site = self.termination.site
|
||||||
|
|
||||||
|
|
||||||
class CablePath(models.Model):
|
class CablePath(models.Model):
|
||||||
"""
|
"""
|
||||||
A CablePath instance represents the physical path from an origin to a destination, including all intermediate
|
A CablePath instance represents the physical path from a set of origin nodes to a set of destination nodes,
|
||||||
elements in the path. Every instance must specify an `origin`, whereas `destination` may be null (for paths which do
|
including all intermediate elements.
|
||||||
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` contains the ordered set of nodes, arranged in lists of (type, ID) tuples. (Each cable in the path can
|
||||||
path must be a Cable instance, followed by a pair of pass-through ports. For example, consider the following
|
terminate to one or more objects.) For example, consider the following
|
||||||
topology:
|
topology:
|
||||||
|
|
||||||
1 2 3
|
A B C
|
||||||
Interface A --- Front Port A | Rear Port A --- Rear Port B | Front Port B --- Interface B
|
Interface 1 --- Front Port 1 | Rear Port 1 --- Rear Port 2 | Front Port 3 --- Interface 2
|
||||||
|
Front Port 2 Front Port 4
|
||||||
|
|
||||||
This path would be expressed as:
|
This path would be expressed as:
|
||||||
|
|
||||||
CablePath(
|
CablePath(
|
||||||
origin = Interface A
|
path = [
|
||||||
destination = Interface B
|
[Interface 1],
|
||||||
path = [Cable 1, Front Port A, Rear Port A, Cable 2, Rear Port B, Front Port B, Cable 3]
|
[Cable A],
|
||||||
|
[Front Port 1, Front Port 2],
|
||||||
|
[Rear Port 1],
|
||||||
|
[Cable B],
|
||||||
|
[Rear Port 2],
|
||||||
|
[Front Port 3, Front Port 4],
|
||||||
|
[Cable C],
|
||||||
|
[Interface 2],
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
`is_active` is set to True only if 1) `destination` is not null, and 2) every Cable within the path has a status of
|
`is_active` is set to True only if every Cable within the path has a status of "connected". `is_complete` is True
|
||||||
"connected".
|
if the instance represents a complete end-to-end path from origin(s) to destination(s). `is_split` is True if the
|
||||||
|
path diverges across multiple cables.
|
||||||
|
|
||||||
|
`_nodes` retains a flattened list of all nodes within the path to enable simple filtering.
|
||||||
"""
|
"""
|
||||||
origin_type = models.ForeignKey(
|
path = models.JSONField(
|
||||||
to=ContentType,
|
default=list
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='+'
|
|
||||||
)
|
)
|
||||||
origin_id = models.PositiveBigIntegerField()
|
|
||||||
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.PositiveBigIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
destination = GenericForeignKey(
|
|
||||||
ct_field='destination_type',
|
|
||||||
fk_field='destination_id'
|
|
||||||
)
|
|
||||||
path = PathField()
|
|
||||||
is_active = models.BooleanField(
|
is_active = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
is_complete = models.BooleanField(
|
||||||
|
default=False
|
||||||
|
)
|
||||||
is_split = models.BooleanField(
|
is_split = models.BooleanField(
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
_nodes = PathField()
|
||||||
class Meta:
|
|
||||||
unique_together = ('origin_type', 'origin_id')
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
status = ' (active)' if self.is_active else ' (split)' if self.is_split else ''
|
return f"Path #{self.pk}: {len(self.path)} hops"
|
||||||
return f"Path #{self.pk}: {self.origin} to {self.destination} via {len(self.path)} nodes{status}"
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
# Save the flattened nodes list
|
||||||
|
self._nodes = list(itertools.chain(*self.path))
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
# Record a direct reference to this CablePath on its originating object
|
# Record a direct reference to this CablePath on its originating object(s)
|
||||||
model = self.origin._meta.model
|
origin_model = self.origin_type.model_class()
|
||||||
model.objects.filter(pk=self.origin.pk).update(_path=self.pk)
|
origin_ids = [decompile_path_node(node)[1] for node in self.path[0]]
|
||||||
|
origin_model.objects.filter(pk__in=origin_ids).update(_path=self.pk)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origin_type(self):
|
||||||
|
if self.path:
|
||||||
|
ct_id, _ = decompile_path_node(self.path[0][0])
|
||||||
|
return ContentType.objects.get_for_id(ct_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destination_type(self):
|
||||||
|
if self.is_complete:
|
||||||
|
ct_id, _ = decompile_path_node(self.path[-1][0])
|
||||||
|
return ContentType.objects.get_for_id(ct_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_objects(self):
|
||||||
|
"""
|
||||||
|
Cache and return the complete path as lists of objects, derived from their annotation within the path.
|
||||||
|
"""
|
||||||
|
if not hasattr(self, '_path_objects'):
|
||||||
|
self._path_objects = self._get_path()
|
||||||
|
return self._path_objects
|
||||||
|
|
||||||
|
@property
|
||||||
|
def origins(self):
|
||||||
|
"""
|
||||||
|
Return the list of originating objects.
|
||||||
|
"""
|
||||||
|
if hasattr(self, '_path_objects'):
|
||||||
|
return self.path_objects[0]
|
||||||
|
return [
|
||||||
|
path_node_to_object(node) for node in self.path[0]
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def destinations(self):
|
||||||
|
"""
|
||||||
|
Return the list of destination objects, if the path is complete.
|
||||||
|
"""
|
||||||
|
if not self.is_complete:
|
||||||
|
return []
|
||||||
|
if hasattr(self, '_path_objects'):
|
||||||
|
return self.path_objects[-1]
|
||||||
|
return [
|
||||||
|
path_node_to_object(node) for node in self.path[-1]
|
||||||
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def segment_count(self):
|
def segment_count(self):
|
||||||
total_length = 1 + len(self.path) + (1 if self.destination else 0)
|
return int(len(self.path) / 3)
|
||||||
return int(total_length / 3)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_origin(cls, origin):
|
def from_origin(cls, terminations):
|
||||||
"""
|
"""
|
||||||
Create a new CablePath instance as traced from the given path origin.
|
Create a new CablePath instance as traced from the given termination objects. These can be any object to which a
|
||||||
|
Cable or WirelessLink connects (interfaces, console ports, circuit termination, etc.). All terminations must be
|
||||||
|
of the same type and must belong to the same parent object.
|
||||||
"""
|
"""
|
||||||
from circuits.models import CircuitTermination
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
if origin is None or origin.link is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
destination = None
|
|
||||||
path = []
|
path = []
|
||||||
position_stack = []
|
position_stack = []
|
||||||
|
is_complete = False
|
||||||
is_active = True
|
is_active = True
|
||||||
is_split = False
|
is_split = False
|
||||||
|
|
||||||
node = origin
|
while terminations:
|
||||||
while node.link is not None:
|
|
||||||
if hasattr(node.link, 'status') and node.link.status != LinkStatusChoices.STATUS_CONNECTED:
|
# Terminations must all be of the same type
|
||||||
|
assert all(isinstance(t, type(terminations[0])) for t in terminations[1:])
|
||||||
|
|
||||||
|
# Step 1: Record the near-end termination object(s)
|
||||||
|
path.append([
|
||||||
|
object_to_path_node(t) for t in terminations
|
||||||
|
])
|
||||||
|
|
||||||
|
# Step 2: Determine the attached link (Cable or WirelessLink), if any
|
||||||
|
link = terminations[0].link
|
||||||
|
assert all(t.link == link for t in terminations[1:])
|
||||||
|
if link is None and len(path) == 1:
|
||||||
|
# If this is the start of the path and no link exists, return None
|
||||||
|
return None
|
||||||
|
elif link is None:
|
||||||
|
# Otherwise, halt the trace if no link exists
|
||||||
|
break
|
||||||
|
assert type(link) in (Cable, WirelessLink)
|
||||||
|
|
||||||
|
# Step 3: Record the link and update path status if not "connected"
|
||||||
|
path.append([object_to_path_node(link)])
|
||||||
|
if hasattr(link, 'status') and link.status != LinkStatusChoices.STATUS_CONNECTED:
|
||||||
is_active = False
|
is_active = False
|
||||||
|
|
||||||
# Follow the link to its far-end termination
|
# Step 4: Determine the far-end terminations
|
||||||
path.append(object_to_path_node(node.link))
|
if isinstance(link, Cable):
|
||||||
peer_termination = node.get_link_peer()
|
termination_type = ContentType.objects.get_for_model(terminations[0])
|
||||||
|
local_cable_terminations = CableTermination.objects.filter(
|
||||||
|
termination_type=termination_type,
|
||||||
|
termination_id__in=[t.pk for t in terminations]
|
||||||
|
)
|
||||||
|
# Terminations must all belong to same end of Cable
|
||||||
|
local_cable_end = local_cable_terminations[0].cable_end
|
||||||
|
assert all(ct.cable_end == local_cable_end for ct in local_cable_terminations[1:])
|
||||||
|
remote_cable_terminations = CableTermination.objects.filter(
|
||||||
|
cable=link,
|
||||||
|
cable_end='A' if local_cable_end == 'B' else 'B'
|
||||||
|
)
|
||||||
|
remote_terminations = [ct.termination for ct in remote_cable_terminations]
|
||||||
|
else:
|
||||||
|
# WirelessLink
|
||||||
|
remote_terminations = [link.interface_b] if link.interface_a is terminations[0] else [link.interface_a]
|
||||||
|
|
||||||
# Follow a FrontPort to its corresponding RearPort
|
# Step 5: Record the far-end termination object(s)
|
||||||
if isinstance(peer_termination, FrontPort):
|
path.append([
|
||||||
path.append(object_to_path_node(peer_termination))
|
object_to_path_node(t) for t in remote_terminations
|
||||||
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 (if any)
|
# Step 6: Determine the "next hop" terminations, if applicable
|
||||||
elif isinstance(peer_termination, RearPort):
|
if isinstance(remote_terminations[0], FrontPort):
|
||||||
path.append(object_to_path_node(peer_termination))
|
# Follow FrontPorts to their corresponding RearPorts
|
||||||
|
rear_ports = RearPort.objects.filter(
|
||||||
|
pk__in=[t.rear_port_id for t in remote_terminations]
|
||||||
|
)
|
||||||
|
if len(rear_ports) > 1:
|
||||||
|
assert all(rp.positions == 1 for rp in rear_ports)
|
||||||
|
elif rear_ports[0].positions > 1:
|
||||||
|
position_stack.append([fp.rear_port_position for fp in remote_terminations])
|
||||||
|
|
||||||
# Determine the peer FrontPort's position
|
terminations = rear_ports
|
||||||
if peer_termination.positions == 1:
|
|
||||||
position = 1
|
elif isinstance(remote_terminations[0], RearPort):
|
||||||
|
|
||||||
|
if len(remote_terminations) > 1 or remote_terminations[0].positions == 1:
|
||||||
|
front_ports = FrontPort.objects.filter(
|
||||||
|
rear_port_id__in=[rp.pk for rp in remote_terminations],
|
||||||
|
rear_port_position=1
|
||||||
|
)
|
||||||
elif position_stack:
|
elif position_stack:
|
||||||
position = position_stack.pop()
|
front_ports = FrontPort.objects.filter(
|
||||||
|
rear_port_id=remote_terminations[0].pk,
|
||||||
|
rear_port_position__in=position_stack.pop()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# No position indicated: path has split, so we stop at the RearPort
|
# No position indicated: path has split, so we stop at the RearPorts
|
||||||
is_split = True
|
is_split = True
|
||||||
break
|
break
|
||||||
|
|
||||||
try:
|
terminations = front_ports
|
||||||
node = FrontPort.objects.get(rear_port=peer_termination, rear_port_position=position)
|
|
||||||
path.append(object_to_path_node(node))
|
elif isinstance(remote_terminations[0], CircuitTermination):
|
||||||
except ObjectDoesNotExist:
|
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||||
# No corresponding FrontPort found for the RearPort
|
term_side = remote_terminations[0].term_side
|
||||||
|
assert all(ct.term_side == term_side for ct in remote_terminations[1:])
|
||||||
|
circuit_termination = CircuitTermination.objects.filter(
|
||||||
|
circuit=remote_terminations[0].circuit,
|
||||||
|
term_side='Z' if term_side == 'A' else 'A'
|
||||||
|
).first()
|
||||||
|
if circuit_termination is None:
|
||||||
|
break
|
||||||
|
elif circuit_termination.provider_network:
|
||||||
|
# Circuit terminates to a ProviderNetwork
|
||||||
|
path.extend([
|
||||||
|
[object_to_path_node(circuit_termination)],
|
||||||
|
[object_to_path_node(circuit_termination.provider_network)],
|
||||||
|
])
|
||||||
|
break
|
||||||
|
elif circuit_termination.site and not circuit_termination.cable:
|
||||||
|
# Circuit terminates to a Site
|
||||||
|
path.extend([
|
||||||
|
[object_to_path_node(circuit_termination)],
|
||||||
|
[object_to_path_node(circuit_termination.site)],
|
||||||
|
])
|
||||||
break
|
break
|
||||||
|
|
||||||
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
terminations = [circuit_termination]
|
||||||
elif isinstance(peer_termination, CircuitTermination):
|
|
||||||
path.append(object_to_path_node(peer_termination))
|
|
||||||
# Get peer CircuitTermination
|
|
||||||
node = peer_termination.get_peer_termination()
|
|
||||||
if node:
|
|
||||||
path.append(object_to_path_node(node))
|
|
||||||
if node.provider_network:
|
|
||||||
destination = node.provider_network
|
|
||||||
break
|
|
||||||
elif node.site and not node.cable:
|
|
||||||
destination = node.site
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# No peer CircuitTermination exists; halt the trace
|
|
||||||
break
|
|
||||||
|
|
||||||
# Anything else marks the end of the path
|
# Anything else marks the end of the path
|
||||||
else:
|
else:
|
||||||
destination = peer_termination
|
is_complete = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if destination is None:
|
|
||||||
is_active = False
|
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
origin=origin,
|
|
||||||
destination=destination,
|
|
||||||
path=path,
|
path=path,
|
||||||
|
is_complete=is_complete,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
is_split=is_split
|
is_split=is_split
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_path(self):
|
def retrace(self):
|
||||||
|
"""
|
||||||
|
Retrace the path from the currently-defined originating termination(s)
|
||||||
|
"""
|
||||||
|
_new = self.from_origin(self.origins)
|
||||||
|
if _new:
|
||||||
|
self.path = _new.path
|
||||||
|
self.is_complete = _new.is_complete
|
||||||
|
self.is_active = _new.is_active
|
||||||
|
self.is_split = _new.is_split
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
self.delete()
|
||||||
|
|
||||||
|
def _get_path(self):
|
||||||
"""
|
"""
|
||||||
Return the path as a list of prefetched objects.
|
Return the path as a list of prefetched objects.
|
||||||
"""
|
"""
|
||||||
# Compile a list of IDs to prefetch for each type of model in the path
|
# Compile a list of IDs to prefetch for each type of model in the path
|
||||||
to_prefetch = defaultdict(list)
|
to_prefetch = defaultdict(list)
|
||||||
for node in self.path:
|
for node in self._nodes:
|
||||||
ct_id, object_id = decompile_path_node(node)
|
ct_id, object_id = decompile_path_node(node)
|
||||||
to_prefetch[ct_id].append(object_id)
|
to_prefetch[ct_id].append(object_id)
|
||||||
|
|
||||||
@ -484,19 +614,15 @@ class CablePath(models.Model):
|
|||||||
|
|
||||||
# Replicate the path using the prefetched objects.
|
# Replicate the path using the prefetched objects.
|
||||||
path = []
|
path = []
|
||||||
for node in self.path:
|
for step in self.path:
|
||||||
ct_id, object_id = decompile_path_node(node)
|
nodes = []
|
||||||
path.append(prefetched[ct_id][object_id])
|
for node in step:
|
||||||
|
ct_id, object_id = decompile_path_node(node)
|
||||||
|
nodes.append(prefetched[ct_id][object_id])
|
||||||
|
path.append(nodes)
|
||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
@property
|
|
||||||
def last_node(self):
|
|
||||||
"""
|
|
||||||
Return either the destination or the last node within the path.
|
|
||||||
"""
|
|
||||||
return self.destination or path_node_to_object(self.path[-1])
|
|
||||||
|
|
||||||
def get_cable_ids(self):
|
def get_cable_ids(self):
|
||||||
"""
|
"""
|
||||||
Return all Cable IDs within the path.
|
Return all Cable IDs within the path.
|
||||||
@ -504,7 +630,7 @@ class CablePath(models.Model):
|
|||||||
cable_ct = ContentType.objects.get_for_model(Cable).pk
|
cable_ct = ContentType.objects.get_for_model(Cable).pk
|
||||||
cable_ids = []
|
cable_ids = []
|
||||||
|
|
||||||
for node in self.path:
|
for node in self._nodes:
|
||||||
ct, id = decompile_path_node(node)
|
ct, id = decompile_path_node(node)
|
||||||
if ct == cable_ct:
|
if ct == cable_ct:
|
||||||
cable_ids.append(id)
|
cable_ids.append(id)
|
||||||
@ -527,6 +653,6 @@ class CablePath(models.Model):
|
|||||||
"""
|
"""
|
||||||
Return all available next segments in a split cable path.
|
Return all available next segments in a split cable path.
|
||||||
"""
|
"""
|
||||||
rearport = path_node_to_object(self.path[-1])
|
rearport = path_node_to_object(self._nodes[-1])
|
||||||
|
|
||||||
return FrontPort.objects.filter(rear_port=rearport)
|
return FrontPort.objects.filter(rear_port=rearport)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
|
from functools import cached_property
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Sum
|
from django.db.models import Sum
|
||||||
@ -10,7 +12,6 @@ from mptt.models import MPTTModel, TreeForeignKey
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.fields import MACAddressField, WWNField
|
from dcim.fields import MACAddressField, WWNField
|
||||||
from dcim.svg import CableTraceSVG
|
|
||||||
from netbox.models import OrganizationalModel, NetBoxModel
|
from netbox.models import OrganizationalModel, NetBoxModel
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField, NaturalOrderingField
|
||||||
@ -23,7 +24,7 @@ from wireless.utils import get_channel_attr
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BaseInterface',
|
'BaseInterface',
|
||||||
'LinkTermination',
|
'CabledObjectModel',
|
||||||
'ConsolePort',
|
'ConsolePort',
|
||||||
'ConsoleServerPort',
|
'ConsoleServerPort',
|
||||||
'DeviceBay',
|
'DeviceBay',
|
||||||
@ -103,14 +104,10 @@ class ModularComponentModel(ComponentModel):
|
|||||||
abstract = True
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
class LinkTermination(models.Model):
|
class CabledObjectModel(models.Model):
|
||||||
"""
|
"""
|
||||||
An abstract model inherited by all models to which a Cable, WirelessLink, or other such link can terminate. Examples
|
An abstract model inherited by all models to which a Cable can terminate. Provides the `cable` and `cable_end`
|
||||||
include most device components, CircuitTerminations, and PowerFeeds. The `cable` and `wireless_link` fields
|
fields for caching cable associations, as well as `mark_connected` to designate "fake" connections.
|
||||||
reference the attached Cable or WirelessLink instance, respectively.
|
|
||||||
|
|
||||||
`_link_peer` is a GenericForeignKey used to cache the far-end LinkTermination on the local instance; this is a
|
|
||||||
shortcut to referencing `instance.link.termination_b`, for example.
|
|
||||||
"""
|
"""
|
||||||
cable = models.ForeignKey(
|
cable = models.ForeignKey(
|
||||||
to='dcim.Cable',
|
to='dcim.Cable',
|
||||||
@ -119,36 +116,21 @@ class LinkTermination(models.Model):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_link_peer_type = models.ForeignKey(
|
cable_end = models.CharField(
|
||||||
to=ContentType,
|
max_length=1,
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
choices=CableEndChoices
|
||||||
)
|
|
||||||
_link_peer_id = models.PositiveBigIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
_link_peer = GenericForeignKey(
|
|
||||||
ct_field='_link_peer_type',
|
|
||||||
fk_field='_link_peer_id'
|
|
||||||
)
|
)
|
||||||
mark_connected = models.BooleanField(
|
mark_connected = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
help_text="Treat as if a cable is connected"
|
help_text="Treat as if a cable is connected"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generic relations to Cable. These ensure that an attached Cable is deleted if the terminated object is deleted.
|
cable_terminations = GenericRelation(
|
||||||
_cabled_as_a = GenericRelation(
|
to='dcim.CableTermination',
|
||||||
to='dcim.Cable',
|
content_type_field='termination_type',
|
||||||
content_type_field='termination_a_type',
|
object_id_field='termination_id',
|
||||||
object_id_field='termination_a_id'
|
related_query_name='%(class)s',
|
||||||
)
|
|
||||||
_cabled_as_b = GenericRelation(
|
|
||||||
to='dcim.Cable',
|
|
||||||
content_type_field='termination_b_type',
|
|
||||||
object_id_field='termination_b_id'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -157,22 +139,19 @@ class LinkTermination(models.Model):
|
|||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
if self.mark_connected and self.cable_id:
|
if self.cable and not self.cable_end:
|
||||||
|
raise ValidationError({
|
||||||
|
"cable_end": "Must specify cable end (A or B) when attaching a cable."
|
||||||
|
})
|
||||||
|
if self.cable_end and not self.cable:
|
||||||
|
raise ValidationError({
|
||||||
|
"cable_end": "Cable end must not be set without a cable."
|
||||||
|
})
|
||||||
|
if self.mark_connected and self.cable:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
"mark_connected": "Cannot mark as connected with a cable attached."
|
"mark_connected": "Cannot mark as connected with a cable attached."
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_link_peer(self):
|
|
||||||
return self._link_peer
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _occupied(self):
|
|
||||||
return bool(self.mark_connected or self.cable_id)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def parent_object(self):
|
|
||||||
raise NotImplementedError("CableTermination models must implement parent_object()")
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def link(self):
|
def link(self):
|
||||||
"""
|
"""
|
||||||
@ -180,10 +159,31 @@ class LinkTermination(models.Model):
|
|||||||
"""
|
"""
|
||||||
return self.cable
|
return self.cable
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def link_peers(self):
|
||||||
|
if self.cable:
|
||||||
|
peers = self.cable.terminations.exclude(cable_end=self.cable_end).prefetch_related('termination')
|
||||||
|
return [peer.termination for peer in peers]
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _occupied(self):
|
||||||
|
return bool(self.mark_connected or self.cable_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_object(self):
|
||||||
|
raise NotImplementedError(f"{self.__class__.__name__} models must declare a parent_object property")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def opposite_cable_end(self):
|
||||||
|
if not self.cable_end:
|
||||||
|
return None
|
||||||
|
return CableEndChoices.SIDE_A if self.cable_end == CableEndChoices.SIDE_B else CableEndChoices.SIDE_B
|
||||||
|
|
||||||
|
|
||||||
class PathEndpoint(models.Model):
|
class PathEndpoint(models.Model):
|
||||||
"""
|
"""
|
||||||
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
|
An abstract model inherited by any CabledObjectModel subclass which represents the end of a CablePath; specifically,
|
||||||
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
|
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, and PowerFeed.
|
||||||
|
|
||||||
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
|
`_path` references the CablePath originating from this instance, if any. It is set or cleared by the receivers in
|
||||||
@ -206,50 +206,41 @@ class PathEndpoint(models.Model):
|
|||||||
origin = self
|
origin = self
|
||||||
path = []
|
path = []
|
||||||
|
|
||||||
# Construct the complete path
|
# Construct the complete path (including e.g. bridged interfaces)
|
||||||
while origin is not None:
|
while origin is not None:
|
||||||
|
|
||||||
if origin._path is None:
|
if origin._path is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
path.extend([origin, *origin._path.get_path()])
|
path.extend(origin._path.path_objects)
|
||||||
while (len(path) + 1) % 3:
|
while (len(path)) % 3:
|
||||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort)
|
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a non-connected FrontPort)
|
||||||
path.append(None)
|
# by inserting empty entries immediately prior to the path's destination node(s)
|
||||||
path.append(origin._path.destination)
|
path.append([])
|
||||||
|
|
||||||
# Check for bridge interface to continue the trace
|
# Check for a bridged relationship to continue the trace
|
||||||
origin = getattr(origin._path.destination, 'bridge', None)
|
destinations = origin._path.destinations
|
||||||
|
if len(destinations) == 1:
|
||||||
|
origin = getattr(destinations[0], 'bridge', None)
|
||||||
|
else:
|
||||||
|
origin = None
|
||||||
|
|
||||||
# Return the path as a list of three-tuples (A termination, cable, B termination)
|
# Return the path as a list of three-tuples (A termination(s), cable(s), B termination(s))
|
||||||
return list(zip(*[iter(path)] * 3))
|
return list(zip(*[iter(path)] * 3))
|
||||||
|
|
||||||
def get_trace_svg(self, base_url=None, width=None):
|
@cached_property
|
||||||
if width is not None:
|
def connected_endpoints(self):
|
||||||
trace = CableTraceSVG(self, base_url=base_url, width=width)
|
|
||||||
else:
|
|
||||||
trace = CableTraceSVG(self, base_url=base_url)
|
|
||||||
return trace.render()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def path(self):
|
|
||||||
return self._path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connected_endpoint(self):
|
|
||||||
"""
|
"""
|
||||||
Caching accessor for the attached CablePath's destination (if any)
|
Caching accessor for the attached CablePath's destination (if any)
|
||||||
"""
|
"""
|
||||||
if not hasattr(self, '_connected_endpoint'):
|
return self._path.destinations if self._path else []
|
||||||
self._connected_endpoint = self._path.destination if self._path else None
|
|
||||||
return self._connected_endpoint
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Console components
|
# Console components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
|
||||||
"""
|
"""
|
||||||
@ -276,7 +267,7 @@ class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
|
||||||
"""
|
"""
|
||||||
@ -307,7 +298,7 @@ class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
# Power components
|
# Power components
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
|
||||||
"""
|
"""
|
||||||
@ -348,36 +339,57 @@ class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def get_downstream_powerports(self, leg=None):
|
||||||
|
"""
|
||||||
|
Return a queryset of all PowerPorts connected via cable to a child PowerOutlet. For example, in the topology
|
||||||
|
below, PP1.get_downstream_powerports() would return PP2-4.
|
||||||
|
|
||||||
|
---- PO1 <---> PP2
|
||||||
|
/
|
||||||
|
PP1 ------- PO2 <---> PP3
|
||||||
|
\
|
||||||
|
---- PO3 <---> PP4
|
||||||
|
|
||||||
|
"""
|
||||||
|
poweroutlets = self.poweroutlets.filter(cable__isnull=False)
|
||||||
|
if leg:
|
||||||
|
poweroutlets = poweroutlets.filter(feed_leg=leg)
|
||||||
|
if not poweroutlets:
|
||||||
|
return PowerPort.objects.none()
|
||||||
|
|
||||||
|
q = Q()
|
||||||
|
for poweroutlet in poweroutlets:
|
||||||
|
q |= Q(
|
||||||
|
cable=poweroutlet.cable,
|
||||||
|
cable_end=poweroutlet.opposite_cable_end
|
||||||
|
)
|
||||||
|
|
||||||
|
return PowerPort.objects.filter(q)
|
||||||
|
|
||||||
def get_power_draw(self):
|
def get_power_draw(self):
|
||||||
"""
|
"""
|
||||||
Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
|
Return the allocated and maximum power draw (in VA) and child PowerOutlet count for this PowerPort.
|
||||||
"""
|
"""
|
||||||
|
from dcim.models import PowerFeed
|
||||||
|
|
||||||
# Calculate aggregate draw of all child power outlets if no numbers have been defined manually
|
# Calculate aggregate draw of all child power outlets if no numbers have been defined manually
|
||||||
if self.allocated_draw is None and self.maximum_draw is None:
|
if self.allocated_draw is None and self.maximum_draw is None:
|
||||||
poweroutlet_ct = ContentType.objects.get_for_model(PowerOutlet)
|
utilization = self.get_downstream_powerports().aggregate(
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self).values_list('pk', flat=True)
|
|
||||||
utilization = PowerPort.objects.filter(
|
|
||||||
_link_peer_type=poweroutlet_ct,
|
|
||||||
_link_peer_id__in=outlet_ids
|
|
||||||
).aggregate(
|
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
maximum_draw_total=Sum('maximum_draw'),
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
allocated_draw_total=Sum('allocated_draw'),
|
||||||
)
|
)
|
||||||
ret = {
|
ret = {
|
||||||
'allocated': utilization['allocated_draw_total'] or 0,
|
'allocated': utilization['allocated_draw_total'] or 0,
|
||||||
'maximum': utilization['maximum_draw_total'] or 0,
|
'maximum': utilization['maximum_draw_total'] or 0,
|
||||||
'outlet_count': len(outlet_ids),
|
'outlet_count': self.poweroutlets.count(),
|
||||||
'legs': [],
|
'legs': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate per-leg aggregates for three-phase feeds
|
# Calculate per-leg aggregates for three-phase power feeds
|
||||||
if getattr(self._link_peer, 'phase', None) == PowerFeedPhaseChoices.PHASE_3PHASE:
|
if len(self.link_peers) == 1 and isinstance(self.link_peers[0], PowerFeed) and \
|
||||||
|
self.link_peers[0].phase == PowerFeedPhaseChoices.PHASE_3PHASE:
|
||||||
for leg, leg_name in PowerOutletFeedLegChoices:
|
for leg, leg_name in PowerOutletFeedLegChoices:
|
||||||
outlet_ids = PowerOutlet.objects.filter(power_port=self, feed_leg=leg).values_list('pk', flat=True)
|
utilization = self.get_downstream_powerports(leg=leg).aggregate(
|
||||||
utilization = PowerPort.objects.filter(
|
|
||||||
_link_peer_type=poweroutlet_ct,
|
|
||||||
_link_peer_id__in=outlet_ids
|
|
||||||
).aggregate(
|
|
||||||
maximum_draw_total=Sum('maximum_draw'),
|
maximum_draw_total=Sum('maximum_draw'),
|
||||||
allocated_draw_total=Sum('allocated_draw'),
|
allocated_draw_total=Sum('allocated_draw'),
|
||||||
)
|
)
|
||||||
@ -385,7 +397,7 @@ class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
'name': leg_name,
|
'name': leg_name,
|
||||||
'allocated': utilization['allocated_draw_total'] or 0,
|
'allocated': utilization['allocated_draw_total'] or 0,
|
||||||
'maximum': utilization['maximum_draw_total'] or 0,
|
'maximum': utilization['maximum_draw_total'] or 0,
|
||||||
'outlet_count': len(outlet_ids),
|
'outlet_count': self.poweroutlets.filter(feed_leg=leg).count(),
|
||||||
})
|
})
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@ -394,12 +406,12 @@ class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
return {
|
return {
|
||||||
'allocated': self.allocated_draw or 0,
|
'allocated': self.allocated_draw or 0,
|
||||||
'maximum': self.maximum_draw or 0,
|
'maximum': self.maximum_draw or 0,
|
||||||
'outlet_count': PowerOutlet.objects.filter(power_port=self).count(),
|
'outlet_count': self.poweroutlets.count(),
|
||||||
'legs': [],
|
'legs': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
|
class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
A physical power outlet (output) within a Device which provides power to a PowerPort.
|
||||||
"""
|
"""
|
||||||
@ -437,9 +449,7 @@ class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
|
|||||||
|
|
||||||
# Validate power port assignment
|
# Validate power port assignment
|
||||||
if self.power_port and self.power_port.device != self.device:
|
if self.power_port and self.power_port.device != self.device:
|
||||||
raise ValidationError(
|
raise ValidationError(f"Parent power port ({self.power_port}) must belong to the same device")
|
||||||
"Parent power port ({}) must belong to the same device".format(self.power_port)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@ -513,7 +523,7 @@ class BaseInterface(models.Model):
|
|||||||
return self.fhrp_group_assignments.count()
|
return self.fhrp_group_assignments.count()
|
||||||
|
|
||||||
|
|
||||||
class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
|
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint):
|
||||||
"""
|
"""
|
||||||
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
|
||||||
"""
|
"""
|
||||||
@ -829,6 +839,18 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
def link(self):
|
def link(self):
|
||||||
return self.cable or self.wireless_link
|
return self.cable or self.wireless_link
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def link_peers(self):
|
||||||
|
if self.cable:
|
||||||
|
return super().link_peers
|
||||||
|
if self.wireless_link:
|
||||||
|
# Return the opposite side of the attached wireless link
|
||||||
|
if self.wireless_link.interface_a == self:
|
||||||
|
return [self.wireless_link.interface_b]
|
||||||
|
else:
|
||||||
|
return [self.wireless_link.interface_a]
|
||||||
|
return []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def l2vpn_termination(self):
|
def l2vpn_termination(self):
|
||||||
return self.l2vpn_terminations.first()
|
return self.l2vpn_terminations.first()
|
||||||
@ -838,7 +860,7 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
|
|||||||
# Pass-through ports
|
# Pass-through ports
|
||||||
#
|
#
|
||||||
|
|
||||||
class FrontPort(ModularComponentModel, LinkTermination):
|
class FrontPort(ModularComponentModel, CabledObjectModel):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the front of a Device.
|
A pass-through port on the front of a Device.
|
||||||
"""
|
"""
|
||||||
@ -891,7 +913,7 @@ class FrontPort(ModularComponentModel, LinkTermination):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
class RearPort(ModularComponentModel, LinkTermination):
|
class RearPort(ModularComponentModel, CabledObjectModel):
|
||||||
"""
|
"""
|
||||||
A pass-through port on the rear of a Device.
|
A pass-through port on the rear of a Device.
|
||||||
"""
|
"""
|
||||||
|
@ -9,7 +9,7 @@ from dcim.constants import *
|
|||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from netbox.models import NetBoxModel
|
from netbox.models import NetBoxModel
|
||||||
from utilities.validators import ExclusionValidator
|
from utilities.validators import ExclusionValidator
|
||||||
from .device_components import LinkTermination, PathEndpoint
|
from .device_components import CabledObjectModel, PathEndpoint
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'PowerFeed',
|
'PowerFeed',
|
||||||
@ -67,7 +67,7 @@ class PowerPanel(NetBoxModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerFeed(NetBoxModel, PathEndpoint, LinkTermination):
|
class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel):
|
||||||
"""
|
"""
|
||||||
An electrical circuit delivered from a PowerPanel.
|
An electrical circuit delivered from a PowerPanel.
|
||||||
"""
|
"""
|
||||||
|
@ -432,17 +432,17 @@ class Rack(NetBoxModel):
|
|||||||
if not available_power_total:
|
if not available_power_total:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
pf_powerports = PowerPort.objects.filter(
|
powerports = []
|
||||||
_link_peer_type=ContentType.objects.get_for_model(PowerFeed),
|
for powerfeed in powerfeeds:
|
||||||
_link_peer_id__in=powerfeeds.values_list('id', flat=True)
|
powerports.extend([
|
||||||
)
|
peer for peer in powerfeed.link_peers if isinstance(peer, PowerPort)
|
||||||
poweroutlets = PowerOutlet.objects.filter(power_port_id__in=pf_powerports)
|
])
|
||||||
allocated_draw_total = PowerPort.objects.filter(
|
|
||||||
_link_peer_type=ContentType.objects.get_for_model(PowerOutlet),
|
|
||||||
_link_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)
|
allocated_draw = sum([
|
||||||
|
powerport.get_power_draw()['allocated'] for powerport in powerports
|
||||||
|
])
|
||||||
|
|
||||||
|
return int(allocated_draw / available_power_total * 100)
|
||||||
|
|
||||||
|
|
||||||
class RackReservation(NetBoxModel):
|
class RackReservation(NetBoxModel):
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .choices import LinkStatusChoices
|
from .choices import CableEndChoices, LinkStatusChoices
|
||||||
from .models import Cable, CablePath, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
from .models import Cable, CablePath, CableTermination, Device, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis
|
||||||
|
from .models.cables import trace_paths
|
||||||
from .utils import create_cablepath, rebuild_paths
|
from .utils import create_cablepath, rebuild_paths
|
||||||
|
|
||||||
|
|
||||||
@ -68,73 +68,55 @@ def clear_virtualchassis_members(instance, **kwargs):
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
|
@receiver(trace_paths, sender=Cable)
|
||||||
@receiver(post_save, sender=Cable)
|
|
||||||
def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
When a Cable is saved, check for and update its two connected endpoints
|
When a Cable is saved with new terminations, retrace any affected cable paths.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.dcim.cable')
|
logger = logging.getLogger('netbox.dcim.cable')
|
||||||
if raw:
|
if raw:
|
||||||
logger.debug(f"Skipping endpoint updates for imported cable {instance}")
|
logger.debug(f"Skipping endpoint updates for imported cable {instance}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Cache the Cable on its two termination points
|
# Update cable paths if new terminations have been set
|
||||||
if instance.termination_a.cable != instance:
|
if hasattr(instance, 'a_terminations') or hasattr(instance, 'b_terminations'):
|
||||||
logger.debug(f"Updating termination A for cable {instance}")
|
a_terminations = []
|
||||||
instance.termination_a.cable = instance
|
b_terminations = []
|
||||||
instance.termination_a._link_peer = instance.termination_b
|
for t in instance.terminations.all():
|
||||||
instance.termination_a.save()
|
if t.cable_end == CableEndChoices.SIDE_A:
|
||||||
if instance.termination_b.cable != instance:
|
a_terminations.append(t.termination)
|
||||||
logger.debug(f"Updating termination B for cable {instance}")
|
|
||||||
instance.termination_b.cable = instance
|
|
||||||
instance.termination_b._link_peer = instance.termination_a
|
|
||||||
instance.termination_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:
|
else:
|
||||||
rebuild_paths(termination)
|
b_terminations.append(t.termination)
|
||||||
|
for nodes in [a_terminations, b_terminations]:
|
||||||
|
# Examine type of first termination to determine object type (all must be the same)
|
||||||
|
if not nodes:
|
||||||
|
continue
|
||||||
|
if isinstance(nodes[0], PathEndpoint):
|
||||||
|
create_cablepath(nodes)
|
||||||
|
else:
|
||||||
|
rebuild_paths(nodes)
|
||||||
|
|
||||||
|
# Update status of CablePaths if Cable status has been changed
|
||||||
elif instance.status != instance._orig_status:
|
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 != LinkStatusChoices.STATUS_CONNECTED:
|
if instance.status != LinkStatusChoices.STATUS_CONNECTED:
|
||||||
CablePath.objects.filter(path__contains=instance).update(is_active=False)
|
CablePath.objects.filter(_nodes__contains=instance).update(is_active=False)
|
||||||
else:
|
else:
|
||||||
rebuild_paths(instance)
|
rebuild_paths([instance])
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Cable)
|
@receiver(post_delete, sender=Cable)
|
||||||
|
def retrace_cable_paths(instance, **kwargs):
|
||||||
|
"""
|
||||||
|
When a Cable is deleted, check for and update its connected endpoints
|
||||||
|
"""
|
||||||
|
for cablepath in CablePath.objects.filter(_nodes__contains=instance):
|
||||||
|
cablepath.retrace()
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_delete, sender=CableTermination)
|
||||||
def nullify_connected_endpoints(instance, **kwargs):
|
def nullify_connected_endpoints(instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
When a Cable is deleted, check for and update its two connected endpoints
|
Disassociate the Cable from the termination object.
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger('netbox.dcim.cable')
|
model = instance.termination_type.model_class()
|
||||||
|
model.objects.filter(pk=instance.termination_id).update(cable=None, cable_end='')
|
||||||
# Disassociate the Cable from its termination points
|
|
||||||
if instance.termination_a is not None:
|
|
||||||
logger.debug(f"Nullifying termination A for cable {instance}")
|
|
||||||
model = instance.termination_a._meta.model
|
|
||||||
model.objects.filter(pk=instance.termination_a.pk).update(_link_peer_type=None, _link_peer_id=None)
|
|
||||||
if instance.termination_b is not None:
|
|
||||||
logger.debug(f"Nullifying termination B for cable {instance}")
|
|
||||||
model = instance.termination_b._meta.model
|
|
||||||
model.objects.filter(pk=instance.termination_b.pk).update(_link_peer_type=None, _link_peer_id=None)
|
|
||||||
|
|
||||||
# Delete and retrace any dependent cable paths
|
|
||||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
|
||||||
cp = CablePath.from_origin(cablepath.origin)
|
|
||||||
if cp:
|
|
||||||
CablePath.objects.filter(pk=cablepath.pk).update(
|
|
||||||
path=cp.path,
|
|
||||||
destination_type=ContentType.objects.get_for_model(cp.destination) if cp.destination else None,
|
|
||||||
destination_id=cp.destination.pk if cp.destination else None,
|
|
||||||
is_active=cp.is_active,
|
|
||||||
is_split=cp.is_split
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cablepath.delete()
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import svgwrite
|
import svgwrite
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from svgwrite.container import Group, Hyperlink
|
from svgwrite.container import Group, Hyperlink
|
||||||
from svgwrite.shapes import Line, Rect
|
from svgwrite.shapes import Line, Polyline, Rect
|
||||||
from svgwrite.text import Text
|
from svgwrite.text import Text
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
|
||||||
from utilities.utils import foreground_color
|
from utilities.utils import foreground_color
|
||||||
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableTraceSVG',
|
'CableTraceSVG',
|
||||||
)
|
)
|
||||||
@ -15,6 +17,95 @@ __all__ = (
|
|||||||
OFFSET = 0.5
|
OFFSET = 0.5
|
||||||
PADDING = 10
|
PADDING = 10
|
||||||
LINE_HEIGHT = 20
|
LINE_HEIGHT = 20
|
||||||
|
FANOUT_HEIGHT = 35
|
||||||
|
FANOUT_LEG_HEIGHT = 15
|
||||||
|
|
||||||
|
|
||||||
|
class Node(Hyperlink):
|
||||||
|
"""
|
||||||
|
Create a node to be represented in the SVG document as a rectangular box with a hyperlink.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
position: (x, y) coordinates of the box's top left corner
|
||||||
|
width: Box width
|
||||||
|
url: Hyperlink URL
|
||||||
|
color: Box fill color (RRGGBB format)
|
||||||
|
labels: An iterable of text strings. Each label will render on a new line within the box.
|
||||||
|
radius: Box corner radius, for rounded corners (default: 10)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, position, width, url, color, labels, radius=10, **extra):
|
||||||
|
super(Node, self).__init__(href=url, target='_blank', **extra)
|
||||||
|
|
||||||
|
x, y = position
|
||||||
|
|
||||||
|
# Add the box
|
||||||
|
dimensions = (width - 2, PADDING + LINE_HEIGHT * len(labels) + PADDING)
|
||||||
|
box = Rect((x + OFFSET, y), dimensions, rx=radius, class_='parent-object', style=f'fill: #{color}')
|
||||||
|
self.add(box)
|
||||||
|
|
||||||
|
cursor = y + PADDING
|
||||||
|
|
||||||
|
# Add text label(s)
|
||||||
|
for i, label in enumerate(labels):
|
||||||
|
cursor += LINE_HEIGHT
|
||||||
|
text_coords = (x + width / 2, cursor - LINE_HEIGHT / 2)
|
||||||
|
text_color = f'#{foreground_color(color, dark="303030")}'
|
||||||
|
text = Text(label, insert=text_coords, fill=text_color, class_='bold' if not i else [])
|
||||||
|
self.add(text)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def box(self):
|
||||||
|
return self.elements[0] if self.elements else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top_center(self):
|
||||||
|
return self.box['x'] + self.box['width'] / 2, self.box['y']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bottom_center(self):
|
||||||
|
return self.box['x'] + self.box['width'] / 2, self.box['y'] + self.box['height']
|
||||||
|
|
||||||
|
|
||||||
|
class Connector(Group):
|
||||||
|
"""
|
||||||
|
Return an SVG group containing a line element and text labels representing a Cable.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
color: Cable (line) color
|
||||||
|
url: Hyperlink URL
|
||||||
|
labels: Iterable of text labels
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, start, url, color, labels=[], **extra):
|
||||||
|
super().__init__(class_='connector', **extra)
|
||||||
|
|
||||||
|
self.start = start
|
||||||
|
self.height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||||
|
self.end = (start[0], start[1] + self.height)
|
||||||
|
self.color = color or '000000'
|
||||||
|
|
||||||
|
# Draw a "shadow" line to give the cable a border
|
||||||
|
cable_shadow = Line(start=self.start, end=self.end, class_='cable-shadow')
|
||||||
|
self.add(cable_shadow)
|
||||||
|
|
||||||
|
# Draw the cable
|
||||||
|
cable = Line(start=self.start, end=self.end, style=f'stroke: #{self.color}')
|
||||||
|
self.add(cable)
|
||||||
|
|
||||||
|
# Add link
|
||||||
|
link = Hyperlink(href=url, target='_blank')
|
||||||
|
|
||||||
|
# Add text label(s)
|
||||||
|
cursor = start[1]
|
||||||
|
cursor += PADDING * 2
|
||||||
|
for i, label in enumerate(labels):
|
||||||
|
cursor += LINE_HEIGHT
|
||||||
|
text_coords = (start[0] + PADDING * 2, cursor - LINE_HEIGHT / 2)
|
||||||
|
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
||||||
|
link.add(text)
|
||||||
|
|
||||||
|
self.add(link)
|
||||||
|
|
||||||
|
|
||||||
class CableTraceSVG:
|
class CableTraceSVG:
|
||||||
@ -25,7 +116,7 @@ class CableTraceSVG:
|
|||||||
:param width: Width of the generated image (in pixels)
|
:param width: Width of the generated image (in pixels)
|
||||||
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
|
:param base_url: Base URL for links within the SVG document. If none, links will be relative.
|
||||||
"""
|
"""
|
||||||
def __init__(self, origin, width=400, base_url=None):
|
def __init__(self, origin, width=CABLE_TRACE_SVG_DEFAULT_WIDTH, base_url=None):
|
||||||
self.origin = origin
|
self.origin = origin
|
||||||
self.width = width
|
self.width = width
|
||||||
self.base_url = base_url.rstrip('/') if base_url is not None else ''
|
self.base_url = base_url.rstrip('/') if base_url is not None else ''
|
||||||
@ -34,6 +125,11 @@ class CableTraceSVG:
|
|||||||
# Center edges on pixels to render sharp borders
|
# Center edges on pixels to render sharp borders
|
||||||
self.cursor = OFFSET
|
self.cursor = OFFSET
|
||||||
|
|
||||||
|
# Prep elements lists
|
||||||
|
self.parent_objects = []
|
||||||
|
self.terminations = []
|
||||||
|
self.connectors = []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def center(self):
|
def center(self):
|
||||||
return self.width / 2
|
return self.width / 2
|
||||||
@ -78,95 +174,103 @@ class CableTraceSVG:
|
|||||||
# Other parent object
|
# Other parent object
|
||||||
return 'e0e0e0'
|
return 'e0e0e0'
|
||||||
|
|
||||||
def _draw_box(self, width, color, url, labels, y_indent=0, padding_multiplier=1, radius=10):
|
def draw_parent_objects(self, obj_list):
|
||||||
"""
|
"""
|
||||||
Return an SVG Link element containing a Rect and one or more text labels representing a
|
Draw a set of parent objects.
|
||||||
parent object or cable termination point.
|
|
||||||
|
|
||||||
:param width: Box width
|
|
||||||
:param color: Box fill color
|
|
||||||
:param url: Hyperlink URL
|
|
||||||
:param labels: Iterable of text labels
|
|
||||||
:param y_indent: Vertical indent (for overlapping other boxes) (default: 0)
|
|
||||||
:param padding_multiplier: Add extra vertical padding (default: 1)
|
|
||||||
:param radius: Box corner radius (default: 10)
|
|
||||||
"""
|
"""
|
||||||
self.cursor -= y_indent
|
width = self.width / len(obj_list)
|
||||||
|
for i, obj in enumerate(obj_list):
|
||||||
|
node = Node(
|
||||||
|
position=(i * width, self.cursor),
|
||||||
|
width=width,
|
||||||
|
url=f'{self.base_url}{obj.get_absolute_url()}',
|
||||||
|
color=self._get_color(obj),
|
||||||
|
labels=self._get_labels(obj)
|
||||||
|
)
|
||||||
|
self.parent_objects.append(node)
|
||||||
|
if i + 1 == len(obj_list):
|
||||||
|
self.cursor += node.box['height']
|
||||||
|
|
||||||
# Create a hyperlink
|
def draw_terminations(self, terminations):
|
||||||
link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
|
"""
|
||||||
|
Draw a row of terminating objects (e.g. interfaces), all of which are attached to the same end of a cable.
|
||||||
|
"""
|
||||||
|
nodes = []
|
||||||
|
nodes_height = 0
|
||||||
|
width = self.width / len(terminations)
|
||||||
|
|
||||||
# Add the box
|
for i, term in enumerate(terminations):
|
||||||
position = (
|
node = Node(
|
||||||
OFFSET + (self.width - width) / 2,
|
position=(i * width, self.cursor),
|
||||||
self.cursor
|
width=width,
|
||||||
|
url=f'{self.base_url}{term.get_absolute_url()}',
|
||||||
|
color=self._get_color(term),
|
||||||
|
labels=self._get_labels(term),
|
||||||
|
radius=5
|
||||||
|
)
|
||||||
|
nodes_height = max(nodes_height, node.box['height'])
|
||||||
|
nodes.append(node)
|
||||||
|
|
||||||
|
self.cursor += nodes_height
|
||||||
|
self.terminations.extend(nodes)
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
def draw_fanin(self, node, connector):
|
||||||
|
points = (
|
||||||
|
node.bottom_center,
|
||||||
|
(node.bottom_center[0], node.bottom_center[1] + FANOUT_LEG_HEIGHT),
|
||||||
|
connector.start,
|
||||||
)
|
)
|
||||||
height = PADDING * padding_multiplier \
|
self.connectors.extend((
|
||||||
+ LINE_HEIGHT * len(labels) \
|
Polyline(points=points, class_='cable-shadow'),
|
||||||
+ PADDING * padding_multiplier
|
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
||||||
box = Rect(position, (width - 2, height), rx=radius, class_='parent-object', style=f'fill: #{color}')
|
))
|
||||||
link.add(box)
|
|
||||||
self.cursor += PADDING * padding_multiplier
|
|
||||||
|
|
||||||
# Add text label(s)
|
def draw_fanout(self, node, connector):
|
||||||
for i, label in enumerate(labels):
|
points = (
|
||||||
self.cursor += LINE_HEIGHT
|
connector.end,
|
||||||
text_coords = (self.center, self.cursor - LINE_HEIGHT / 2)
|
(node.top_center[0], node.top_center[1] - FANOUT_LEG_HEIGHT),
|
||||||
text_color = f'#{foreground_color(color, dark="303030")}'
|
node.top_center,
|
||||||
text = Text(label, insert=text_coords, fill=text_color, class_='bold' if not i else [])
|
)
|
||||||
link.add(text)
|
self.connectors.extend((
|
||||||
|
Polyline(points=points, class_='cable-shadow'),
|
||||||
|
Polyline(points=points, style=f'stroke: #{connector.color}'),
|
||||||
|
))
|
||||||
|
|
||||||
self.cursor += PADDING * padding_multiplier
|
def draw_cable(self, cable):
|
||||||
|
labels = [
|
||||||
|
f'Cable {cable}',
|
||||||
|
cable.get_status_display()
|
||||||
|
]
|
||||||
|
if cable.type:
|
||||||
|
labels.append(cable.get_type_display())
|
||||||
|
if cable.length and cable.length_unit:
|
||||||
|
labels.append(f'{cable.length} {cable.get_length_unit_display()}')
|
||||||
|
connector = Connector(
|
||||||
|
start=(self.center + OFFSET, self.cursor),
|
||||||
|
color=cable.color or '000000',
|
||||||
|
url=f'{self.base_url}{cable.get_absolute_url()}',
|
||||||
|
labels=labels
|
||||||
|
)
|
||||||
|
|
||||||
return link
|
self.cursor += connector.height
|
||||||
|
|
||||||
def _draw_cable(self, color, url, labels):
|
return connector
|
||||||
"""
|
|
||||||
Return an SVG group containing a line element and text labels representing a Cable.
|
|
||||||
|
|
||||||
:param color: Cable (line) color
|
def draw_wirelesslink(self, wirelesslink):
|
||||||
:param url: Hyperlink URL
|
|
||||||
:param labels: Iterable of text labels
|
|
||||||
"""
|
|
||||||
group = Group(class_='connector')
|
|
||||||
|
|
||||||
# Draw a "shadow" line to give the cable a border
|
|
||||||
start = (OFFSET + self.center, self.cursor)
|
|
||||||
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
|
||||||
end = (start[0], start[1] + height)
|
|
||||||
cable_shadow = Line(start=start, end=end, class_='cable-shadow')
|
|
||||||
group.add(cable_shadow)
|
|
||||||
|
|
||||||
# Draw the cable
|
|
||||||
cable = Line(start=start, end=end, style=f'stroke: #{color}')
|
|
||||||
group.add(cable)
|
|
||||||
|
|
||||||
self.cursor += PADDING * 2
|
|
||||||
|
|
||||||
# Add link
|
|
||||||
link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
|
|
||||||
|
|
||||||
# Add text label(s)
|
|
||||||
for i, label in enumerate(labels):
|
|
||||||
self.cursor += LINE_HEIGHT
|
|
||||||
text_coords = (self.center + PADDING * 2, self.cursor - LINE_HEIGHT / 2)
|
|
||||||
text = Text(label, insert=text_coords, class_='bold' if not i else [])
|
|
||||||
link.add(text)
|
|
||||||
|
|
||||||
group.add(link)
|
|
||||||
self.cursor += PADDING * 2
|
|
||||||
|
|
||||||
return group
|
|
||||||
|
|
||||||
def _draw_wirelesslink(self, url, labels):
|
|
||||||
"""
|
"""
|
||||||
Draw a line with labels representing a WirelessLink.
|
Draw a line with labels representing a WirelessLink.
|
||||||
|
|
||||||
:param url: Hyperlink URL
|
|
||||||
:param labels: Iterable of text labels
|
|
||||||
"""
|
"""
|
||||||
group = Group(class_='connector')
|
group = Group(class_='connector')
|
||||||
|
|
||||||
|
labels = [
|
||||||
|
f'Wireless link {wirelesslink}',
|
||||||
|
wirelesslink.get_status_display()
|
||||||
|
]
|
||||||
|
if wirelesslink.ssid:
|
||||||
|
labels.append(wirelesslink.ssid)
|
||||||
|
|
||||||
# Draw the wireless link
|
# Draw the wireless link
|
||||||
start = (OFFSET + self.center, self.cursor)
|
start = (OFFSET + self.center, self.cursor)
|
||||||
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
height = PADDING * 2 + LINE_HEIGHT * len(labels) + PADDING * 2
|
||||||
@ -177,7 +281,7 @@ class CableTraceSVG:
|
|||||||
self.cursor += PADDING * 2
|
self.cursor += PADDING * 2
|
||||||
|
|
||||||
# Add link
|
# Add link
|
||||||
link = Hyperlink(href=f'{self.base_url}{url}', target='_blank')
|
link = Hyperlink(href=f'{self.base_url}{wirelesslink.get_absolute_url()}', target='_blank')
|
||||||
|
|
||||||
# Add text label(s)
|
# Add text label(s)
|
||||||
for i, label in enumerate(labels):
|
for i, label in enumerate(labels):
|
||||||
@ -191,7 +295,7 @@ class CableTraceSVG:
|
|||||||
|
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def _draw_attachment(self):
|
def draw_attachment(self):
|
||||||
"""
|
"""
|
||||||
Return an SVG group containing a line element and "Attachment" label.
|
Return an SVG group containing a line element and "Attachment" label.
|
||||||
"""
|
"""
|
||||||
@ -216,109 +320,63 @@ class CableTraceSVG:
|
|||||||
|
|
||||||
traced_path = self.origin.trace()
|
traced_path = self.origin.trace()
|
||||||
|
|
||||||
# Prep elements list
|
# Iterate through each (terms, cable, terms) segment in the path
|
||||||
parent_objects = []
|
|
||||||
terminations = []
|
|
||||||
connectors = []
|
|
||||||
|
|
||||||
# Iterate through each (term, cable, term) segment in the path
|
|
||||||
for i, segment in enumerate(traced_path):
|
for i, segment in enumerate(traced_path):
|
||||||
near_end, connector, far_end = segment
|
near_ends, links, far_ends = segment
|
||||||
|
|
||||||
# Near end parent
|
# Near end parent
|
||||||
if i == 0:
|
if i == 0:
|
||||||
# If this is the first segment, draw the originating termination's parent object
|
# If this is the first segment, draw the originating termination's parent object
|
||||||
parent_object = self._draw_box(
|
self.draw_parent_objects(set(end.parent_object for end in near_ends))
|
||||||
width=self.width,
|
|
||||||
color=self._get_color(near_end.parent_object),
|
|
||||||
url=near_end.parent_object.get_absolute_url(),
|
|
||||||
labels=self._get_labels(near_end.parent_object),
|
|
||||||
padding_multiplier=2
|
|
||||||
)
|
|
||||||
parent_objects.append(parent_object)
|
|
||||||
|
|
||||||
# Near end termination
|
# Near end termination(s)
|
||||||
if near_end is not None:
|
terminations = self.draw_terminations(near_ends)
|
||||||
termination = self._draw_box(
|
|
||||||
width=self.width * .8,
|
|
||||||
color=self._get_color(near_end),
|
|
||||||
url=near_end.get_absolute_url(),
|
|
||||||
labels=self._get_labels(near_end),
|
|
||||||
y_indent=PADDING,
|
|
||||||
radius=5
|
|
||||||
)
|
|
||||||
terminations.append(termination)
|
|
||||||
|
|
||||||
# Connector (a Cable or WirelessLink)
|
# Connector (a Cable or WirelessLink)
|
||||||
if connector is not None:
|
if links:
|
||||||
|
link = links[0] # Remove Cable from list
|
||||||
|
|
||||||
# Cable
|
# Cable
|
||||||
if type(connector) is Cable:
|
if type(link) is Cable:
|
||||||
connector_labels = [
|
|
||||||
f'Cable {connector}',
|
# Account for fan-ins height
|
||||||
connector.get_status_display()
|
if len(near_ends) > 1:
|
||||||
]
|
self.cursor += FANOUT_HEIGHT
|
||||||
if connector.type:
|
|
||||||
connector_labels.append(connector.get_type_display())
|
cable = self.draw_cable(link)
|
||||||
if connector.length and connector.length_unit:
|
self.connectors.append(cable)
|
||||||
connector_labels.append(f'{connector.length} {connector.get_length_unit_display()}')
|
|
||||||
cable = self._draw_cable(
|
# Draw fan-ins
|
||||||
color=connector.color or '000000',
|
if len(near_ends) > 1:
|
||||||
url=connector.get_absolute_url(),
|
for term in terminations:
|
||||||
labels=connector_labels
|
self.draw_fanin(term, cable)
|
||||||
)
|
|
||||||
connectors.append(cable)
|
|
||||||
|
|
||||||
# WirelessLink
|
# WirelessLink
|
||||||
elif type(connector) is WirelessLink:
|
elif type(link) is WirelessLink:
|
||||||
connector_labels = [
|
wirelesslink = self.draw_wirelesslink(link)
|
||||||
f'Wireless link {connector}',
|
self.connectors.append(wirelesslink)
|
||||||
connector.get_status_display()
|
|
||||||
]
|
|
||||||
if connector.ssid:
|
|
||||||
connector_labels.append(connector.ssid)
|
|
||||||
wirelesslink = self._draw_wirelesslink(
|
|
||||||
url=connector.get_absolute_url(),
|
|
||||||
labels=connector_labels
|
|
||||||
)
|
|
||||||
connectors.append(wirelesslink)
|
|
||||||
|
|
||||||
# Far end termination
|
# Far end termination(s)
|
||||||
termination = self._draw_box(
|
if len(far_ends) > 1:
|
||||||
width=self.width * .8,
|
self.cursor += FANOUT_HEIGHT
|
||||||
color=self._get_color(far_end),
|
terminations = self.draw_terminations(far_ends)
|
||||||
url=far_end.get_absolute_url(),
|
for term in terminations:
|
||||||
labels=self._get_labels(far_end),
|
self.draw_fanout(term, cable)
|
||||||
radius=5
|
else:
|
||||||
)
|
self.draw_terminations(far_ends)
|
||||||
terminations.append(termination)
|
|
||||||
|
|
||||||
# Far end parent
|
# Far end parent
|
||||||
parent_object = self._draw_box(
|
parent_objects = set(end.parent_object for end in far_ends)
|
||||||
width=self.width,
|
self.draw_parent_objects(parent_objects)
|
||||||
color=self._get_color(far_end.parent_object),
|
|
||||||
url=far_end.parent_object.get_absolute_url(),
|
|
||||||
labels=self._get_labels(far_end.parent_object),
|
|
||||||
y_indent=PADDING,
|
|
||||||
padding_multiplier=2
|
|
||||||
)
|
|
||||||
parent_objects.append(parent_object)
|
|
||||||
|
|
||||||
elif far_end:
|
elif far_ends:
|
||||||
|
|
||||||
# Attachment
|
# Attachment
|
||||||
attachment = self._draw_attachment()
|
attachment = self.draw_attachment()
|
||||||
connectors.append(attachment)
|
self.connectors.append(attachment)
|
||||||
|
|
||||||
# ProviderNetwork
|
# ProviderNetwork
|
||||||
parent_object = self._draw_box(
|
self.draw_parent_objects(set(end.parent_object for end in far_ends))
|
||||||
width=self.width,
|
|
||||||
color=self._get_color(far_end),
|
|
||||||
url=far_end.get_absolute_url(),
|
|
||||||
labels=self._get_labels(far_end),
|
|
||||||
padding_multiplier=2
|
|
||||||
)
|
|
||||||
parent_objects.append(parent_object)
|
|
||||||
|
|
||||||
# Determine drawing size
|
# Determine drawing size
|
||||||
self.drawing = svgwrite.Drawing(
|
self.drawing = svgwrite.Drawing(
|
||||||
@ -330,7 +388,7 @@ class CableTraceSVG:
|
|||||||
self.drawing.defs.add(self.drawing.style(css_file.read()))
|
self.drawing.defs.add(self.drawing.style(css_file.read()))
|
||||||
|
|
||||||
# Add elements to the drawing in order of depth (Z axis)
|
# Add elements to the drawing in order of depth (Z axis)
|
||||||
for element in connectors + parent_objects + terminations:
|
for element in self.connectors + self.parent_objects + self.terminations:
|
||||||
self.drawing.add(element)
|
self.drawing.add(element)
|
||||||
|
|
||||||
return self.drawing
|
return self.drawing
|
||||||
|
@ -1,56 +1,109 @@
|
|||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django_tables2.utils import Accessor
|
from django_tables2.utils import Accessor
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from dcim.models import Cable
|
from dcim.models import Cable
|
||||||
from netbox.tables import NetBoxTable, columns
|
from netbox.tables import NetBoxTable, columns
|
||||||
from tenancy.tables import TenantColumn
|
from tenancy.tables import TenantColumn
|
||||||
from .template_code import CABLE_LENGTH, CABLE_TERMINATION_PARENT
|
from .template_code import CABLE_LENGTH
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CableTable',
|
'CableTable',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CableTerminationsColumn(tables.Column):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
cable_end: Which side of the cable to report on (A or B)
|
||||||
|
attr: The CableTermination attribute to return for each instance (returns the termination object by default)
|
||||||
|
"""
|
||||||
|
def __init__(self, cable_end, attr='termination', *args, **kwargs):
|
||||||
|
self.cable_end = cable_end
|
||||||
|
self.attr = attr
|
||||||
|
super().__init__(accessor=Accessor('terminations'), *args, **kwargs)
|
||||||
|
|
||||||
|
def _get_terminations(self, manager):
|
||||||
|
terminations = set()
|
||||||
|
for cabletermination in manager.all():
|
||||||
|
if cabletermination.cable_end == self.cable_end:
|
||||||
|
if termination := getattr(cabletermination, self.attr, None):
|
||||||
|
terminations.add(termination)
|
||||||
|
|
||||||
|
return terminations
|
||||||
|
|
||||||
|
def render(self, value):
|
||||||
|
links = [
|
||||||
|
f'<a href="{term.get_absolute_url()}">{term}</a>' for term in self._get_terminations(value)
|
||||||
|
]
|
||||||
|
return mark_safe('<br />'.join(links) or '—')
|
||||||
|
|
||||||
|
def value(self, value):
|
||||||
|
return ','.join([str(t) for t in self._get_terminations(value)])
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableTable(NetBoxTable):
|
class CableTable(NetBoxTable):
|
||||||
termination_a_parent = tables.TemplateColumn(
|
a_terminations = CableTerminationsColumn(
|
||||||
template_code=CABLE_TERMINATION_PARENT,
|
cable_end='A',
|
||||||
accessor=Accessor('termination_a'),
|
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Side A'
|
|
||||||
)
|
|
||||||
rack_a = tables.Column(
|
|
||||||
accessor=Accessor('termination_a__device__rack'),
|
|
||||||
orderable=False,
|
|
||||||
linkify=True,
|
|
||||||
verbose_name='Rack A'
|
|
||||||
)
|
|
||||||
termination_a = tables.Column(
|
|
||||||
accessor=Accessor('termination_a'),
|
|
||||||
orderable=False,
|
|
||||||
linkify=True,
|
|
||||||
verbose_name='Termination A'
|
verbose_name='Termination A'
|
||||||
)
|
)
|
||||||
termination_b_parent = tables.TemplateColumn(
|
b_terminations = CableTerminationsColumn(
|
||||||
template_code=CABLE_TERMINATION_PARENT,
|
cable_end='B',
|
||||||
accessor=Accessor('termination_b'),
|
|
||||||
orderable=False,
|
orderable=False,
|
||||||
verbose_name='Side B'
|
verbose_name='Termination B'
|
||||||
)
|
)
|
||||||
rack_b = tables.Column(
|
device_a = CableTerminationsColumn(
|
||||||
accessor=Accessor('termination_b__device__rack'),
|
cable_end='A',
|
||||||
|
attr='_device',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Device A'
|
||||||
|
)
|
||||||
|
device_b = CableTerminationsColumn(
|
||||||
|
cable_end='B',
|
||||||
|
attr='_device',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Device B'
|
||||||
|
)
|
||||||
|
location_a = CableTerminationsColumn(
|
||||||
|
cable_end='A',
|
||||||
|
attr='_location',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Location A'
|
||||||
|
)
|
||||||
|
location_b = CableTerminationsColumn(
|
||||||
|
cable_end='B',
|
||||||
|
attr='_location',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Location B'
|
||||||
|
)
|
||||||
|
rack_a = CableTerminationsColumn(
|
||||||
|
cable_end='A',
|
||||||
|
attr='_rack',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Rack A'
|
||||||
|
)
|
||||||
|
rack_b = CableTerminationsColumn(
|
||||||
|
cable_end='B',
|
||||||
|
attr='_rack',
|
||||||
orderable=False,
|
orderable=False,
|
||||||
linkify=True,
|
|
||||||
verbose_name='Rack B'
|
verbose_name='Rack B'
|
||||||
)
|
)
|
||||||
termination_b = tables.Column(
|
site_a = CableTerminationsColumn(
|
||||||
accessor=Accessor('termination_b'),
|
cable_end='A',
|
||||||
|
attr='_site',
|
||||||
orderable=False,
|
orderable=False,
|
||||||
linkify=True,
|
verbose_name='Site A'
|
||||||
verbose_name='Termination B'
|
)
|
||||||
|
site_b = CableTerminationsColumn(
|
||||||
|
cable_end='B',
|
||||||
|
attr='_site',
|
||||||
|
orderable=False,
|
||||||
|
verbose_name='Site B'
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn()
|
status = columns.ChoiceFieldColumn()
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
@ -66,10 +119,10 @@ class CableTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'rack_a', 'termination_a', 'termination_b_parent', 'rack_b', 'termination_b',
|
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'device_a', 'device_b', 'rack_a', 'rack_b',
|
||||||
'status', 'type', 'tenant', 'color', 'length', 'tags', 'created', 'last_updated',
|
'location_a', 'location_b', 'site_a', 'site_b', 'status', 'type', 'tenant', 'color', 'length', 'tags',
|
||||||
|
'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = (
|
default_columns = (
|
||||||
'pk', 'id', 'label', 'termination_a_parent', 'termination_a', 'termination_b_parent', 'termination_b',
|
'pk', 'id', 'label', 'a_terminations', 'b_terminations', 'status', 'type',
|
||||||
'status', 'type',
|
|
||||||
)
|
)
|
||||||
|
@ -13,15 +13,17 @@ CABLE_LENGTH = """
|
|||||||
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
CABLE_TERMINATION_PARENT = """
|
# CABLE_TERMINATION_PARENT = """
|
||||||
{% if value.device %}
|
# {% with value.0 as termination %}
|
||||||
<a href="{{ value.device.get_absolute_url }}">{{ value.device }}</a>
|
# {% if termination.device %}
|
||||||
{% elif value.circuit %}
|
# <a href="{{ termination.device.get_absolute_url }}">{{ termination.device }}</a>
|
||||||
<a href="{{ value.circuit.get_absolute_url }}">{{ value.circuit }}</a>
|
# {% elif termination.circuit %}
|
||||||
{% elif value.power_panel %}
|
# <a href="{{ termination.circuit.get_absolute_url }}">{{ termination.circuit }}</a>
|
||||||
<a href="{{ value.power_panel.get_absolute_url }}">{{ value.power_panel }}</a>
|
# {% elif termination.power_panel %}
|
||||||
{% endif %}
|
# <a href="{{ termination.power_panel.get_absolute_url }}">{{ termination.power_panel }}</a>
|
||||||
"""
|
# {% endif %}
|
||||||
|
# {% endwith %}
|
||||||
|
# """
|
||||||
|
|
||||||
DEVICE_LINK = """
|
DEVICE_LINK = """
|
||||||
<a href="{% url 'dcim:device' pk=record.pk %}">
|
<a href="{% url 'dcim:device' pk=record.pk %}">
|
||||||
@ -133,9 +135,9 @@ CONSOLEPORT_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Console Server Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleports' pk=object.pk %}">Rear Port</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -165,9 +167,9 @@ CONSOLESERVERPORT_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Console Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:consoleserverport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_consoleserverports' pk=object.pk %}">Rear Port</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -197,8 +199,8 @@ POWERPORT_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-outlet' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.poweroutlet&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Outlet</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:powerport_connect' termination_a_id=record.pk termination_b_type='power-feed' %}?return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerport&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerfeed&return_url={% url 'dcim:device_powerports' pk=object.pk %}">Power Feed</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -224,7 +226,7 @@ POWEROUTLET_BUTTONS = """
|
|||||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i></a>
|
||||||
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
<a href="#" class="btn btn-outline-dark btn-sm disabled"><i class="mdi mdi-lan-connect" aria-hidden="true"></i></a>
|
||||||
{% if not record.mark_connected %}
|
{% if not record.mark_connected %}
|
||||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=record.pk termination_b_type='power-port' %}?return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ record.pk }}&b_terminations_type=dcim.powerport&return_url={% url 'dcim:device_poweroutlets' pk=object.pk %}" title="Connect" class="btn btn-success btn-sm">
|
||||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
|
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -274,10 +276,10 @@ INTERFACE_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Interface</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Rear Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_interfaces' pk=object.pk %}">Circuit Termination</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -313,12 +315,12 @@ FRONTPORT_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Interface</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Server Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Console Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Rear Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuittermination&return_url={% url 'dcim:device_frontports' pk=object.pk %}">Circuit Termination</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -350,12 +352,12 @@ REARPORT_BUTTONS = """
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='interface' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.interface&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Interface</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='console-server-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleserverport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Server Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='console-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.consoleport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Console Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='front-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.frontport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='rear-port' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=dcim.rearport&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Rear Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'dcim:rearport_connect' termination_a_id=record.pk termination_b_type='circuit-termination' %}?return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ record.pk }}&b_terminations_type=circuits.circuitterminations&return_url={% url 'dcim:device_rearports' pk=object.pk %}">Circuit Termination</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -45,7 +45,7 @@ class Mixins:
|
|||||||
device=peer_device,
|
device=peer_device,
|
||||||
name='Peer Termination'
|
name='Peer Termination'
|
||||||
)
|
)
|
||||||
cable = Cable(termination_a=obj, termination_b=peer_obj, label='Cable 1')
|
cable = Cable(a_terminations=[obj], b_terminations=[peer_obj], label='Cable 1')
|
||||||
cable.save()
|
cable.save()
|
||||||
|
|
||||||
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
self.add_permissions(f'dcim.view_{self.model._meta.model_name}')
|
||||||
@ -55,9 +55,9 @@ class Mixins:
|
|||||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 1)
|
self.assertEqual(len(response.data), 1)
|
||||||
segment1 = response.data[0]
|
segment1 = response.data[0]
|
||||||
self.assertEqual(segment1[0]['name'], obj.name)
|
self.assertEqual(segment1[0][0]['name'], obj.name)
|
||||||
self.assertEqual(segment1[1]['label'], cable.label)
|
self.assertEqual(segment1[1]['label'], cable.label)
|
||||||
self.assertEqual(segment1[2]['name'], peer_obj.name)
|
self.assertEqual(segment1[2][0]['name'], peer_obj.name)
|
||||||
|
|
||||||
|
|
||||||
class RegionTest(APIViewTestCases.APIViewTestCase):
|
class RegionTest(APIViewTestCases.APIViewTestCase):
|
||||||
@ -1884,33 +1884,33 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
|||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
cables = (
|
cables = (
|
||||||
Cable(termination_a=interfaces[0], termination_b=interfaces[10], label='Cable 1'),
|
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[10]], label='Cable 1'),
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[11], label='Cable 2'),
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[11]], label='Cable 2'),
|
||||||
Cable(termination_a=interfaces[2], termination_b=interfaces[12], label='Cable 3'),
|
Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[12]], label='Cable 3'),
|
||||||
)
|
)
|
||||||
for cable in cables:
|
for cable in cables:
|
||||||
cable.save()
|
cable.save()
|
||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'termination_a_type': 'dcim.interface',
|
'a_terminations_type': 'dcim.interface',
|
||||||
'termination_a_id': interfaces[4].pk,
|
'a_terminations': [interfaces[4].pk],
|
||||||
'termination_b_type': 'dcim.interface',
|
'b_terminations_type': 'dcim.interface',
|
||||||
'termination_b_id': interfaces[14].pk,
|
'b_terminations': [interfaces[14].pk],
|
||||||
'label': 'Cable 4',
|
'label': 'Cable 4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'termination_a_type': 'dcim.interface',
|
'a_terminations_type': 'dcim.interface',
|
||||||
'termination_a_id': interfaces[5].pk,
|
'a_terminations': [interfaces[5].pk],
|
||||||
'termination_b_type': 'dcim.interface',
|
'b_terminations_type': 'dcim.interface',
|
||||||
'termination_b_id': interfaces[15].pk,
|
'b_terminations': [interfaces[15].pk],
|
||||||
'label': 'Cable 5',
|
'label': 'Cable 5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'termination_a_type': 'dcim.interface',
|
'a_terminations_type': 'dcim.interface',
|
||||||
'termination_a_id': interfaces[6].pk,
|
'a_terminations': [interfaces[6].pk],
|
||||||
'termination_b_type': 'dcim.interface',
|
'b_terminations_type': 'dcim.interface',
|
||||||
'termination_b_id': interfaces[16].pk,
|
'b_terminations': [interfaces[16].pk],
|
||||||
'label': 'Cable 6',
|
'label': 'Cable 6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -1936,7 +1936,7 @@ class ConnectedDeviceTest(APITestCase):
|
|||||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||||
self.interface3 = Interface.objects.create(device=self.device1, name='eth1') # Not connected
|
self.interface3 = Interface.objects.create(device=self.device1, name='eth1') # Not connected
|
||||||
|
|
||||||
cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
cable = Cable(a_terminations=[self.interface1], b_terminations=[self.interface2])
|
||||||
cable.save()
|
cable.save()
|
||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1950,8 +1950,8 @@ class ConsolePortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ConsolePort.objects.bulk_create(console_ports)
|
ConsolePort.objects.bulk_create(console_ports)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=console_ports[0], termination_b=console_server_ports[0]).save()
|
Cable(a_terminations=[console_ports[0]], b_terminations=[console_server_ports[0]]).save()
|
||||||
Cable(termination_a=console_ports[1], termination_b=console_server_ports[1]).save()
|
Cable(a_terminations=[console_ports[1]], b_terminations=[console_server_ports[1]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -2097,8 +2097,8 @@ class ConsoleServerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
ConsoleServerPort.objects.bulk_create(console_server_ports)
|
ConsoleServerPort.objects.bulk_create(console_server_ports)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=console_server_ports[0], termination_b=console_ports[0]).save()
|
Cable(a_terminations=[console_server_ports[0]], b_terminations=[console_ports[0]]).save()
|
||||||
Cable(termination_a=console_server_ports[1], termination_b=console_ports[1]).save()
|
Cable(a_terminations=[console_server_ports[1]], b_terminations=[console_ports[1]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -2244,8 +2244,8 @@ class PowerPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
PowerPort.objects.bulk_create(power_ports)
|
PowerPort.objects.bulk_create(power_ports)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=power_ports[0], termination_b=power_outlets[0]).save()
|
Cable(a_terminations=[power_ports[0]], b_terminations=[power_outlets[0]]).save()
|
||||||
Cable(termination_a=power_ports[1], termination_b=power_outlets[1]).save()
|
Cable(a_terminations=[power_ports[1]], b_terminations=[power_outlets[1]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -2399,8 +2399,8 @@ class PowerOutletTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
PowerOutlet.objects.bulk_create(power_outlets)
|
PowerOutlet.objects.bulk_create(power_outlets)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=power_outlets[0], termination_b=power_ports[0]).save()
|
Cable(a_terminations=[power_outlets[0]], b_terminations=[power_ports[0]]).save()
|
||||||
Cable(termination_a=power_outlets[1], termination_b=power_ports[1]).save()
|
Cable(a_terminations=[power_outlets[1]], b_terminations=[power_ports[1]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -2656,8 +2656,8 @@ class InterfaceTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=interfaces[0], termination_b=interfaces[3]).save()
|
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]]).save()
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[4]).save()
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]]).save()
|
||||||
# Third pair is not connected
|
# Third pair is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -2932,8 +2932,8 @@ class FrontPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
FrontPort.objects.bulk_create(front_ports)
|
FrontPort.objects.bulk_create(front_ports)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=front_ports[0], termination_b=front_ports[3]).save()
|
Cable(a_terminations=[front_ports[0]], b_terminations=[front_ports[3]]).save()
|
||||||
Cable(termination_a=front_ports[1], termination_b=front_ports[4]).save()
|
Cable(a_terminations=[front_ports[1]], b_terminations=[front_ports[4]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -3078,8 +3078,8 @@ class RearPortTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
RearPort.objects.bulk_create(rear_ports)
|
RearPort.objects.bulk_create(rear_ports)
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=rear_ports[0], termination_b=rear_ports[3]).save()
|
Cable(a_terminations=[rear_ports[0]], b_terminations=[rear_ports[3]]).save()
|
||||||
Cable(termination_a=rear_ports[1], termination_b=rear_ports[4]).save()
|
Cable(a_terminations=[rear_ports[1]], b_terminations=[rear_ports[4]]).save()
|
||||||
# Third port is not connected
|
# Third port is not connected
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
@ -3663,6 +3663,21 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
|
locations = (
|
||||||
|
Location(name='Location 1', site=sites[0], slug='location-1'),
|
||||||
|
Location(name='Location 2', site=sites[1], slug='location-1'),
|
||||||
|
Location(name='Location 3', site=sites[2], slug='location-1'),
|
||||||
|
)
|
||||||
|
for location in locations:
|
||||||
|
location.save()
|
||||||
|
|
||||||
|
racks = (
|
||||||
|
Rack(name='Rack 1', site=sites[0], location=locations[0]),
|
||||||
|
Rack(name='Rack 2', site=sites[1], location=locations[1]),
|
||||||
|
Rack(name='Rack 3', site=sites[2], location=locations[2]),
|
||||||
|
)
|
||||||
|
Rack.objects.bulk_create(racks)
|
||||||
|
|
||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
@ -3670,24 +3685,17 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Tenant.objects.bulk_create(tenants)
|
Tenant.objects.bulk_create(tenants)
|
||||||
|
|
||||||
racks = (
|
|
||||||
Rack(name='Rack 1', site=sites[0]),
|
|
||||||
Rack(name='Rack 2', site=sites[1]),
|
|
||||||
Rack(name='Rack 3', site=sites[2]),
|
|
||||||
)
|
|
||||||
Rack.objects.bulk_create(racks)
|
|
||||||
|
|
||||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
|
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
|
||||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||||
|
|
||||||
devices = (
|
devices = (
|
||||||
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=1),
|
Device(name='Device 1', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], location=locations[0], position=1),
|
||||||
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], position=2),
|
Device(name='Device 2', device_type=device_type, device_role=device_role, site=sites[0], rack=racks[0], location=locations[0], position=2),
|
||||||
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=1),
|
Device(name='Device 3', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], location=locations[1], position=1),
|
||||||
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], position=2),
|
Device(name='Device 4', device_type=device_type, device_role=device_role, site=sites[1], rack=racks[1], location=locations[1], position=2),
|
||||||
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=1),
|
Device(name='Device 5', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], location=locations[2], position=1),
|
||||||
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], position=2),
|
Device(name='Device 6', device_type=device_type, device_role=device_role, site=sites[2], rack=racks[2], location=locations[2], position=2),
|
||||||
)
|
)
|
||||||
Device.objects.bulk_create(devices)
|
Device.objects.bulk_create(devices)
|
||||||
|
|
||||||
@ -3711,13 +3719,13 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
console_server_port = ConsoleServerPort.objects.create(device=devices[0], name='Console Server Port 1')
|
||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[2], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[2]], label='Cable 1', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=10, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[3], termination_b=interfaces[4], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(a_terminations=[interfaces[3]], b_terminations=[interfaces[4]], label='Cable 2', type=CableTypeChoices.TYPE_CAT3, tenant=tenants[0], status=LinkStatusChoices.STATUS_CONNECTED, color='aa1409', length=20, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[5], termination_b=interfaces[6], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(a_terminations=[interfaces[5]], b_terminations=[interfaces[6]], label='Cable 3', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_CONNECTED, color='f44336', length=30, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[7], termination_b=interfaces[8], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
Cable(a_terminations=[interfaces[7]], b_terminations=[interfaces[8]], label='Cable 4', type=CableTypeChoices.TYPE_CAT5E, tenant=tenants[1], status=LinkStatusChoices.STATUS_PLANNED, color='f44336', length=40, length_unit=CableLengthUnitChoices.UNIT_FOOT).save()
|
||||||
Cable(termination_a=interfaces[9], termination_b=interfaces[10], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(a_terminations=[interfaces[9]], b_terminations=[interfaces[10]], label='Cable 5', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=10, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=interfaces[11], termination_b=interfaces[0], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
Cable(a_terminations=[interfaces[11]], b_terminations=[interfaces[0]], label='Cable 6', type=CableTypeChoices.TYPE_CAT6, tenant=tenants[2], status=LinkStatusChoices.STATUS_PLANNED, color='e91e63', length=20, length_unit=CableLengthUnitChoices.UNIT_METER).save()
|
||||||
Cable(termination_a=console_port, termination_b=console_server_port, label='Cable 7').save()
|
Cable(a_terminations=[console_port], b_terminations=[console_server_port], label='Cable 7').save()
|
||||||
|
|
||||||
def test_label(self):
|
def test_label(self):
|
||||||
params = {'label': ['Cable 1', 'Cable 2']}
|
params = {'label': ['Cable 1', 'Cable 2']}
|
||||||
@ -3759,6 +3767,13 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'rack': [racks[0].name, racks[1].name]}
|
params = {'rack': [racks[0].name, racks[1].name]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
|
def test_location(self):
|
||||||
|
locations = Location.objects.all()[:2]
|
||||||
|
params = {'location_id': [locations[0].pk, locations[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
params = {'location': [locations[0].name, locations[1].name]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 6)
|
||||||
|
|
||||||
def test_site(self):
|
def test_site(self):
|
||||||
site = Site.objects.all()[:2]
|
site = Site.objects.all()[:2]
|
||||||
params = {'site_id': [site[0].pk, site[1].pk]}
|
params = {'site_id': [site[0].pk, site[1].pk]}
|
||||||
@ -3780,7 +3795,10 @@ class CableTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
def test_termination_ids(self):
|
def test_termination_ids(self):
|
||||||
interface_ids = Cable.objects.values_list('termination_a_id', flat=True)[:3]
|
interface_ids = CableTermination.objects.filter(
|
||||||
|
cable__in=Cable.objects.all()[:3],
|
||||||
|
cable_end='A'
|
||||||
|
).values_list('termination_id', flat=True)
|
||||||
params = {
|
params = {
|
||||||
'termination_a_type': 'dcim.interface',
|
'termination_a_type': 'dcim.interface',
|
||||||
'termination_a_id': list(interface_ids),
|
'termination_a_id': list(interface_ids),
|
||||||
@ -3924,8 +3942,8 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
PowerPort(device=device, name='Power Port 2'),
|
PowerPort(device=device, name='Power Port 2'),
|
||||||
]
|
]
|
||||||
PowerPort.objects.bulk_create(power_ports)
|
PowerPort.objects.bulk_create(power_ports)
|
||||||
Cable(termination_a=power_feeds[0], termination_b=power_ports[0]).save()
|
Cable(a_terminations=[power_feeds[0]], b_terminations=[power_ports[0]]).save()
|
||||||
Cable(termination_a=power_feeds[1], termination_b=power_ports[1]).save()
|
Cable(a_terminations=[power_feeds[1]], b_terminations=[power_ports[1]]).save()
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
params = {'name': ['Power Feed 1', 'Power Feed 2']}
|
params = {'name': ['Power Feed 1', 'Power Feed 2']}
|
||||||
|
@ -457,7 +457,7 @@ class CableTestCase(TestCase):
|
|||||||
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
self.interface1 = Interface.objects.create(device=self.device1, name='eth0')
|
||||||
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
self.interface2 = Interface.objects.create(device=self.device2, name='eth0')
|
||||||
self.interface3 = Interface.objects.create(device=self.device2, name='eth1')
|
self.interface3 = Interface.objects.create(device=self.device2, name='eth1')
|
||||||
self.cable = Cable(termination_a=self.interface1, termination_b=self.interface2)
|
self.cable = Cable(a_terminations=[self.interface1], b_terminations=[self.interface2])
|
||||||
self.cable.save()
|
self.cable.save()
|
||||||
|
|
||||||
self.power_port1 = PowerPort.objects.create(device=self.device2, name='psu1')
|
self.power_port1 = PowerPort.objects.create(device=self.device2, name='psu1')
|
||||||
@ -493,12 +493,14 @@ class CableTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
When a new Cable is created, it must be cached on either termination point.
|
When a new Cable is created, it must be cached on either termination point.
|
||||||
"""
|
"""
|
||||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
self.interface1.refresh_from_db()
|
||||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
self.interface2.refresh_from_db()
|
||||||
self.assertEqual(self.cable.termination_a, interface1)
|
self.assertEqual(self.interface1.cable, self.cable)
|
||||||
self.assertEqual(interface1._link_peer, interface2)
|
self.assertEqual(self.interface2.cable, self.cable)
|
||||||
self.assertEqual(self.cable.termination_b, interface2)
|
self.assertEqual(self.interface1.cable_end, 'A')
|
||||||
self.assertEqual(interface2._link_peer, interface1)
|
self.assertEqual(self.interface2.cable_end, 'B')
|
||||||
|
self.assertEqual(self.interface1.link_peers, [self.interface2])
|
||||||
|
self.assertEqual(self.interface2.link_peers, [self.interface1])
|
||||||
|
|
||||||
def test_cable_deletion(self):
|
def test_cable_deletion(self):
|
||||||
"""
|
"""
|
||||||
@ -510,50 +512,33 @@ class CableTestCase(TestCase):
|
|||||||
self.assertNotEqual(str(self.cable), '#None')
|
self.assertNotEqual(str(self.cable), '#None')
|
||||||
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
interface1 = Interface.objects.get(pk=self.interface1.pk)
|
||||||
self.assertIsNone(interface1.cable)
|
self.assertIsNone(interface1.cable)
|
||||||
self.assertIsNone(interface1._link_peer)
|
self.assertListEqual(interface1.link_peers, [])
|
||||||
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
interface2 = Interface.objects.get(pk=self.interface2.pk)
|
||||||
self.assertIsNone(interface2.cable)
|
self.assertIsNone(interface2.cable)
|
||||||
self.assertIsNone(interface2._link_peer)
|
self.assertListEqual(interface2.link_peers, [])
|
||||||
|
|
||||||
def test_cabletermination_deletion(self):
|
def test_cable_validates_same_parent_object(self):
|
||||||
"""
|
"""
|
||||||
When a CableTermination object is deleted, its attached Cable (if any) must also be deleted.
|
The clean method should ensure that all terminations at either end of a Cable belong to the same parent object.
|
||||||
"""
|
"""
|
||||||
self.interface1.delete()
|
cable = Cable(a_terminations=[self.interface1], b_terminations=[self.power_port1])
|
||||||
cable = Cable.objects.filter(pk=self.cable.pk).first()
|
with self.assertRaises(ValidationError):
|
||||||
self.assertIsNone(cable)
|
cable.clean()
|
||||||
|
|
||||||
|
def test_cable_validates_same_type(self):
|
||||||
|
"""
|
||||||
|
The clean method should ensure that all terminations at either end of a Cable are of the same type.
|
||||||
|
"""
|
||||||
|
cable = Cable(a_terminations=[self.front_port1, self.rear_port1], b_terminations=[self.interface1])
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
cable.clean()
|
||||||
|
|
||||||
def test_cable_validates_compatible_types(self):
|
def test_cable_validates_compatible_types(self):
|
||||||
"""
|
"""
|
||||||
The clean method should have a check to ensure only compatible port types can be connected by a cable
|
The clean method should have a check to ensure only compatible port types can be connected by a cable
|
||||||
"""
|
"""
|
||||||
# An interface cannot be connected to a power port
|
# An interface cannot be connected to a power port, for example
|
||||||
cable = Cable(termination_a=self.interface1, termination_b=self.power_port1)
|
cable = Cable(a_terminations=[self.interface1], b_terminations=[self.power_port1])
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
cable.clean()
|
|
||||||
|
|
||||||
def test_cable_cannot_have_the_same_terminination_on_both_ends(self):
|
|
||||||
"""
|
|
||||||
A cable cannot be made with the same A and B side terminations
|
|
||||||
"""
|
|
||||||
cable = Cable(termination_a=self.interface1, termination_b=self.interface1)
|
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
cable.clean()
|
|
||||||
|
|
||||||
def test_cable_front_port_cannot_connect_to_corresponding_rear_port(self):
|
|
||||||
"""
|
|
||||||
A cable cannot connect a front port to its corresponding rear port
|
|
||||||
"""
|
|
||||||
cable = Cable(termination_a=self.front_port1, termination_b=self.rear_port1)
|
|
||||||
with self.assertRaises(ValidationError):
|
|
||||||
cable.clean()
|
|
||||||
|
|
||||||
def test_cable_cannot_terminate_to_an_existing_connection(self):
|
|
||||||
"""
|
|
||||||
Either side of a cable cannot be terminated when that side already has a connection
|
|
||||||
"""
|
|
||||||
# Try to create a cable with the same interface terminations
|
|
||||||
cable = Cable(termination_a=self.interface2, termination_b=self.interface1)
|
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
|
||||||
@ -561,45 +546,16 @@ class CableTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
Neither side of a cable can be terminated to a CircuitTermination which is attached to a ProviderNetwork
|
Neither side of a cable can be terminated to a CircuitTermination which is attached to a ProviderNetwork
|
||||||
"""
|
"""
|
||||||
cable = Cable(termination_a=self.interface3, termination_b=self.circuittermination3)
|
cable = Cable(a_terminations=[self.interface3], b_terminations=[self.circuittermination3])
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
|
||||||
def test_rearport_connections(self):
|
|
||||||
"""
|
|
||||||
Test various combinations of RearPort connections.
|
|
||||||
"""
|
|
||||||
# Connecting a single-position RearPort to a multi-position RearPort is ok
|
|
||||||
Cable(termination_a=self.rear_port1, termination_b=self.rear_port2).full_clean()
|
|
||||||
|
|
||||||
# Connecting a single-position RearPort to an Interface is ok
|
|
||||||
Cable(termination_a=self.rear_port1, termination_b=self.interface3).full_clean()
|
|
||||||
|
|
||||||
# Connecting a single-position RearPort to a CircuitTermination is ok
|
|
||||||
Cable(termination_a=self.rear_port1, termination_b=self.circuittermination1).full_clean()
|
|
||||||
|
|
||||||
# Connecting a multi-position RearPort to another RearPort with the same number of positions is ok
|
|
||||||
Cable(termination_a=self.rear_port3, termination_b=self.rear_port4).full_clean()
|
|
||||||
|
|
||||||
# Connecting a multi-position RearPort to an Interface is ok
|
|
||||||
Cable(termination_a=self.rear_port2, termination_b=self.interface3).full_clean()
|
|
||||||
|
|
||||||
# Connecting a multi-position RearPort to a CircuitTermination is ok
|
|
||||||
Cable(termination_a=self.rear_port2, termination_b=self.circuittermination1).full_clean()
|
|
||||||
|
|
||||||
# Connecting a two-position RearPort to a three-position RearPort is NOT ok
|
|
||||||
with self.assertRaises(
|
|
||||||
ValidationError,
|
|
||||||
msg='Connecting a 2-position RearPort to a 3-position RearPort should fail'
|
|
||||||
):
|
|
||||||
Cable(termination_a=self.rear_port2, termination_b=self.rear_port3).full_clean()
|
|
||||||
|
|
||||||
def test_cable_cannot_terminate_to_a_virtual_interface(self):
|
def test_cable_cannot_terminate_to_a_virtual_interface(self):
|
||||||
"""
|
"""
|
||||||
A cable cannot terminate to a virtual interface
|
A cable cannot terminate to a virtual interface
|
||||||
"""
|
"""
|
||||||
virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
virtual_interface = Interface(device=self.device1, name="V1", type=InterfaceTypeChoices.TYPE_VIRTUAL)
|
||||||
cable = Cable(termination_a=self.interface2, termination_b=virtual_interface)
|
cable = Cable(a_terminations=[self.interface2], b_terminations=[virtual_interface])
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
|
||||||
@ -608,6 +564,6 @@ class CableTestCase(TestCase):
|
|||||||
A cable cannot terminate to a wireless interface
|
A cable cannot terminate to a wireless interface
|
||||||
"""
|
"""
|
||||||
wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
|
wireless_interface = Interface(device=self.device1, name="W1", type=InterfaceTypeChoices.TYPE_80211A)
|
||||||
cable = Cable(termination_a=self.interface2, termination_b=wireless_interface)
|
cable = Cable(a_terminations=[self.interface2], b_terminations=[wireless_interface])
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
cable.clean()
|
cable.clean()
|
||||||
|
@ -1961,7 +1961,7 @@ class ConsolePortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device=consoleport.device,
|
device=consoleport.device,
|
||||||
name='Console Server Port 1'
|
name='Console Server Port 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=consoleport, termination_b=consoleserverport).save()
|
Cable(a_terminations=[consoleport], b_terminations=[consoleserverport]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:consoleport_trace', kwargs={'pk': consoleport.pk}))
|
response = self.client.get(reverse('dcim:consoleport_trace', kwargs={'pk': consoleport.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2017,7 +2017,7 @@ class ConsoleServerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device=consoleserverport.device,
|
device=consoleserverport.device,
|
||||||
name='Console Port 1'
|
name='Console Port 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=consoleserverport, termination_b=consoleport).save()
|
Cable(a_terminations=[consoleserverport], b_terminations=[consoleport]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:consoleserverport_trace', kwargs={'pk': consoleserverport.pk}))
|
response = self.client.get(reverse('dcim:consoleserverport_trace', kwargs={'pk': consoleserverport.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2079,7 +2079,7 @@ class PowerPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device=powerport.device,
|
device=powerport.device,
|
||||||
name='Power Outlet 1'
|
name='Power Outlet 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=powerport, termination_b=poweroutlet).save()
|
Cable(a_terminations=[powerport], b_terminations=[poweroutlet]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:powerport_trace', kwargs={'pk': powerport.pk}))
|
response = self.client.get(reverse('dcim:powerport_trace', kwargs={'pk': powerport.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2144,7 +2144,7 @@ class PowerOutletTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
def test_trace(self):
|
def test_trace(self):
|
||||||
poweroutlet = PowerOutlet.objects.first()
|
poweroutlet = PowerOutlet.objects.first()
|
||||||
powerport = PowerPort.objects.first()
|
powerport = PowerPort.objects.first()
|
||||||
Cable(termination_a=poweroutlet, termination_b=powerport).save()
|
Cable(a_terminations=[poweroutlet], b_terminations=[powerport]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:poweroutlet_trace', kwargs={'pk': poweroutlet.pk}))
|
response = self.client.get(reverse('dcim:poweroutlet_trace', kwargs={'pk': poweroutlet.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2268,7 +2268,7 @@ class InterfaceTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_trace(self):
|
def test_trace(self):
|
||||||
interface1, interface2 = Interface.objects.all()[:2]
|
interface1, interface2 = Interface.objects.all()[:2]
|
||||||
Cable(termination_a=interface1, termination_b=interface2).save()
|
Cable(a_terminations=[interface1], b_terminations=[interface2]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk}))
|
response = self.client.get(reverse('dcim:interface_trace', kwargs={'pk': interface1.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2339,7 +2339,7 @@ class FrontPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device=frontport.device,
|
device=frontport.device,
|
||||||
name='Interface 1'
|
name='Interface 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=frontport, termination_b=interface).save()
|
Cable(a_terminations=[frontport], b_terminations=[interface]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:frontport_trace', kwargs={'pk': frontport.pk}))
|
response = self.client.get(reverse('dcim:frontport_trace', kwargs={'pk': frontport.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2397,7 +2397,7 @@ class RearPortTestCase(ViewTestCases.DeviceComponentViewTestCase):
|
|||||||
device=rearport.device,
|
device=rearport.device,
|
||||||
name='Interface 1'
|
name='Interface 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=rearport, termination_b=interface).save()
|
Cable(a_terminations=[rearport], b_terminations=[interface]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:rearport_trace', kwargs={'pk': rearport.pk}))
|
response = self.client.get(reverse('dcim:rearport_trace', kwargs={'pk': rearport.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
@ -2630,19 +2630,18 @@ class CableTestCase(
|
|||||||
)
|
)
|
||||||
Interface.objects.bulk_create(interfaces)
|
Interface.objects.bulk_create(interfaces)
|
||||||
|
|
||||||
Cable(termination_a=interfaces[0], termination_b=interfaces[3], type=CableTypeChoices.TYPE_CAT6).save()
|
Cable(a_terminations=[interfaces[0]], b_terminations=[interfaces[3]], type=CableTypeChoices.TYPE_CAT6).save()
|
||||||
Cable(termination_a=interfaces[1], termination_b=interfaces[4], type=CableTypeChoices.TYPE_CAT6).save()
|
Cable(a_terminations=[interfaces[1]], b_terminations=[interfaces[4]], type=CableTypeChoices.TYPE_CAT6).save()
|
||||||
Cable(termination_a=interfaces[2], termination_b=interfaces[5], type=CableTypeChoices.TYPE_CAT6).save()
|
Cable(a_terminations=[interfaces[2]], b_terminations=[interfaces[5]], type=CableTypeChoices.TYPE_CAT6).save()
|
||||||
|
|
||||||
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
tags = create_tags('Alpha', 'Bravo', 'Charlie')
|
||||||
|
|
||||||
interface_ct = ContentType.objects.get_for_model(Interface)
|
interface_ct = ContentType.objects.get_for_model(Interface)
|
||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
|
# TODO: Revisit this limitation
|
||||||
# Changing terminations not supported when editing an existing Cable
|
# Changing terminations not supported when editing an existing Cable
|
||||||
'termination_a_type': interface_ct.pk,
|
'a_terminations': interfaces[0].pk,
|
||||||
'termination_a_id': interfaces[0].pk,
|
'b_terminations': interfaces[3].pk,
|
||||||
'termination_b_type': interface_ct.pk,
|
|
||||||
'termination_b_id': interfaces[3].pk,
|
|
||||||
'type': CableTypeChoices.TYPE_CAT6,
|
'type': CableTypeChoices.TYPE_CAT6,
|
||||||
'status': LinkStatusChoices.STATUS_PLANNED,
|
'status': LinkStatusChoices.STATUS_PLANNED,
|
||||||
'label': 'Label',
|
'label': 'Label',
|
||||||
@ -2864,7 +2863,7 @@ class PowerFeedTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
device=device,
|
device=device,
|
||||||
name='Power Port 1'
|
name='Power Port 1'
|
||||||
)
|
)
|
||||||
Cable(termination_a=powerfeed, termination_b=powerport).save()
|
Cable(a_terminations=[powerfeed], b_terminations=[powerport]).save()
|
||||||
|
|
||||||
response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk}))
|
response = self.client.get(reverse('dcim:powerfeed_trace', kwargs={'pk': powerfeed.pk}))
|
||||||
self.assertHttpStatus(response, 200)
|
self.assertHttpStatus(response, 200)
|
||||||
|
@ -294,7 +294,6 @@ urlpatterns = [
|
|||||||
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
path('console-ports/<int:pk>/delete/', views.ConsolePortDeleteView.as_view(), name='consoleport_delete'),
|
||||||
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
|
path('console-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleport_changelog', kwargs={'model': ConsolePort}),
|
||||||
path('console-ports/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/console-ports/add/', views.DeviceBulkAddConsolePortView.as_view(), name='device_bulk_add_consoleport'),
|
||||||
|
|
||||||
# Console server ports
|
# Console server ports
|
||||||
@ -310,7 +309,6 @@ urlpatterns = [
|
|||||||
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
path('console-server-ports/<int:pk>/delete/', views.ConsoleServerPortDeleteView.as_view(), name='consoleserverport_delete'),
|
||||||
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
|
path('console-server-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='consoleserverport_changelog', kwargs={'model': ConsoleServerPort}),
|
||||||
path('console-server-ports/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/console-server-ports/add/', views.DeviceBulkAddConsoleServerPortView.as_view(), name='device_bulk_add_consoleserverport'),
|
||||||
|
|
||||||
# Power ports
|
# Power ports
|
||||||
@ -326,7 +324,6 @@ urlpatterns = [
|
|||||||
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
path('power-ports/<int:pk>/delete/', views.PowerPortDeleteView.as_view(), name='powerport_delete'),
|
||||||
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
|
path('power-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerport_changelog', kwargs={'model': PowerPort}),
|
||||||
path('power-ports/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/power-ports/add/', views.DeviceBulkAddPowerPortView.as_view(), name='device_bulk_add_powerport'),
|
||||||
|
|
||||||
# Power outlets
|
# Power outlets
|
||||||
@ -342,7 +339,6 @@ urlpatterns = [
|
|||||||
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
path('power-outlets/<int:pk>/delete/', views.PowerOutletDeleteView.as_view(), name='poweroutlet_delete'),
|
||||||
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
|
path('power-outlets/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='poweroutlet_changelog', kwargs={'model': PowerOutlet}),
|
||||||
path('power-outlets/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/power-outlets/add/', views.DeviceBulkAddPowerOutletView.as_view(), name='device_bulk_add_poweroutlet'),
|
||||||
|
|
||||||
# Interfaces
|
# Interfaces
|
||||||
@ -358,7 +354,6 @@ urlpatterns = [
|
|||||||
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
path('interfaces/<int:pk>/delete/', views.InterfaceDeleteView.as_view(), name='interface_delete'),
|
||||||
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
path('interfaces/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='interface_changelog', kwargs={'model': Interface}),
|
||||||
path('interfaces/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/interfaces/add/', views.DeviceBulkAddInterfaceView.as_view(), name='device_bulk_add_interface'),
|
||||||
|
|
||||||
# Front ports
|
# Front ports
|
||||||
@ -374,7 +369,6 @@ urlpatterns = [
|
|||||||
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
path('front-ports/<int:pk>/delete/', views.FrontPortDeleteView.as_view(), name='frontport_delete'),
|
||||||
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
|
path('front-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='frontport_changelog', kwargs={'model': FrontPort}),
|
||||||
path('front-ports/<int:pk>/trace/', views.PathTraceView.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'),
|
# path('devices/front-ports/add/', views.DeviceBulkAddFrontPortView.as_view(), name='device_bulk_add_frontport'),
|
||||||
|
|
||||||
# Rear ports
|
# Rear ports
|
||||||
@ -390,7 +384,6 @@ urlpatterns = [
|
|||||||
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
path('rear-ports/<int:pk>/delete/', views.RearPortDeleteView.as_view(), name='rearport_delete'),
|
||||||
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
|
path('rear-ports/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='rearport_changelog', kwargs={'model': RearPort}),
|
||||||
path('rear-ports/<int:pk>/trace/', views.PathTraceView.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'),
|
path('devices/rear-ports/add/', views.DeviceBulkAddRearPortView.as_view(), name='device_bulk_add_rearport'),
|
||||||
|
|
||||||
# Module bays
|
# Module bays
|
||||||
@ -447,6 +440,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Cables
|
# Cables
|
||||||
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
path('cables/', views.CableListView.as_view(), name='cable_list'),
|
||||||
|
path('cables/add/', views.CableEditView.as_view(), name='cable_add'),
|
||||||
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
path('cables/import/', views.CableBulkImportView.as_view(), name='cable_import'),
|
||||||
path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
path('cables/edit/', views.CableBulkEditView.as_view(), name='cable_bulk_edit'),
|
||||||
path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
path('cables/delete/', views.CableBulkDeleteView.as_view(), name='cable_bulk_delete'),
|
||||||
@ -500,6 +494,5 @@ urlpatterns = [
|
|||||||
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
|
path('power-feeds/<int:pk>/trace/', views.PathTraceView.as_view(), name='powerfeed_trace', kwargs={'model': PowerFeed}),
|
||||||
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
path('power-feeds/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='powerfeed_changelog', kwargs={'model': PowerFeed}),
|
||||||
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', kwargs={'model': PowerFeed}),
|
path('power-feeds/<int:pk>/journal/', ObjectJournalView.as_view(), name='powerfeed_journal', 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}),
|
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import itertools
|
||||||
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
@ -29,27 +31,29 @@ def path_node_to_object(repr):
|
|||||||
return ct.model_class().objects.get(pk=object_id)
|
return ct.model_class().objects.get(pk=object_id)
|
||||||
|
|
||||||
|
|
||||||
def create_cablepath(node):
|
def create_cablepath(terminations):
|
||||||
"""
|
"""
|
||||||
Create CablePaths for all paths originating from the specified node.
|
Create CablePaths for all paths originating from the specified set of nodes.
|
||||||
|
|
||||||
|
:param terminations: Iterable of CableTermination objects
|
||||||
"""
|
"""
|
||||||
from dcim.models import CablePath
|
from dcim.models import CablePath
|
||||||
|
|
||||||
cp = CablePath.from_origin(node)
|
cp = CablePath.from_origin(terminations)
|
||||||
if cp:
|
if cp:
|
||||||
cp.save()
|
cp.save()
|
||||||
|
|
||||||
|
|
||||||
def rebuild_paths(obj):
|
def rebuild_paths(terminations):
|
||||||
"""
|
"""
|
||||||
Rebuild all CablePaths which traverse the specified node
|
Rebuild all CablePaths which traverse the specified nodes.
|
||||||
"""
|
"""
|
||||||
from dcim.models import CablePath
|
from dcim.models import CablePath
|
||||||
|
|
||||||
cable_paths = CablePath.objects.filter(path__contains=obj)
|
for obj in terminations:
|
||||||
|
cable_paths = CablePath.objects.filter(_nodes__contains=obj)
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
for cp in cable_paths:
|
for cp in cable_paths:
|
||||||
cp.delete()
|
cp.delete()
|
||||||
if cp.origin:
|
create_cablepath(cp.origins)
|
||||||
create_cablepath(cp.origin)
|
|
||||||
|
@ -12,7 +12,7 @@ from django.utils.html import escape
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit, CircuitTermination
|
||||||
from extras.views import ObjectConfigContextView
|
from extras.views import ObjectConfigContextView
|
||||||
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup
|
||||||
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||||
@ -28,6 +28,18 @@ from .choices import DeviceFaceChoices
|
|||||||
from .constants import NONCONNECTABLE_IFACE_TYPES
|
from .constants import NONCONNECTABLE_IFACE_TYPES
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
CABLE_TERMINATION_TYPES = {
|
||||||
|
'dcim.consoleport': ConsolePort,
|
||||||
|
'dcim.consoleserverport': ConsoleServerPort,
|
||||||
|
'dcim.powerport': PowerPort,
|
||||||
|
'dcim.poweroutlet': PowerOutlet,
|
||||||
|
'dcim.interface': Interface,
|
||||||
|
'dcim.frontport': FrontPort,
|
||||||
|
'dcim.rearport': RearPort,
|
||||||
|
'dcim.powerfeed': PowerFeed,
|
||||||
|
'circuits.circuittermination': CircuitTermination,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DeviceComponentsView(generic.ObjectChildrenView):
|
class DeviceComponentsView(generic.ObjectChildrenView):
|
||||||
queryset = Device.objects.all()
|
queryset = Device.objects.all()
|
||||||
@ -1717,7 +1729,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
|
|||||||
|
|
||||||
def get_extra_context(self, request, instance):
|
def get_extra_context(self, request, instance):
|
||||||
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
|
||||||
'_path__destination'
|
'_path'
|
||||||
).exclude(
|
).exclude(
|
||||||
type__in=NONCONNECTABLE_IFACE_TYPES
|
type__in=NONCONNECTABLE_IFACE_TYPES
|
||||||
)
|
)
|
||||||
@ -2744,7 +2756,10 @@ class DeviceBulkAddInventoryItemView(generic.BulkComponentCreateView):
|
|||||||
#
|
#
|
||||||
|
|
||||||
class CableListView(generic.ObjectListView):
|
class CableListView(generic.ObjectListView):
|
||||||
queryset = Cable.objects.all()
|
queryset = Cable.objects.prefetch_related(
|
||||||
|
'terminations__termination', 'terminations___device', 'terminations___rack', 'terminations___location',
|
||||||
|
'terminations___site',
|
||||||
|
)
|
||||||
filterset = filtersets.CableFilterSet
|
filterset = filtersets.CableFilterSet
|
||||||
filterset_form = forms.CableFilterForm
|
filterset_form = forms.CableFilterForm
|
||||||
table = tables.CableTable
|
table = tables.CableTable
|
||||||
@ -2777,7 +2792,7 @@ class PathTraceView(generic.ObjectView):
|
|||||||
|
|
||||||
# Otherwise, find all CablePaths which traverse the specified object
|
# Otherwise, find all CablePaths which traverse the specified object
|
||||||
else:
|
else:
|
||||||
related_paths = CablePath.objects.filter(path__contains=instance).prefetch_related('origin')
|
related_paths = CablePath.objects.filter(_nodes__contains=instance)
|
||||||
# Check for specification of a particular path (when tracing pass-through ports)
|
# Check for specification of a particular path (when tracing pass-through ports)
|
||||||
try:
|
try:
|
||||||
path_id = int(request.GET.get('cablepath_id'))
|
path_id = int(request.GET.get('cablepath_id'))
|
||||||
@ -2798,8 +2813,8 @@ class PathTraceView(generic.ObjectView):
|
|||||||
total_length, is_definitive = path.get_total_length() if path else (None, False)
|
total_length, is_definitive = path.get_total_length() if path else (None, False)
|
||||||
|
|
||||||
# Determine the path to the SVG trace image
|
# Determine the path to the SVG trace image
|
||||||
api_viewname = f"{path.origin._meta.app_label}-api:{path.origin._meta.model_name}-trace"
|
api_viewname = f"{path.origin_type.app_label}-api:{path.origin_type.model}-trace"
|
||||||
svg_url = f"{reverse(api_viewname, kwargs={'pk': path.origin.pk})}?render=svg"
|
svg_url = f"{reverse(api_viewname, kwargs={'pk': path.origins[0].pk})}?render=svg"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'path': path,
|
'path': path,
|
||||||
@ -2810,77 +2825,38 @@ class PathTraceView(generic.ObjectView):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CableCreateView(generic.ObjectEditView):
|
class CableEditView(generic.ObjectEditView):
|
||||||
queryset = Cable.objects.all()
|
queryset = Cable.objects.all()
|
||||||
template_name = 'dcim/cable_connect.html'
|
template_name = 'dcim/cable_edit.html'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
|
|
||||||
# Set the form class based on the type of component being connected
|
# If creating a new Cable, initialize the form class using URL query params
|
||||||
self.form = {
|
if 'pk' not in kwargs:
|
||||||
'console-port': forms.ConnectCableToConsolePortForm,
|
self.form = forms.get_cable_form(
|
||||||
'console-server-port': forms.ConnectCableToConsoleServerPortForm,
|
a_type=CABLE_TERMINATION_TYPES.get(request.GET.get('a_terminations_type')),
|
||||||
'power-port': forms.ConnectCableToPowerPortForm,
|
b_type=CABLE_TERMINATION_TYPES.get(request.GET.get('b_terminations_type'))
|
||||||
'power-outlet': forms.ConnectCableToPowerOutletForm,
|
)
|
||||||
'interface': forms.ConnectCableToInterfaceForm,
|
|
||||||
'front-port': forms.ConnectCableToFrontPortForm,
|
|
||||||
'rear-port': forms.ConnectCableToRearPortForm,
|
|
||||||
'power-feed': forms.ConnectCableToPowerFeedForm,
|
|
||||||
'circuit-termination': forms.ConnectCableToCircuitTerminationForm,
|
|
||||||
}[kwargs.get('termination_b_type')]
|
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get_object(self, **kwargs):
|
def get_object(self, **kwargs):
|
||||||
# Always return a new instance
|
"""
|
||||||
return self.queryset.model()
|
Hack into get_object() to set the form class when editing an existing Cable, since ObjectEditView
|
||||||
|
doesn't currently provide a hook for dynamic class resolution.
|
||||||
|
"""
|
||||||
|
obj = super().get_object(**kwargs)
|
||||||
|
|
||||||
def alter_object(self, obj, request, url_args, url_kwargs):
|
if obj.pk:
|
||||||
termination_a_type = url_kwargs.get('termination_a_type')
|
# TODO: Optimize this logic
|
||||||
termination_a_id = url_kwargs.get('termination_a_id')
|
termination_a = obj.terminations.filter(cable_end='A').first()
|
||||||
termination_b_type_name = url_kwargs.get('termination_b_type')
|
a_type = termination_a.termination._meta.model if termination_a else None
|
||||||
self.termination_b_type = ContentType.objects.get(model=termination_b_type_name.replace('-', ''))
|
termination_b = obj.terminations.filter(cable_end='B').first()
|
||||||
|
b_type = termination_b.termination._meta.model if termination_a else None
|
||||||
# Initialize Cable termination attributes
|
self.form = forms.get_cable_form(a_type, b_type)
|
||||||
obj.termination_a = termination_a_type.objects.get(pk=termination_a_id)
|
|
||||||
obj.termination_b_type = self.termination_b_type
|
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
|
||||||
obj = self.get_object(**kwargs)
|
|
||||||
obj = self.alter_object(obj, request, args, kwargs)
|
|
||||||
|
|
||||||
# Parse initial data manually to avoid setting field values as lists
|
|
||||||
initial_data = {k: request.GET[k] for k in request.GET}
|
|
||||||
|
|
||||||
# Set initial site and rack based on side A termination (if not already set)
|
|
||||||
termination_a_site = getattr(obj.termination_a.parent_object, 'site', None)
|
|
||||||
if termination_a_site and 'termination_b_region' not in initial_data:
|
|
||||||
initial_data['termination_b_region'] = termination_a_site.region
|
|
||||||
if termination_a_site and 'termination_b_site_group' not in initial_data:
|
|
||||||
initial_data['termination_b_site_group'] = termination_a_site.group
|
|
||||||
if 'termination_b_site' not in initial_data:
|
|
||||||
initial_data['termination_b_site'] = termination_a_site
|
|
||||||
if 'termination_b_rack' not in initial_data:
|
|
||||||
initial_data['termination_b_rack'] = getattr(obj.termination_a.parent_object, 'rack', None)
|
|
||||||
|
|
||||||
form = self.form(instance=obj, initial=initial_data)
|
|
||||||
|
|
||||||
return render(request, self.template_name, {
|
|
||||||
'obj': obj,
|
|
||||||
'obj_type': Cable._meta.verbose_name,
|
|
||||||
'termination_b_type': self.termination_b_type.name,
|
|
||||||
'form': form,
|
|
||||||
'return_url': self.get_return_url(request, obj),
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class CableEditView(generic.ObjectEditView):
|
|
||||||
queryset = Cable.objects.all()
|
|
||||||
form = forms.CableForm
|
|
||||||
template_name = 'dcim/cable_edit.html'
|
|
||||||
|
|
||||||
|
|
||||||
class CableDeleteView(generic.ObjectDeleteView):
|
class CableDeleteView(generic.ObjectDeleteView):
|
||||||
queryset = Cable.objects.all()
|
queryset = Cable.objects.all()
|
||||||
@ -2893,14 +2869,14 @@ class CableBulkImportView(generic.BulkImportView):
|
|||||||
|
|
||||||
|
|
||||||
class CableBulkEditView(generic.BulkEditView):
|
class CableBulkEditView(generic.BulkEditView):
|
||||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
queryset = Cable.objects.prefetch_related('terminations')
|
||||||
filterset = filtersets.CableFilterSet
|
filterset = filtersets.CableFilterSet
|
||||||
table = tables.CableTable
|
table = tables.CableTable
|
||||||
form = forms.CableBulkEditForm
|
form = forms.CableBulkEditForm
|
||||||
|
|
||||||
|
|
||||||
class CableBulkDeleteView(generic.BulkDeleteView):
|
class CableBulkDeleteView(generic.BulkDeleteView):
|
||||||
queryset = Cable.objects.prefetch_related('termination_a', 'termination_b')
|
queryset = Cable.objects.prefetch_related('terminations')
|
||||||
filterset = filtersets.CableFilterSet
|
filterset = filtersets.CableFilterSet
|
||||||
table = tables.CableTable
|
table = tables.CableTable
|
||||||
|
|
||||||
|
@ -179,8 +179,8 @@ class ExceptionHandlingMiddleware:
|
|||||||
def process_exception(self, request, exception):
|
def process_exception(self, request, exception):
|
||||||
|
|
||||||
# Handle exceptions that occur from REST API requests
|
# Handle exceptions that occur from REST API requests
|
||||||
if is_api_request(request):
|
# if is_api_request(request):
|
||||||
return rest_api_server_error(request)
|
# return rest_api_server_error(request)
|
||||||
|
|
||||||
# Don't catch exceptions when in debug mode
|
# Don't catch exceptions when in debug mode
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
@ -3,7 +3,6 @@ import sys
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.db.models import F
|
|
||||||
from django.http import HttpResponseServerError
|
from django.http import HttpResponseServerError
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.template import loader
|
from django.template import loader
|
||||||
@ -37,14 +36,13 @@ class HomeView(View):
|
|||||||
return redirect("login")
|
return redirect("login")
|
||||||
|
|
||||||
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
connected_consoleports = ConsolePort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__destination_id__isnull=False
|
_path__is_complete=True
|
||||||
)
|
)
|
||||||
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
connected_powerports = PowerPort.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__destination_id__isnull=False
|
_path__is_complete=True
|
||||||
)
|
)
|
||||||
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
connected_interfaces = Interface.objects.restrict(request.user, 'view').prefetch_related('_path').filter(
|
||||||
_path__destination_id__isnull=False,
|
_path__is_complete=True
|
||||||
pk__lt=F('_path__destination_id')
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def build_stats():
|
def build_stats():
|
||||||
|
BIN
netbox/project-static/dist/cable_trace.css
vendored
BIN
netbox/project-static/dist/cable_trace.css
vendored
Binary file not shown.
@ -55,7 +55,11 @@ svg {
|
|||||||
line {
|
line {
|
||||||
stroke-width: 5px;
|
stroke-width: 5px;
|
||||||
}
|
}
|
||||||
line.cable-shadow {
|
polyline {
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 5px;
|
||||||
|
}
|
||||||
|
.cable-shadow {
|
||||||
stroke: var(--nbx-trace-cable-shadow);
|
stroke: var(--nbx-trace-cable-shadow);
|
||||||
stroke-width: 7px;
|
stroke-width: 7px;
|
||||||
}
|
}
|
||||||
|
@ -44,16 +44,15 @@
|
|||||||
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
<span class="text-success"><i class="mdi mdi-check-bold"></i></span>
|
||||||
<span class="text-muted">Marked as connected</span>
|
<span class="text-muted">Marked as connected</span>
|
||||||
{% elif termination.cable %}
|
{% elif termination.cable %}
|
||||||
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a>
|
<a class="d-block d-md-inline" href="{{ termination.cable.get_absolute_url }}">{{ termination.cable }}</a> to
|
||||||
{% with peer=termination.get_link_peer %}
|
{% for peer in termination.link_peers %}
|
||||||
to
|
|
||||||
{% if peer.device %}
|
{% if peer.device %}
|
||||||
{{ peer.device|linkify }}<br/>
|
{{ peer.device|linkify }}<br/>
|
||||||
{% elif peer.circuit %}
|
{% elif peer.circuit %}
|
||||||
{{ peer.circuit|linkify }}<br/>
|
{{ peer.circuit|linkify }}<br/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ peer|linkify }}
|
{{ peer|linkify }}{% if not forloop.last %},{% endif %}
|
||||||
{% endwith %}
|
{% endfor %}
|
||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
<a href="{% url 'circuits:circuittermination_trace' pk=termination.pk %}" class="btn btn-primary btn-sm lh-1" title="Trace">
|
||||||
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> Trace
|
<i class="mdi mdi-transit-connection-variant" aria-hidden="true"></i> Trace
|
||||||
@ -70,10 +69,10 @@
|
|||||||
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
<span class="mdi mdi-ethernet-cable" aria-hidden="true"></span> Connect
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='interface' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Interface</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.interface&termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Interface</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='front-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.frontport&termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Front Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='rear-port' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=dcim.rearport&termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Rear Port</a></li>
|
||||||
<li><a class="dropdown-item" href="{% url 'circuits:circuittermination_connect' termination_a_id=termination.pk termination_b_type='circuit-termination' %}?termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
|
<li><a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=circuits.circuittermination&a_terminations={{ termination.pk }}&b_terminations_type=circuits.circuittermination&termination_b_site={{ termination.site.pk }}&return_url={{ object.get_absolute_url }}">Circuit Termination</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -5,85 +5,79 @@
|
|||||||
{% load plugins %}
|
{% load plugins %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-md-6">
|
<div class="col col-md-6">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">
|
<h5 class="card-header">Cable</h5>
|
||||||
Cable
|
<div class="card-body">
|
||||||
</h5>
|
<table class="table table-hover attr-table">
|
||||||
<div class="card-body">
|
<tr>
|
||||||
<table class="table table-hover attr-table">
|
<th scope="row">Type</th>
|
||||||
<tr>
|
<td>{{ object.get_type_display|placeholder }}</td>
|
||||||
<th scope="row">Type</th>
|
</tr>
|
||||||
<td>{{ object.get_type_display|placeholder }}</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Status</th>
|
||||||
<tr>
|
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||||
<th scope="row">Status</th>
|
</tr>
|
||||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Tenant</th>
|
||||||
<tr>
|
<td>
|
||||||
<th scope="row">Tenant</th>
|
{% if object.tenant.group %}
|
||||||
<td>
|
{{ object.tenant.group|linkify }} /
|
||||||
{% if object.tenant.group %}
|
{% endif %}
|
||||||
{{ object.tenant.group|linkify }} /
|
{{ object.tenant|linkify|placeholder }}
|
||||||
{% endif %}
|
</td>
|
||||||
{{ object.tenant|linkify|placeholder }}
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Label</th>
|
||||||
<tr>
|
<td>{{ object.label|placeholder }}</td>
|
||||||
<th scope="row">Label</th>
|
</tr>
|
||||||
<td>{{ object.label|placeholder }}</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Color</th>
|
||||||
<tr>
|
<td>
|
||||||
<th scope="row">Color</th>
|
{% if object.color %}
|
||||||
<td>
|
<span class="color-label" style="background-color: #{{ object.color }}"> </span>
|
||||||
{% if object.color %}
|
{% else %}
|
||||||
<span class="color-label" style="background-color: #{{ object.color }}"> </span>
|
{{ ''|placeholder }}
|
||||||
{% else %}
|
{% endif %}
|
||||||
{{ ''|placeholder }}
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
</td>
|
<tr>
|
||||||
</tr>
|
<th scope="row">Length</th>
|
||||||
<tr>
|
<td>
|
||||||
<th scope="row">Length</th>
|
{% if object.length %}
|
||||||
<td>
|
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
|
||||||
{% if object.length %}
|
{% else %}
|
||||||
{{ object.length|floatformat }} {{ object.get_length_unit_display }}
|
{{ ''|placeholder }}
|
||||||
{% else %}
|
{% endif %}
|
||||||
{{ ''|placeholder }}
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
</td>
|
</table>
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% include 'inc/panels/custom_fields.html' %}
|
|
||||||
{% include 'inc/panels/tags.html' %}
|
|
||||||
{% plugin_left_page object %}
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-6">
|
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">
|
|
||||||
Termination A
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'dcim/inc/cable_termination.html' with termination=object.termination_a %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header">
|
|
||||||
Termination B
|
|
||||||
</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'dcim/inc/cable_termination.html' with termination=object.termination_b %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% plugin_right_page object %}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'inc/panels/custom_fields.html' %}
|
||||||
|
{% include 'inc/panels/tags.html' %}
|
||||||
|
{% plugin_left_page object %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="col col-md-6">
|
||||||
<div class="col col-md-12">
|
<div class="card">
|
||||||
{% plugin_full_width_page object %}
|
<h5 class="card-header">Termination A</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'dcim/inc/cable_termination.html' with terminations=object.get_a_terminations %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header">Termination B</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% include 'dcim/inc/cable_termination.html' with terminations=object.get_b_terminations %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
{% plugin_full_width_page object %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -1,186 +0,0 @@
|
|||||||
{% extends 'base/layout.html' %}
|
|
||||||
{% load static %}
|
|
||||||
{% load helpers %}
|
|
||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% block title %}Connect {{ form.instance.termination_a.device }} {{ form.instance.termination_a }} to {{ termination_b_type|bettertitle }}{% endblock %}
|
|
||||||
|
|
||||||
{% block tabs %}
|
|
||||||
<ul class="nav nav-tabs px-3">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a href="#" role="tab" data-bs-toggle="tab" class="nav-link active">Connect Cable</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content-wrapper %}
|
|
||||||
<div class="tab-content">
|
|
||||||
{% with termination_a=form.instance.termination_a %}
|
|
||||||
{% render_errors form %}
|
|
||||||
<form method="post">
|
|
||||||
{% csrf_token %}
|
|
||||||
{% for field in form.hidden_fields %}
|
|
||||||
{{ field }}
|
|
||||||
{% endfor %}
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col col-md-5">
|
|
||||||
<div class="card h-100">
|
|
||||||
<h5 class="card-header offset-sm-3">A Side</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if termination_a.device %}
|
|
||||||
{# Device component #}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Region</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device.site.region }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Site Group</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device.site.group }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Site</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device.site }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Location</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device.location|default:"None" }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Rack</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device.rack|default:"None" }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Device</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.device }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Type</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a|meta:"verbose_name"|capfirst }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Name</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{# Circuit termination #}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label">Site</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.site }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label">Provider</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.circuit.provider }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label">Circuit</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.circuit.cid }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label">Side</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_a.term_side }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-2 flex-column justify-content-center align-items-center d-none d-md-flex">
|
|
||||||
<i class="mdi mdi-swap-horizontal-bold mdi-48px"></i>
|
|
||||||
</div>
|
|
||||||
<div class="col col-md-5">
|
|
||||||
<div class="card h-100">
|
|
||||||
<h5 class="card-header offset-sm-3">B Side</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if tabs %}
|
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
{% for url, link in tabs %}
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<a class="nav-link" href="{{ url }}">{{ link }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_provider' in form.fields %}
|
|
||||||
{% render_field form.termination_b_provider %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_region' in form.fields %}
|
|
||||||
{% render_field form.termination_b_region %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_sitegroup' in form.fields %}
|
|
||||||
{% render_field form.termination_b_sitegroup %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_site' in form.fields %}
|
|
||||||
{% render_field form.termination_b_site %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_location' in form.fields %}
|
|
||||||
{% render_field form.termination_b_location %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_rack' in form.fields %}
|
|
||||||
{% render_field form.termination_b_rack %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_device' in form.fields %}
|
|
||||||
{% render_field form.termination_b_device %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_type' in form.fields %}
|
|
||||||
{% render_field form.termination_b_type %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_powerpanel' in form.fields %}
|
|
||||||
{% render_field form.termination_b_powerpanel %}
|
|
||||||
{% endif %}
|
|
||||||
{% if 'termination_b_circuit' in form.fields %}
|
|
||||||
{% render_field form.termination_b_circuit %}
|
|
||||||
{% endif %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">Type</label>
|
|
||||||
<div class="col">
|
|
||||||
<input class="form-control" value="{{ termination_b_type|capfirst }}" disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.termination_b_id %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row my-3 justify-content-center">
|
|
||||||
<div class="col col-md-8">
|
|
||||||
<div class="card">
|
|
||||||
<h5 class="card-header offset-sm-3">Cable</h5>
|
|
||||||
<div class="card-body">
|
|
||||||
{% include 'dcim/inc/cable_form.html' %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row my-3">
|
|
||||||
<div class="col col-md-12 text-center">
|
|
||||||
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
|
||||||
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{% endwith %}
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,5 +1,125 @@
|
|||||||
{% extends 'generic/object_edit.html' %}
|
{% extends 'base/layout.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load helpers %}
|
||||||
|
{% load form_helpers %}
|
||||||
|
|
||||||
{% block form %}
|
{% block title %}Connect Cable{% endblock %}
|
||||||
{% include 'dcim/inc/cable_form.html' %}
|
|
||||||
|
{% block tabs %}
|
||||||
|
<ul class="nav nav-tabs px-3">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<a href="#" role="tab" data-bs-toggle="tab" class="nav-link active">Connect Cable</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content-wrapper %}
|
||||||
|
<div class="tab-content">
|
||||||
|
{% render_errors form %}
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<div class="card h-100">
|
||||||
|
<h5 class="card-header offset-sm-3">A Side</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% render_field form.termination_a_region %}
|
||||||
|
{% render_field form.termination_a_sitegroup %}
|
||||||
|
{% render_field form.termination_a_site %}
|
||||||
|
{% render_field form.termination_a_location %}
|
||||||
|
{% if 'termination_a_rack' in form.fields %}
|
||||||
|
{% render_field form.termination_a_rack %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_a_device' in form.fields %}
|
||||||
|
{% render_field form.termination_a_device %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_a_powerpanel' in form.fields %}
|
||||||
|
{% render_field form.termination_a_powerpanel %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_a_provider' in form.fields %}
|
||||||
|
{% render_field form.termination_a_provider %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_a_circuit' in form.fields %}
|
||||||
|
{% render_field form.termination_a_circuit %}
|
||||||
|
{% endif %}
|
||||||
|
{% render_field form.a_terminations %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-2 flex-column justify-content-center align-items-center d-none d-md-flex">
|
||||||
|
<i class="mdi mdi-swap-horizontal-bold mdi-48px"></i>
|
||||||
|
</div>
|
||||||
|
<div class="col col-md-5">
|
||||||
|
<div class="card h-100">
|
||||||
|
<h5 class="card-header offset-sm-3">B Side</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% render_field form.termination_b_region %}
|
||||||
|
{% render_field form.termination_b_sitegroup %}
|
||||||
|
{% render_field form.termination_b_site %}
|
||||||
|
{% render_field form.termination_b_location %}
|
||||||
|
{% if 'termination_b_rack' in form.fields %}
|
||||||
|
{% render_field form.termination_b_rack %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_b_device' in form.fields %}
|
||||||
|
{% render_field form.termination_b_device %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_b_powerpanel' in form.fields %}
|
||||||
|
{% render_field form.termination_b_powerpanel %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_b_provider' in form.fields %}
|
||||||
|
{% render_field form.termination_b_provider %}
|
||||||
|
{% endif %}
|
||||||
|
{% if 'termination_b_circuit' in form.fields %}
|
||||||
|
{% render_field form.termination_b_circuit %}
|
||||||
|
{% endif %}
|
||||||
|
{% render_field form.b_terminations %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-3 justify-content-center">
|
||||||
|
<div class="col col-md-8">
|
||||||
|
<div class="card">
|
||||||
|
<h5 class="card-header offset-sm-3">Cable</h5>
|
||||||
|
<div class="card-body">
|
||||||
|
{% render_field form.status %}
|
||||||
|
{% render_field form.type %}
|
||||||
|
{% render_field form.tenant_group %}
|
||||||
|
{% render_field form.tenant %}
|
||||||
|
{% render_field form.label %}
|
||||||
|
{% render_field form.color %}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label text-lg-end">{{ form.length.label }}</label>
|
||||||
|
<div class="col-md-5">
|
||||||
|
{{ form.length }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
{{ form.length_unit }}
|
||||||
|
</div>
|
||||||
|
<div class="invalid-feedback"></div>
|
||||||
|
</div>
|
||||||
|
{% render_field form.tags %}
|
||||||
|
{% if form.custom_fields %}
|
||||||
|
<div class="field-group">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<h5 class="offset-sm-3">Custom Fields</h5>
|
||||||
|
</div>
|
||||||
|
{% render_custom_fields form %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row my-3">
|
||||||
|
<div class="col col-md-12 text-center">
|
||||||
|
<a href="{{ return_url }}" class="btn btn-outline-danger">Cancel</a>
|
||||||
|
<button type="submit" name="_update" class="btn btn-primary">Connect</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -111,28 +111,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Server Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='console-server-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Console Server Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Front Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Rear Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,28 +113,13 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Console Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleserverport_connect' termination_a_id=object.pk termination_b_type='console-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Console Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Front Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleserverport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Front Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.consoleserverport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}" class="dropdown-item">Rear Port</a>
|
||||||
class="dropdown-item"
|
|
||||||
href="{% url 'dcim:consoleserverport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}"
|
|
||||||
>
|
|
||||||
Rear Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -105,22 +105,22 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='interface' %}?return_url={{ object.get_absolute_url }}">Interface</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='console-server-port' %}?return_url={{ object.get_absolute_url }}">Console Server Port</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleserverport&return_url={{ object.get_absolute_url }}">Console Server Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='console-port' %}?return_url={{ object.get_absolute_url }}">Console Port</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.consoleport&return_url={{ object.get_absolute_url }}">Console Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}">Front Port</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}">Rear Port</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:frontport_connect' termination_a_id=object.pk termination_b_type='circuit-termination' %}?return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.frontport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
{% load form_helpers %}
|
|
||||||
|
|
||||||
{% render_field form.status %}
|
|
||||||
{% render_field form.type %}
|
|
||||||
{% render_field form.tenant_group %}
|
|
||||||
{% render_field form.tenant %}
|
|
||||||
{% render_field form.label %}
|
|
||||||
{% render_field form.color %}
|
|
||||||
<div class="row mb-3">
|
|
||||||
<label class="col-sm-3 col-form-label text-lg-end">{{ form.length.label }}</label>
|
|
||||||
<div class="col-md-5">
|
|
||||||
{{ form.length }}
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
{{ form.length_unit }}
|
|
||||||
</div>
|
|
||||||
<div class="invalid-feedback"></div>
|
|
||||||
</div>
|
|
||||||
{% render_field form.tags %}
|
|
||||||
{% if form.custom_fields %}
|
|
||||||
<div class="field-group">
|
|
||||||
<div class="row mb-3">
|
|
||||||
<h5 class="offset-sm-3">Custom Fields</h5>
|
|
||||||
</div>
|
|
||||||
{% render_custom_fields form %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
@ -1,42 +1,58 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
<table class="table table-hover panel-body attr-table">
|
<table class="table table-hover panel-body attr-table">
|
||||||
{% if termination.device %}
|
{% if terminations.0.device %}
|
||||||
{# Device component #}
|
{# Device component #}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Device</td>
|
<td>Site</td>
|
||||||
<td>{{ termination.device|linkify }}</td>
|
<td>{{ terminations.0.device.site|linkify }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Site</td>
|
<td>Rack</td>
|
||||||
<td>{{ termination.device.site|linkify }}</td>
|
<td>{{ terminations.0.device.rack|linkify|placeholder }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% if termination.device.rack %}
|
<tr>
|
||||||
<tr>
|
<td>Device</td>
|
||||||
<td>Rack</td>
|
<td>{{ terminations.0.device|linkify }}</td>
|
||||||
<td>{{ termination.device.rack|linkify }}</td>
|
</tr>
|
||||||
</tr>
|
<tr>
|
||||||
{% endif %}
|
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||||
<tr>
|
<td>
|
||||||
<td>Type</td>
|
{% for term in terminations %}
|
||||||
<td>{{ termination|meta:"verbose_name"|capfirst }}</td>
|
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||||
</tr>
|
{% endfor %}
|
||||||
<tr>
|
</td>
|
||||||
<td>Component</td>
|
</tr>
|
||||||
<td>{{ termination|linkify }}</td>
|
{% elif terminations.0.power_panel %}
|
||||||
</tr>
|
{# Power feed #}
|
||||||
{% else %}
|
<tr>
|
||||||
{# Circuit termination #}
|
<td>Site</td>
|
||||||
<tr>
|
<td>{{ terminations.0.power_panel.site|linkify }}</td>
|
||||||
<td>Provider</td>
|
</tr>
|
||||||
<td>{{ termination.circuit.provider|linkify }}</td>
|
<tr>
|
||||||
</tr>
|
<td>Power Panel</td>
|
||||||
<tr>
|
<td>{{ terminations.0.power_panel|linkify }}</td>
|
||||||
<td>Circuit</td>
|
</tr>
|
||||||
<td>{{ termination.circuit|linkify }}</td>
|
<tr>
|
||||||
</tr>
|
<td>{{ terminations.0|meta:"verbose_name"|capfirst }}</td>
|
||||||
<tr>
|
<td>
|
||||||
<td>Termination</td>
|
{% for term in terminations %}
|
||||||
<td>{{ termination }}</td>
|
{{ term|linkify }}{% if not forloop.last %},{% endif %}
|
||||||
</tr>
|
{% endfor %}
|
||||||
{% endif %}
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
{# Circuit termination #}
|
||||||
|
<tr>
|
||||||
|
<td>Provider</td>
|
||||||
|
<td>{{ terminations.0.circuit.provider|linkify }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Circuit</td>
|
||||||
|
<td>
|
||||||
|
{% for term in terminations %}
|
||||||
|
{{ term.circuit|linkify }} ({{ term }}){% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
</table>
|
</table>
|
||||||
|
@ -263,24 +263,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=object.pk termination_b_type='interface' %}?return_url={{ object.get_absolute_url }}">
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
|
||||||
Interface
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}">
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
|
||||||
Front Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}">
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
|
||||||
Rear Port
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{% url 'dcim:interface_connect' termination_a_id=object.pk termination_b_type='circuit-termination' %}?return_url={{ object.get_absolute_url }}">
|
<a class="dropdown-item" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.interface&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
||||||
Circuit Termination
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -158,8 +158,7 @@
|
|||||||
{% if not object.mark_connected and not object.cable %}
|
{% if not object.mark_connected and not object.cable %}
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<a href="{% url 'dcim:powerfeed_connect' termination_a_id=object.pk termination_b_type='power-port' %}?return_url={{ object.get_absolute_url }}"
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerfeed&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" class="btn btn-primary btn-sm float-end">
|
||||||
class="btn btn-primary btn-sm float-end">
|
|
||||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -111,7 +111,7 @@
|
|||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
Not Connected
|
Not Connected
|
||||||
{% if perms.dcim.add_cable %}
|
{% if perms.dcim.add_cable %}
|
||||||
<a href="{% url 'dcim:poweroutlet_connect' termination_a_id=object.pk termination_b_type='power-port' %}?return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.poweroutlet&a_terminations={{ object.pk }}&b_terminations_type=dcim.powerport&return_url={{ object.get_absolute_url }}" title="Connect" class="btn btn-primary btn-sm float-end">
|
||||||
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
<i class="mdi mdi-ethernet-cable" aria-hidden="true"></i> Connect
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -117,10 +117,10 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:powerport_connect' termination_a_id=object.pk termination_b_type='power-outlet' %}?return_url={{ object.get_absolute_url }}">Power Outlet</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.poweroutlet&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Outlet</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:powerport_connect' termination_a_id=object.pk termination_b_type='power-feed' %}?return_url={{ object.get_absolute_url }}">Power Feed</a>
|
<a href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.powerportport&a_terminations={{ object.pk }}&termination_b_type=dcim.powerfeed&return_url={{ object.get_absolute_url }}" class="dropdown-link">Power Feed</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
@ -101,16 +101,16 @@
|
|||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='interface' %}?return_url={{ object.get_absolute_url }}">Interface</a>
|
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.interface&return_url={{ object.get_absolute_url }}">Interface</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='front-port' %}?return_url={{ object.get_absolute_url }}">Front Port</a>
|
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.frontport&return_url={{ object.get_absolute_url }}">Front Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='rear-port' %}?return_url={{ object.get_absolute_url }}">Rear Port</a>
|
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=dcim.rearport&return_url={{ object.get_absolute_url }}">Rear Port</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-link" href="{% url 'dcim:rearport_connect' termination_a_id=object.pk termination_b_type='circuit-termination' %}?return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
<a class="dropdown-link" href="{% url 'dcim:cable_add' %}?a_terminations_type=dcim.rearport&a_terminations={{ object.pk }}&b_terminations_type=circuits.circuittermination&return_url={{ object.get_absolute_url }}">Circuit Termination</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</span>
|
</span>
|
||||||
|
@ -25,18 +25,16 @@ def update_connected_interfaces(instance, created, raw=False, **kwargs):
|
|||||||
if instance.interface_a.wireless_link != instance:
|
if instance.interface_a.wireless_link != instance:
|
||||||
logger.debug(f"Updating interface A for wireless link {instance}")
|
logger.debug(f"Updating interface A for wireless link {instance}")
|
||||||
instance.interface_a.wireless_link = instance
|
instance.interface_a.wireless_link = instance
|
||||||
instance.interface_a._link_peer = instance.interface_b
|
|
||||||
instance.interface_a.save()
|
instance.interface_a.save()
|
||||||
if instance.interface_b.cable != instance:
|
if instance.interface_b.cable != instance:
|
||||||
logger.debug(f"Updating interface B for wireless link {instance}")
|
logger.debug(f"Updating interface B for wireless link {instance}")
|
||||||
instance.interface_b.wireless_link = instance
|
instance.interface_b.wireless_link = instance
|
||||||
instance.interface_b._link_peer = instance.interface_a
|
|
||||||
instance.interface_b.save()
|
instance.interface_b.save()
|
||||||
|
|
||||||
# Create/update cable paths
|
# Create/update cable paths
|
||||||
if created:
|
if created:
|
||||||
for interface in (instance.interface_a, instance.interface_b):
|
for interface in (instance.interface_a, instance.interface_b):
|
||||||
create_cablepath(interface)
|
create_cablepath([interface])
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=WirelessLink)
|
@receiver(post_delete, sender=WirelessLink)
|
||||||
@ -48,19 +46,11 @@ def nullify_connected_interfaces(instance, **kwargs):
|
|||||||
|
|
||||||
if instance.interface_a is not None:
|
if instance.interface_a is not None:
|
||||||
logger.debug(f"Nullifying interface A for wireless link {instance}")
|
logger.debug(f"Nullifying interface A for wireless link {instance}")
|
||||||
Interface.objects.filter(pk=instance.interface_a.pk).update(
|
Interface.objects.filter(pk=instance.interface_a.pk).update(wireless_link=None)
|
||||||
wireless_link=None,
|
|
||||||
_link_peer_type=None,
|
|
||||||
_link_peer_id=None
|
|
||||||
)
|
|
||||||
if instance.interface_b is not None:
|
if instance.interface_b is not None:
|
||||||
logger.debug(f"Nullifying interface B for wireless link {instance}")
|
logger.debug(f"Nullifying interface B for wireless link {instance}")
|
||||||
Interface.objects.filter(pk=instance.interface_b.pk).update(
|
Interface.objects.filter(pk=instance.interface_b.pk).update(wireless_link=None)
|
||||||
wireless_link=None,
|
|
||||||
_link_peer_type=None,
|
|
||||||
_link_peer_id=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete and retrace any dependent cable paths
|
# Delete and retrace any dependent cable paths
|
||||||
for cablepath in CablePath.objects.filter(path__contains=instance):
|
for cablepath in CablePath.objects.filter(_nodes__contains=instance):
|
||||||
cablepath.delete()
|
cablepath.delete()
|
||||||
|
Loading…
Reference in New Issue
Block a user