Skip to content

Commit 9b89af7

Browse files
authored
Fixes #20432: Allow cablepaths with CircuitTerminations that have different parent Circuit's (#20770)
1 parent 4961b0d commit 9b89af7

File tree

2 files changed

+133
-20
lines changed

2 files changed

+133
-20
lines changed

netbox/dcim/models/cables.py

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from core.models import ObjectType
1111
from dcim.choices import *
1212
from dcim.constants import *
13+
from dcim.exceptions import UnsupportedCablePath
1314
from dcim.fields import PathField
1415
from dcim.utils import decompile_path_node, object_to_path_node
1516
from netbox.choices import ColorChoices
@@ -28,8 +29,6 @@
2829
'CableTermination',
2930
)
3031

31-
from ..exceptions import UnsupportedCablePath
32-
3332
trace_paths = Signal()
3433

3534

@@ -615,7 +614,7 @@ def from_origin(cls, terminations):
615614
Cable or WirelessLink connects (interfaces, console ports, circuit termination, etc.). All terminations must be
616615
of the same type and must belong to the same parent object.
617616
"""
618-
from circuits.models import CircuitTermination
617+
from circuits.models import CircuitTermination, Circuit
619618

620619
if not terminations:
621620
return None
@@ -637,8 +636,11 @@ def from_origin(cls, terminations):
637636
raise UnsupportedCablePath(_("All mid-span terminations must have the same termination type"))
638637

639638
# All mid-span terminations must all be attached to the same device
640-
if (not isinstance(terminations[0], PathEndpoint) and not
641-
all(t.parent_object == terminations[0].parent_object for t in terminations[1:])):
639+
if (
640+
not isinstance(terminations[0], PathEndpoint) and
641+
not isinstance(terminations[0].parent_object, Circuit) and
642+
not all(t.parent_object == terminations[0].parent_object for t in terminations[1:])
643+
):
642644
raise UnsupportedCablePath(_("All mid-span terminations must have the same parent object"))
643645

644646
# Check for a split path (e.g. rear port fanning out to multiple front ports with
@@ -782,32 +784,39 @@ def from_origin(cls, terminations):
782784

783785
elif isinstance(remote_terminations[0], CircuitTermination):
784786
# Follow a CircuitTermination to its corresponding CircuitTermination (A to Z or vice versa)
785-
if len(remote_terminations) > 1:
786-
is_split = True
787-
break
788-
circuit_termination = CircuitTermination.objects.filter(
789-
circuit=remote_terminations[0].circuit,
790-
term_side='Z' if remote_terminations[0].term_side == 'A' else 'A'
791-
).first()
792-
if circuit_termination is None:
787+
qs = Q()
788+
for remote_termination in remote_terminations:
789+
qs |= Q(
790+
circuit=remote_termination.circuit,
791+
term_side='Z' if remote_termination.term_side == 'A' else 'A'
792+
)
793+
794+
# Get all circuit terminations
795+
circuit_terminations = CircuitTermination.objects.filter(qs)
796+
797+
if not circuit_terminations.exists():
793798
break
794-
elif circuit_termination._provider_network:
799+
elif all([ct._provider_network for ct in circuit_terminations]):
795800
# Circuit terminates to a ProviderNetwork
796801
path.extend([
797-
[object_to_path_node(circuit_termination)],
798-
[object_to_path_node(circuit_termination._provider_network)],
802+
[object_to_path_node(ct) for ct in circuit_terminations],
803+
[object_to_path_node(ct._provider_network) for ct in circuit_terminations],
799804
])
800805
is_complete = True
801806
break
802-
elif circuit_termination.termination and not circuit_termination.cable:
807+
elif all([ct.termination and not ct.cable for ct in circuit_terminations]):
803808
# Circuit terminates to a Region/Site/etc.
804809
path.extend([
805-
[object_to_path_node(circuit_termination)],
806-
[object_to_path_node(circuit_termination.termination)],
810+
[object_to_path_node(ct) for ct in circuit_terminations],
811+
[object_to_path_node(ct.termination) for ct in circuit_terminations],
807812
])
808813
break
814+
elif any([ct.cable in links for ct in circuit_terminations]):
815+
# No valid path
816+
is_split = True
817+
break
809818

810-
terminations = [circuit_termination]
819+
terminations = circuit_terminations
811820

812821
else:
813822
# Check for non-symmetric path

netbox/dcim/tests/test_cablepaths.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2270,6 +2270,80 @@ def test_222_single_path_via_multiple_singleposition_rear_ports(self):
22702270
CableTraceSVG(interface1).render()
22712271
CableTraceSVG(interface2).render()
22722272

2273+
def test_223_interface_to_interface_via_multiple_circuit_terminations(self):
2274+
provider = Provider.objects.first()
2275+
circuit_type = CircuitType.objects.first()
2276+
circuit1 = self.circuit
2277+
circuit2 = Circuit.objects.create(provider=provider, type=circuit_type, cid='Circuit 2')
2278+
interface1 = Interface.objects.create(device=self.device, name='Interface 1')
2279+
interface2 = Interface.objects.create(device=self.device, name='Interface 2')
2280+
circuittermination1_A = CircuitTermination.objects.create(
2281+
circuit=circuit1,
2282+
termination=self.site,
2283+
term_side='A'
2284+
)
2285+
circuittermination1_Z = CircuitTermination.objects.create(
2286+
circuit=circuit1,
2287+
termination=self.site,
2288+
term_side='Z'
2289+
)
2290+
circuittermination2_A = CircuitTermination.objects.create(
2291+
circuit=circuit2,
2292+
termination=self.site,
2293+
term_side='A'
2294+
)
2295+
circuittermination2_Z = CircuitTermination.objects.create(
2296+
circuit=circuit2,
2297+
termination=self.site,
2298+
term_side='Z'
2299+
)
2300+
2301+
# Create cables
2302+
cable1 = Cable(
2303+
a_terminations=[interface1],
2304+
b_terminations=[circuittermination1_A, circuittermination2_A]
2305+
)
2306+
cable2 = Cable(
2307+
a_terminations=[interface2],
2308+
b_terminations=[circuittermination1_Z, circuittermination2_Z]
2309+
)
2310+
cable1.save()
2311+
cable2.save()
2312+
2313+
self.assertEqual(CablePath.objects.count(), 2)
2314+
2315+
path1 = self.assertPathExists(
2316+
(
2317+
interface1,
2318+
cable1,
2319+
(circuittermination1_A, circuittermination2_A),
2320+
(circuittermination1_Z, circuittermination2_Z),
2321+
cable2,
2322+
interface2
2323+
2324+
),
2325+
is_active=True,
2326+
is_complete=True,
2327+
)
2328+
interface1.refresh_from_db()
2329+
self.assertPathIsSet(interface1, path1)
2330+
2331+
path2 = self.assertPathExists(
2332+
(
2333+
interface2,
2334+
cable2,
2335+
(circuittermination1_Z, circuittermination2_Z),
2336+
(circuittermination1_A, circuittermination2_A),
2337+
cable1,
2338+
interface1
2339+
2340+
),
2341+
is_active=True,
2342+
is_complete=True,
2343+
)
2344+
interface2.refresh_from_db()
2345+
self.assertPathIsSet(interface2, path2)
2346+
22732347
def test_301_create_path_via_existing_cable(self):
22742348
"""
22752349
[IF1] --C1-- [FP1] [RP1] --C2-- [RP2] [FP2] --C3-- [IF2]
@@ -2510,3 +2584,33 @@ def test_401_exclude_midspan_devices(self):
25102584
is_active=True
25112585
)
25122586
self.assertEqual(CablePath.objects.count(), 0)
2587+
2588+
def test_402_exclude_circuit_loopback(self):
2589+
interface = Interface.objects.create(device=self.device, name='Interface 1')
2590+
circuittermination1 = CircuitTermination.objects.create(
2591+
circuit=self.circuit,
2592+
termination=self.site,
2593+
term_side='A'
2594+
)
2595+
circuittermination2 = CircuitTermination.objects.create(
2596+
circuit=self.circuit,
2597+
termination=self.site,
2598+
term_side='Z'
2599+
)
2600+
2601+
# Create cables
2602+
cable = Cable(
2603+
a_terminations=[interface],
2604+
b_terminations=[circuittermination1, circuittermination2]
2605+
)
2606+
cable.save()
2607+
2608+
path = self.assertPathExists(
2609+
(interface, cable, (circuittermination1, circuittermination2)),
2610+
is_active=True,
2611+
is_complete=False,
2612+
is_split=True
2613+
)
2614+
self.assertEqual(CablePath.objects.count(), 1)
2615+
interface.refresh_from_db()
2616+
self.assertPathIsSet(interface, path)

0 commit comments

Comments
 (0)