mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-24 17:38:37 -06:00
Closes #6071: Cable traces now traverse circuits
This commit is contained in:
parent
d57222328b
commit
96759af86f
@ -112,6 +112,7 @@ A new provider network model has been introduced to represent the boundary of a
|
|||||||
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
|
* [#5990](https://github.com/netbox-community/netbox/issues/5990) - Deprecated `display_field` parameter for custom script ObjectVar and MultiObjectVar fields
|
||||||
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
|
* [#5995](https://github.com/netbox-community/netbox/issues/5995) - Dropped backward compatibility for `queryset` parameter on ObjectVar and MultiObjectVar (use `model` instead)
|
||||||
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
|
* [#6014](https://github.com/netbox-community/netbox/issues/6014) - Moved the virtual machine interfaces list to a separate view
|
||||||
|
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits
|
||||||
|
|
||||||
### REST API Changes
|
### REST API Changes
|
||||||
|
|
||||||
@ -131,6 +132,7 @@ A new provider network model has been introduced to represent the boundary of a
|
|||||||
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
|
* The `/dcim/rack-groups/` endpoint is now `/dcim/locations/`
|
||||||
* circuits.CircuitTermination
|
* circuits.CircuitTermination
|
||||||
* Added the `provider_network` field
|
* Added the `provider_network` field
|
||||||
|
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
|
||||||
* circuits.ProviderNetwork
|
* circuits.ProviderNetwork
|
||||||
* Added the `/api/circuits/provider-networks/` endpoint
|
* Added the `/api/circuits/provider-networks/` endpoint
|
||||||
* dcim.Device
|
* dcim.Device
|
||||||
|
@ -60,7 +60,7 @@ class CircuitTypeSerializer(OrganizationalModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEndpointSerializer):
|
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
provider_network = NestedProviderNetworkSerializer()
|
provider_network = NestedProviderNetworkSerializer()
|
||||||
@ -69,7 +69,6 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEnd
|
|||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id',
|
||||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -91,7 +90,7 @@ class CircuitSerializer(PrimaryModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer, ConnectedEndpointSerializer):
|
class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializer):
|
||||||
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)
|
site = NestedSiteSerializer(required=False)
|
||||||
@ -103,5 +102,5 @@ class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializ
|
|||||||
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', 'cable_peer', 'cable_peer_type',
|
'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_peer', 'cable_peer_type',
|
||||||
'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', '_occupied',
|
'_occupied',
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.db.models import Prefetch
|
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from circuits import filters
|
from circuits import filters
|
||||||
@ -60,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||||
queryset = CircuitTermination.objects.prefetch_related(
|
queryset = CircuitTermination.objects.prefetch_related(
|
||||||
'circuit', 'site', '_path__destination', 'cable'
|
'circuit', 'site', 'provider_network', 'cable'
|
||||||
)
|
)
|
||||||
serializer_class = serializers.CircuitTerminationSerializer
|
serializer_class = serializers.CircuitTerminationSerializer
|
||||||
filterset_class = filters.CircuitTerminationFilterSet
|
filterset_class = filters.CircuitTerminationFilterSet
|
||||||
|
@ -207,7 +207,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
|||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
|
32
netbox/circuits/migrations/0029_circuit_tracing.py
Normal file
32
netbox/circuits/migrations/0029_circuit_tracing.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
|
||||||
|
def delete_obsolete_cablepaths(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Delete all CablePath instances which originate or terminate at a CircuitTermination.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
CircuitTermination = apps.get_model('circuits', 'CircuitTermination')
|
||||||
|
CablePath = apps.get_model('dcim', 'CablePath')
|
||||||
|
|
||||||
|
ct = ContentType.objects.get_for_model(CircuitTermination)
|
||||||
|
CablePath.objects.filter(Q(origin_type=ct) | Q(destination_type=ct)).delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0028_cache_circuit_terminations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='circuittermination',
|
||||||
|
name='_path',
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
code=delete_obsolete_cablepaths,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
]
|
@ -294,7 +294,7 @@ class Circuit(PrimaryModel):
|
|||||||
|
|
||||||
|
|
||||||
@extras_features('webhooks')
|
@extras_features('webhooks')
|
||||||
class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
|
class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||||
circuit = models.ForeignKey(
|
circuit = models.ForeignKey(
|
||||||
to='circuits.Circuit',
|
to='circuits.Circuit',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
|
@ -381,12 +381,6 @@ class CircuitTerminationTestCase(TestCase):
|
|||||||
params = {'cabled': True}
|
params = {'cabled': True}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_connected(self):
|
|
||||||
params = {'connected': True}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
|
||||||
params = {'connected': False}
|
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
|
||||||
|
|
||||||
|
|
||||||
class ProviderNetworkTestCase(TestCase):
|
class ProviderNetworkTestCase(TestCase):
|
||||||
queryset = ProviderNetwork.objects.all()
|
queryset = ProviderNetwork.objects.all()
|
||||||
|
@ -219,8 +219,6 @@ class CircuitView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||||
).first()
|
).first()
|
||||||
if termination_a and termination_a.connected_endpoint and hasattr(termination_a.connected_endpoint, 'ip_addresses'):
|
|
||||||
termination_a.ip_addresses = termination_a.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
|
||||||
|
|
||||||
# Z-side termination
|
# Z-side termination
|
||||||
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||||
@ -228,8 +226,6 @@ class CircuitView(generic.ObjectView):
|
|||||||
).filter(
|
).filter(
|
||||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||||
).first()
|
).first()
|
||||||
if termination_z and termination_z.connected_endpoint and hasattr(termination_z.connected_endpoint, 'ip_addresses'):
|
|
||||||
termination_z.ip_addresses = termination_z.connected_endpoint.ip_addresses.restrict(request.user, 'view')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'termination_a': termination_a,
|
'termination_a': termination_a,
|
||||||
|
@ -2,12 +2,10 @@ from django.core.management.base import BaseCommand
|
|||||||
from django.core.management.color import no_style
|
from django.core.management.color import no_style
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
from circuits.models import CircuitTermination
|
|
||||||
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
||||||
from dcim.signals import create_cablepath
|
from dcim.signals import create_cablepath
|
||||||
|
|
||||||
ENDPOINT_MODELS = (
|
ENDPOINT_MODELS = (
|
||||||
CircuitTermination,
|
|
||||||
ConsolePort,
|
ConsolePort,
|
||||||
ConsoleServerPort,
|
ConsoleServerPort,
|
||||||
Interface,
|
Interface,
|
||||||
|
@ -394,6 +394,8 @@ class CablePath(BigIDModel):
|
|||||||
"""
|
"""
|
||||||
Create a new CablePath instance as traced from the given path origin.
|
Create a new CablePath instance as traced from the given path origin.
|
||||||
"""
|
"""
|
||||||
|
from circuits.models import CircuitTermination
|
||||||
|
|
||||||
if origin is None or origin.cable is None:
|
if origin is None or origin.cable is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -441,6 +443,23 @@ class CablePath(BigIDModel):
|
|||||||
# No corresponding FrontPort found for the RearPort
|
# No corresponding FrontPort found for the RearPort
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
|
||||||
|
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
|
destination = peer_termination
|
||||||
@ -486,15 +505,26 @@ class CablePath(BigIDModel):
|
|||||||
|
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
def get_cable_ids(self):
|
||||||
|
"""
|
||||||
|
Return all Cable IDs within the path.
|
||||||
|
"""
|
||||||
|
cable_ct = ContentType.objects.get_for_model(Cable).pk
|
||||||
|
cable_ids = []
|
||||||
|
|
||||||
|
for node in self.path:
|
||||||
|
ct, id = decompile_path_node(node)
|
||||||
|
if ct == cable_ct:
|
||||||
|
cable_ids.append(id)
|
||||||
|
|
||||||
|
return cable_ids
|
||||||
|
|
||||||
def get_total_length(self):
|
def get_total_length(self):
|
||||||
"""
|
"""
|
||||||
Return a tuple containing the sum of the length of each cable in the path
|
Return a tuple containing the sum of the length of each cable in the path
|
||||||
and a flag indicating whether the length is definitive.
|
and a flag indicating whether the length is definitive.
|
||||||
"""
|
"""
|
||||||
cable_ids = [
|
cable_ids = self.get_cable_ids()
|
||||||
# Starting from the first element, every third element in the path should be a Cable
|
|
||||||
decompile_path_node(self.path[i])[1] for i in range(0, len(self.path), 3)
|
|
||||||
]
|
|
||||||
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
||||||
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
||||||
is_definitive = len(cables) == len(cable_ids)
|
is_definitive = len(cables) == len(cable_ids)
|
||||||
|
@ -160,7 +160,7 @@ class CableTermination(models.Model):
|
|||||||
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 CableTermination subclass which represents the end of a CablePath; specifically,
|
||||||
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination.
|
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
|
||||||
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
|
dcim.signals in response to changes in the cable path, and complements the `origin` GenericForeignKey field on the
|
||||||
@ -184,10 +184,11 @@ class PathEndpoint(models.Model):
|
|||||||
|
|
||||||
# Construct the complete path
|
# Construct the complete path
|
||||||
path = [self, *self._path.get_path()]
|
path = [self, *self._path.get_path()]
|
||||||
while (len(path) + 1) % 3:
|
if self._path.destination:
|
||||||
|
path.append(self._path.destination)
|
||||||
|
while len(path) % 3:
|
||||||
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
|
# Pad to ensure we have complete three-tuples (e.g. for paths that end at a RearPort)
|
||||||
path.append(None)
|
path.insert(-1, None)
|
||||||
path.append(self._path.destination)
|
|
||||||
|
|
||||||
# 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, cable, B termination)
|
||||||
return list(zip(*[iter(path)] * 3))
|
return list(zip(*[iter(path)] * 3))
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
CABLETERMINATION = """
|
CABLETERMINATION = """
|
||||||
{% if value %}
|
{% if value %}
|
||||||
|
{% if value.parent_object %}
|
||||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||||
<i class="mdi mdi-chevron-right"></i>
|
<i class="mdi mdi-chevron-right"></i>
|
||||||
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
{% endif %}
|
||||||
|
<a href="{{ value.get_absolute_url }}">{{ value }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
—
|
—
|
||||||
{% endif %}
|
{% endif %}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -229,40 +229,6 @@ class CablePathTestCase(TestCase):
|
|||||||
# Check that all CablePaths have been deleted
|
# Check that all CablePaths have been deleted
|
||||||
self.assertEqual(CablePath.objects.count(), 0)
|
self.assertEqual(CablePath.objects.count(), 0)
|
||||||
|
|
||||||
def test_105_interface_to_circuittermination(self):
|
|
||||||
"""
|
|
||||||
[IF1] --C1-- [CT1A]
|
|
||||||
"""
|
|
||||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
|
||||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
|
||||||
|
|
||||||
# Create cable 1
|
|
||||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
|
||||||
cable1.save()
|
|
||||||
path1 = self.assertPathExists(
|
|
||||||
origin=interface1,
|
|
||||||
destination=circuittermination1,
|
|
||||||
path=(cable1,),
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
path2 = self.assertPathExists(
|
|
||||||
origin=circuittermination1,
|
|
||||||
destination=interface1,
|
|
||||||
path=(cable1,),
|
|
||||||
is_active=True
|
|
||||||
)
|
|
||||||
self.assertEqual(CablePath.objects.count(), 2)
|
|
||||||
interface1.refresh_from_db()
|
|
||||||
circuittermination1.refresh_from_db()
|
|
||||||
self.assertPathIsSet(interface1, path1)
|
|
||||||
self.assertPathIsSet(circuittermination1, path2)
|
|
||||||
|
|
||||||
# Delete cable 1
|
|
||||||
cable1.delete()
|
|
||||||
|
|
||||||
# Check that all CablePaths have been deleted
|
|
||||||
self.assertEqual(CablePath.objects.count(), 0)
|
|
||||||
|
|
||||||
def test_201_single_path_via_pass_through(self):
|
def test_201_single_path_via_pass_through(self):
|
||||||
"""
|
"""
|
||||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||||
@ -820,6 +786,213 @@ class CablePathTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(CablePath.objects.count(), 1)
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_208_circuittermination(self):
|
||||||
|
"""
|
||||||
|
[IF1] --C1-- [CT1]
|
||||||
|
"""
|
||||||
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||||
|
|
||||||
|
# Create cable 1
|
||||||
|
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||||
|
cable1.save()
|
||||||
|
|
||||||
|
# Check for incomplete path
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=None,
|
||||||
|
path=(cable1, circuittermination1),
|
||||||
|
is_active=False
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
|
||||||
|
# Delete cable 1
|
||||||
|
cable1.delete()
|
||||||
|
self.assertEqual(CablePath.objects.count(), 0)
|
||||||
|
interface1.refresh_from_db()
|
||||||
|
self.assertPathIsNotSet(interface1)
|
||||||
|
|
||||||
|
def test_209_circuit_to_interface(self):
|
||||||
|
"""
|
||||||
|
[IF1] --C1-- [CT1] [CT2] --C2-- [IF2]
|
||||||
|
"""
|
||||||
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||||
|
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||||
|
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||||
|
|
||||||
|
# Create cables
|
||||||
|
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||||
|
cable1.save()
|
||||||
|
cable2 = Cable(termination_a=circuittermination2, termination_b=interface2)
|
||||||
|
cable2.save()
|
||||||
|
|
||||||
|
# Check for paths
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=interface2,
|
||||||
|
path=(cable1, circuittermination1, circuittermination2, cable2),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface2,
|
||||||
|
destination=interface1,
|
||||||
|
path=(cable2, circuittermination2, circuittermination1, cable1),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 2)
|
||||||
|
|
||||||
|
# Delete cable 2
|
||||||
|
cable2.delete()
|
||||||
|
path1 = self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=self.site,
|
||||||
|
path=(cable1, circuittermination1, circuittermination2),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
interface1.refresh_from_db()
|
||||||
|
interface2.refresh_from_db()
|
||||||
|
self.assertPathIsSet(interface1, path1)
|
||||||
|
self.assertPathIsNotSet(interface2)
|
||||||
|
|
||||||
|
def test_210_circuit_to_site(self):
|
||||||
|
"""
|
||||||
|
[IF1] --C1-- [CT1] [CT2] --> [Site2]
|
||||||
|
"""
|
||||||
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
site2 = Site.objects.create(name='Site 2', slug='site-2')
|
||||||
|
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||||
|
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=site2, term_side='Z')
|
||||||
|
|
||||||
|
# Create cable 1
|
||||||
|
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||||
|
cable1.save()
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=site2,
|
||||||
|
path=(cable1, circuittermination1, circuittermination2),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
|
||||||
|
# Delete cable 1
|
||||||
|
cable1.delete()
|
||||||
|
self.assertEqual(CablePath.objects.count(), 0)
|
||||||
|
interface1.refresh_from_db()
|
||||||
|
self.assertPathIsNotSet(interface1)
|
||||||
|
|
||||||
|
def test_211_circuit_to_providernetwork(self):
|
||||||
|
"""
|
||||||
|
[IF1] --C1-- [CT1] [CT2] --> [PN1]
|
||||||
|
"""
|
||||||
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
providernetwork = ProviderNetwork.objects.create(name='Provider Network 1', provider=self.circuit.provider)
|
||||||
|
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||||
|
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, provider_network=providernetwork, term_side='Z')
|
||||||
|
|
||||||
|
# Create cable 1
|
||||||
|
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||||
|
cable1.save()
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=providernetwork,
|
||||||
|
path=(cable1, circuittermination1, circuittermination2),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 1)
|
||||||
|
|
||||||
|
# Delete cable 1
|
||||||
|
cable1.delete()
|
||||||
|
self.assertEqual(CablePath.objects.count(), 0)
|
||||||
|
interface1.refresh_from_db()
|
||||||
|
self.assertPathIsNotSet(interface1)
|
||||||
|
|
||||||
|
def test_212_multiple_paths_via_circuit(self):
|
||||||
|
"""
|
||||||
|
[IF1] --C1-- [FP1:1] [RP1] --C3-- [CT1] [CT2] --C4-- [RP2] [FP2:1] --C5-- [IF3]
|
||||||
|
[IF2] --C2-- [FP1:2] [FP2:2] --C6-- [IF4]
|
||||||
|
"""
|
||||||
|
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||||
|
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
|
||||||
|
interface3 = Interface.objects.create(device=self.device, name='Interface 3')
|
||||||
|
interface4 = Interface.objects.create(device=self.device, name='Interface 4')
|
||||||
|
rearport1 = RearPort.objects.create(device=self.device, name='Rear Port 1', positions=4)
|
||||||
|
rearport2 = RearPort.objects.create(device=self.device, name='Rear Port 2', positions=4)
|
||||||
|
frontport1_1 = FrontPort.objects.create(
|
||||||
|
device=self.device, name='Front Port 1:1', rear_port=rearport1, rear_port_position=1
|
||||||
|
)
|
||||||
|
frontport1_2 = FrontPort.objects.create(
|
||||||
|
device=self.device, name='Front Port 1:2', rear_port=rearport1, rear_port_position=2
|
||||||
|
)
|
||||||
|
frontport2_1 = FrontPort.objects.create(
|
||||||
|
device=self.device, name='Front Port 2:1', rear_port=rearport2, rear_port_position=1
|
||||||
|
)
|
||||||
|
frontport2_2 = FrontPort.objects.create(
|
||||||
|
device=self.device, name='Front Port 2:2', rear_port=rearport2, rear_port_position=2
|
||||||
|
)
|
||||||
|
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||||
|
circuittermination2 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='Z')
|
||||||
|
|
||||||
|
# Create cables
|
||||||
|
cable1 = Cable(termination_a=interface1, termination_b=frontport1_1) # IF1 -> FP1:1
|
||||||
|
cable1.save()
|
||||||
|
cable2 = Cable(termination_a=interface2, termination_b=frontport1_2) # IF2 -> FP1:2
|
||||||
|
cable2.save()
|
||||||
|
cable3 = Cable(termination_a=rearport1, termination_b=circuittermination1) # RP1 -> CT1
|
||||||
|
cable3.save()
|
||||||
|
cable4 = Cable(termination_a=rearport2, termination_b=circuittermination2) # RP2 -> CT2
|
||||||
|
cable4.save()
|
||||||
|
cable5 = Cable(termination_a=interface3, termination_b=frontport2_1) # IF3 -> FP2:1
|
||||||
|
cable5.save()
|
||||||
|
cable6 = Cable(termination_a=interface4, termination_b=frontport2_2) # IF4 -> FP2:2
|
||||||
|
cable6.save()
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface1,
|
||||||
|
destination=interface3,
|
||||||
|
path=(
|
||||||
|
cable1, frontport1_1, rearport1, cable3, circuittermination1, circuittermination2,
|
||||||
|
cable4, rearport2, frontport2_1, cable5
|
||||||
|
),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface2,
|
||||||
|
destination=interface4,
|
||||||
|
path=(
|
||||||
|
cable2, frontport1_2, rearport1, cable3, circuittermination1, circuittermination2,
|
||||||
|
cable4, rearport2, frontport2_2, cable6
|
||||||
|
),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface3,
|
||||||
|
destination=interface1,
|
||||||
|
path=(
|
||||||
|
cable5, frontport2_1, rearport2, cable4, circuittermination2, circuittermination1,
|
||||||
|
cable3, rearport1, frontport1_1, cable1
|
||||||
|
),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertPathExists(
|
||||||
|
origin=interface4,
|
||||||
|
destination=interface2,
|
||||||
|
path=(
|
||||||
|
cable6, frontport2_2, rearport2, cable4, circuittermination2, circuittermination1,
|
||||||
|
cable3, rearport1, frontport1_2, cable2
|
||||||
|
),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
self.assertEqual(CablePath.objects.count(), 4)
|
||||||
|
|
||||||
|
# Delete cables 3-4
|
||||||
|
cable3.delete()
|
||||||
|
cable4.delete()
|
||||||
|
|
||||||
|
# Check for four partial paths; one from each interface
|
||||||
|
self.assertEqual(CablePath.objects.filter(destination_id__isnull=True).count(), 4)
|
||||||
|
self.assertEqual(CablePath.objects.filter(destination_id__isnull=False).count(), 0)
|
||||||
|
|
||||||
def test_301_create_path_via_existing_cable(self):
|
def test_301_create_path_via_existing_cable(self):
|
||||||
"""
|
"""
|
||||||
[IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2]
|
[IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||||
|
@ -104,21 +104,6 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td>IP Addressing</td>
|
|
||||||
<td>
|
|
||||||
{% if termination.connected_endpoint %}
|
|
||||||
{% for ip in termination.ip_addresses %}
|
|
||||||
{% if not forloop.first %}<br />{% endif %}
|
|
||||||
<a href="{% url 'ipam:ipaddress' pk=ip.pk %}">{{ ip }}</a> ({{ ip.vrf|default:"Global" }})
|
|
||||||
{% empty %}
|
|
||||||
<span class="text-muted">None</span>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">—</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td>Cross-Connect</td>
|
<td>Cross-Connect</td>
|
||||||
<td>{{ termination.xconnect_id|placeholder }}</td>
|
<td>{{ termination.xconnect_id|placeholder }}</td>
|
||||||
|
@ -27,6 +27,8 @@
|
|||||||
{# Cable #}
|
{# Cable #}
|
||||||
{% if cable %}
|
{% if cable %}
|
||||||
{% include 'dcim/trace/cable.html' %}
|
{% include 'dcim/trace/cable.html' %}
|
||||||
|
{% elif far_end %}
|
||||||
|
{% include 'dcim/trace/attachment.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# Far end #}
|
{# Far end #}
|
||||||
@ -43,6 +45,8 @@
|
|||||||
{% if forloop.last %}
|
{% if forloop.last %}
|
||||||
{% include 'dcim/trace/circuit.html' with circuit=far_end.circuit %}
|
{% include 'dcim/trace/circuit.html' with circuit=far_end.circuit %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% elif far_end %}
|
||||||
|
{% include 'dcim/trace/object.html' with object=far_end %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if forloop.last %}
|
{% if forloop.last %}
|
||||||
|
5
netbox/templates/dcim/trace/attachment.html
Normal file
5
netbox/templates/dcim/trace/attachment.html
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div class="cable" style="border-left-color: #c0c0c0; border-left-style: dashed">
|
||||||
|
<strong>Attachment</strong>
|
||||||
|
</div>
|
3
netbox/templates/dcim/trace/object.html
Normal file
3
netbox/templates/dcim/trace/object.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div class="node">
|
||||||
|
<strong><a href="{{ object.get_absolute_url }}">{{ object }}</a></strong>
|
||||||
|
</div>
|
Loading…
Reference in New Issue
Block a user