mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 04:22:01 -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
|
||||
* [#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
|
||||
* [#6071](https://github.com/netbox-community/netbox/issues/6071) - Cable traces now traverse circuits
|
||||
|
||||
### 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/`
|
||||
* circuits.CircuitTermination
|
||||
* Added the `provider_network` field
|
||||
* Removed the `connected_endpoint`, `connected_endpoint_type`, and `connected_endpoint_reachable` fields
|
||||
* circuits.ProviderNetwork
|
||||
* Added the `/api/circuits/provider-networks/` endpoint
|
||||
* 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')
|
||||
site = NestedSiteSerializer()
|
||||
provider_network = NestedProviderNetworkSerializer()
|
||||
@ -69,7 +69,6 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer, ConnectedEnd
|
||||
model = CircuitTermination
|
||||
fields = [
|
||||
'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')
|
||||
circuit = NestedCircuitSerializer()
|
||||
site = NestedSiteSerializer(required=False)
|
||||
@ -103,5 +102,5 @@ class CircuitTerminationSerializer(BaseModelSerializer, CableTerminationSerializ
|
||||
fields = [
|
||||
'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',
|
||||
'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 circuits import filters
|
||||
@ -60,7 +59,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class CircuitTerminationViewSet(PathEndpointMixin, ModelViewSet):
|
||||
queryset = CircuitTermination.objects.prefetch_related(
|
||||
'circuit', 'site', '_path__destination', 'cable'
|
||||
'circuit', 'site', 'provider_network', 'cable'
|
||||
)
|
||||
serializer_class = serializers.CircuitTerminationSerializer
|
||||
filterset_class = filters.CircuitTerminationFilterSet
|
||||
|
@ -207,7 +207,7 @@ class CircuitFilterSet(BaseFilterSet, CustomFieldModelFilterSet, TenancyFilterSe
|
||||
).distinct()
|
||||
|
||||
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet, PathEndpointFilterSet):
|
||||
class CircuitTerminationFilterSet(BaseFilterSet, CableTerminationFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='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')
|
||||
class CircuitTermination(ChangeLoggedModel, PathEndpoint, CableTermination):
|
||||
class CircuitTermination(ChangeLoggedModel, CableTermination):
|
||||
circuit = models.ForeignKey(
|
||||
to='circuits.Circuit',
|
||||
on_delete=models.CASCADE,
|
||||
|
@ -381,12 +381,6 @@ class CircuitTerminationTestCase(TestCase):
|
||||
params = {'cabled': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_connected(self):
|
||||
params = {'connected': True}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
params = {'connected': False}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 7)
|
||||
|
||||
|
||||
class ProviderNetworkTestCase(TestCase):
|
||||
queryset = ProviderNetwork.objects.all()
|
||||
|
@ -219,8 +219,6 @@ class CircuitView(generic.ObjectView):
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
|
||||
).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
|
||||
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
|
||||
@ -228,8 +226,6 @@ class CircuitView(generic.ObjectView):
|
||||
).filter(
|
||||
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
|
||||
).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 {
|
||||
'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.db import connection
|
||||
|
||||
from circuits.models import CircuitTermination
|
||||
from dcim.models import CablePath, ConsolePort, ConsoleServerPort, Interface, PowerFeed, PowerOutlet, PowerPort
|
||||
from dcim.signals import create_cablepath
|
||||
|
||||
ENDPOINT_MODELS = (
|
||||
CircuitTermination,
|
||||
ConsolePort,
|
||||
ConsoleServerPort,
|
||||
Interface,
|
||||
|
@ -394,6 +394,8 @@ class CablePath(BigIDModel):
|
||||
"""
|
||||
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:
|
||||
return None
|
||||
|
||||
@ -441,6 +443,23 @@ class CablePath(BigIDModel):
|
||||
# No corresponding FrontPort found for the RearPort
|
||||
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
|
||||
else:
|
||||
destination = peer_termination
|
||||
@ -486,15 +505,26 @@ class CablePath(BigIDModel):
|
||||
|
||||
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):
|
||||
"""
|
||||
Return a tuple containing the sum of the length of each cable in the path
|
||||
and a flag indicating whether the length is definitive.
|
||||
"""
|
||||
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)
|
||||
]
|
||||
cable_ids = self.get_cable_ids()
|
||||
cables = Cable.objects.filter(id__in=cable_ids, _abs_length__isnull=False)
|
||||
total_length = cables.aggregate(total=Sum('_abs_length'))['total']
|
||||
is_definitive = len(cables) == len(cable_ids)
|
||||
|
@ -160,7 +160,7 @@ class CableTermination(models.Model):
|
||||
class PathEndpoint(models.Model):
|
||||
"""
|
||||
An abstract model inherited by any CableTermination subclass which represents the end of a CablePath; specifically,
|
||||
these include ConsolePort, ConsoleServerPort, PowerPort, PowerOutlet, Interface, PowerFeed, and CircuitTermination.
|
||||
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
|
||||
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
|
||||
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)
|
||||
path.append(None)
|
||||
path.append(self._path.destination)
|
||||
path.insert(-1, None)
|
||||
|
||||
# Return the path as a list of three-tuples (A termination, cable, B termination)
|
||||
return list(zip(*[iter(path)] * 3))
|
||||
|
@ -1,10 +1,12 @@
|
||||
CABLETERMINATION = """
|
||||
{% if value %}
|
||||
{% if value.parent_object %}
|
||||
<a href="{{ value.parent_object.get_absolute_url }}">{{ value.parent_object }}</a>
|
||||
<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 %}
|
||||
—
|
||||
—
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
|
@ -229,40 +229,6 @@ class CablePathTestCase(TestCase):
|
||||
# Check that all CablePaths have been deleted
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
|
||||
def test_105_interface_to_circuittermination(self):
|
||||
"""
|
||||
[IF1] --C1-- [CT1A]
|
||||
"""
|
||||
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
|
||||
circuittermination1 = CircuitTermination.objects.create(circuit=self.circuit, site=self.site, term_side='A')
|
||||
|
||||
# Create cable 1
|
||||
cable1 = Cable(termination_a=interface1, termination_b=circuittermination1)
|
||||
cable1.save()
|
||||
path1 = self.assertPathExists(
|
||||
origin=interface1,
|
||||
destination=circuittermination1,
|
||||
path=(cable1,),
|
||||
is_active=True
|
||||
)
|
||||
path2 = self.assertPathExists(
|
||||
origin=circuittermination1,
|
||||
destination=interface1,
|
||||
path=(cable1,),
|
||||
is_active=True
|
||||
)
|
||||
self.assertEqual(CablePath.objects.count(), 2)
|
||||
interface1.refresh_from_db()
|
||||
circuittermination1.refresh_from_db()
|
||||
self.assertPathIsSet(interface1, path1)
|
||||
self.assertPathIsSet(circuittermination1, path2)
|
||||
|
||||
# Delete cable 1
|
||||
cable1.delete()
|
||||
|
||||
# Check that all CablePaths have been deleted
|
||||
self.assertEqual(CablePath.objects.count(), 0)
|
||||
|
||||
def test_201_single_path_via_pass_through(self):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP1] --C2-- [IF2]
|
||||
@ -820,6 +786,213 @@ class CablePathTestCase(TestCase):
|
||||
)
|
||||
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):
|
||||
"""
|
||||
[IF1] --C1-- [FP1] [RP2] --C2-- [RP2] [FP2] --C3-- [IF2]
|
||||
|
@ -104,21 +104,6 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
</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>
|
||||
<td>Cross-Connect</td>
|
||||
<td>{{ termination.xconnect_id|placeholder }}</td>
|
||||
|
@ -27,6 +27,8 @@
|
||||
{# Cable #}
|
||||
{% if cable %}
|
||||
{% include 'dcim/trace/cable.html' %}
|
||||
{% elif far_end %}
|
||||
{% include 'dcim/trace/attachment.html' %}
|
||||
{% endif %}
|
||||
|
||||
{# Far end #}
|
||||
@ -43,6 +45,8 @@
|
||||
{% if forloop.last %}
|
||||
{% include 'dcim/trace/circuit.html' with circuit=far_end.circuit %}
|
||||
{% endif %}
|
||||
{% elif far_end %}
|
||||
{% include 'dcim/trace/object.html' with object=far_end %}
|
||||
{% endif %}
|
||||
|
||||
{% 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