CablePath.from_origin() discards multi-position cable positions during profile-based tracing #163

Closed
opened 2026-04-05 16:22:34 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @jsenecal on 3/12/2026

NetBox Edition

NetBox Community

NetBox Version

v4.5.4

Python Version

3.12

Steps to Reproduce

This bug is in CablePath.from_origin() (netbox/dcim/models/cables.py). It affects profile-based cable tracing when a single origin carries multiple positions (e.g., duplex fiber).

Setup topology (duplex fiber through patch panels with splice closure):

[SW-A IF1] ─C1(1C2P)─ [FP1(pos=2)][RP1(pos=2)] ─C2(1C2P)─ [RP2(pos=2)][FP2] ─C3(splice)─ [FP4][RP3(pos=2)] ─C4(1C2P)─ [RP4(pos=2)][FP6(pos=2)] ─C5(1C2P)─ [SW-B IF2]
                        pos1→RP1p1                                       [FP3] ─C6(splice)─ [FP5]              pos1→RP4p1
                        pos2→RP1p2                                                                              pos2→RP4p2
  1. Create a device with two interfaces (IF1, IF2)
  2. Create four RearPorts (RP1-RP4), each with positions=2
  3. Create six FrontPorts:
    • FP1 with positions=2 (duplex panel A)
    • FP2, FP3 with positions=1 (splice closure A, one per strand)
    • FP4, FP5 with positions=1 (splice closure B, one per strand)
    • FP6 with positions=2 (duplex panel B)
  4. Create PortMappings:
    • FP1 fp_pos=1 → RP1 rp_pos=1, FP1 fp_pos=2 → RP1 rp_pos=2
    • FP2 fp_pos=1 → RP2 rp_pos=1, FP3 fp_pos=1 → RP2 rp_pos=2
    • FP4 fp_pos=1 → RP3 rp_pos=1, FP5 fp_pos=1 → RP3 rp_pos=2
    • FP6 fp_pos=1 → RP4 rp_pos=1, FP6 fp_pos=2 → RP4 rp_pos=2
  5. Create cables:
    • C1: profile=single-1c2p, IF1 → FP1 (duplex patch)
    • C2: profile=single-1c2p, RP1 → RP2 (trunk)
    • C3: unprofiled, FP2 → FP4 (splice strand 1)
    • C6: unprofiled, FP3 → FP5 (splice strand 2)
    • C4: profile=single-1c2p, RP3 → RP4 (trunk)
    • C5: profile=single-1c2p, FP6 → IF2 (duplex patch)
  6. Trace the cable path from IF1

The same bug also manifests with unprofiled 1:2 cables (one interface to two separate single-position FrontPorts) when positions are seeded via PortMapping and then cross a profiled cable.

Root cause: two bugs in CablePath.from_origin():

Bug 1 (line 817): When seeding cable positions from a PathEndpoint, only the first position is pushed onto the position stack:

position_stack.append([terminations[0].cable_positions[0]])

For a duplex interface with cable_positions=[1, 2], only [1] is pushed.

Bug 2 (lines 858-861): When crossing a profiled cable, only the first position from the stack is used:

position = position_stack.pop()[0] if position_stack else None
term, position = cable_profile.get_peer_termination(terminations[0], position)
remote_terminations = [term]
position_stack.append([position])

Even if the stack correctly contained [1, 2], pop()[0] discards position 2.

Expected Behavior

The trace from IF1 should follow both positions through the entire path, producing a single CablePath that includes both strands of the duplex connection:

IF1 → C1 → FP1 → RP1 → C2 → RP2 → [FP2, FP3] → [C3, C6] → [FP4, FP5] → RP3 → C4 → RP4 → FP6 → C5 → IF2

Both fiber strands (positions 1 and 2) should be visible in the trace, with the splice closure FrontPorts appearing as multi-node path steps.

Observed Behavior

Only position 1 is traced. Position 2 is silently discarded at the first profiled cable crossing. The resulting CablePath shows only one strand:

IF1 → C1 → FP1 → RP1 → C2 → RP2 → FP2 → C3 → FP4 → RP3 → C4 → RP4 → FP6 → C5 → IF2

The second strand (FP3 → C6 → FP5) is completely missing from the trace. This affects any topology where a single interface carries multiple positions through profiled cables, which is the standard configuration for duplex fiber (TX/RX over a single SFP transceiver through one duplex LC connector).

Tests

The existing test_102_cable_profile_single_1c2p test uses separate interfaces per position (each with its own unprofiled patch cable to a single-position FrontPort). No single interface ever carries multiple positions, so the bug is never triggered.

*Originally created by @jsenecal on 3/12/2026* ### NetBox Edition NetBox Community ### NetBox Version v4.5.4 ### Python Version 3.12 ### Steps to Reproduce This bug is in `CablePath.from_origin()` (`netbox/dcim/models/cables.py`). It affects profile-based cable tracing when a single origin carries multiple positions (e.g., duplex fiber). **Setup topology (duplex fiber through patch panels with splice closure):** ``` [SW-A IF1] ─C1(1C2P)─ [FP1(pos=2)][RP1(pos=2)] ─C2(1C2P)─ [RP2(pos=2)][FP2] ─C3(splice)─ [FP4][RP3(pos=2)] ─C4(1C2P)─ [RP4(pos=2)][FP6(pos=2)] ─C5(1C2P)─ [SW-B IF2] pos1→RP1p1 [FP3] ─C6(splice)─ [FP5] pos1→RP4p1 pos2→RP1p2 pos2→RP4p2 ``` 1. Create a device with two interfaces (IF1, IF2) 2. Create four RearPorts (RP1-RP4), each with `positions=2` 3. Create six FrontPorts: - FP1 with `positions=2` (duplex panel A) - FP2, FP3 with `positions=1` (splice closure A, one per strand) - FP4, FP5 with `positions=1` (splice closure B, one per strand) - FP6 with `positions=2` (duplex panel B) 4. Create PortMappings: - FP1 fp_pos=1 → RP1 rp_pos=1, FP1 fp_pos=2 → RP1 rp_pos=2 - FP2 fp_pos=1 → RP2 rp_pos=1, FP3 fp_pos=1 → RP2 rp_pos=2 - FP4 fp_pos=1 → RP3 rp_pos=1, FP5 fp_pos=1 → RP3 rp_pos=2 - FP6 fp_pos=1 → RP4 rp_pos=1, FP6 fp_pos=2 → RP4 rp_pos=2 5. Create cables: - C1: profile=`single-1c2p`, IF1 → FP1 (duplex patch) - C2: profile=`single-1c2p`, RP1 → RP2 (trunk) - C3: unprofiled, FP2 → FP4 (splice strand 1) - C6: unprofiled, FP3 → FP5 (splice strand 2) - C4: profile=`single-1c2p`, RP3 → RP4 (trunk) - C5: profile=`single-1c2p`, FP6 → IF2 (duplex patch) 6. Trace the cable path from IF1 The same bug also manifests with unprofiled 1:2 cables (one interface to two separate single-position FrontPorts) when positions are seeded via PortMapping and then cross a profiled cable. **Root cause: two bugs in `CablePath.from_origin()`:** **Bug 1 (line 817):** When seeding cable positions from a PathEndpoint, only the first position is pushed onto the position stack: ```python position_stack.append([terminations[0].cable_positions[0]]) ``` For a duplex interface with `cable_positions=[1, 2]`, only `[1]` is pushed. **Bug 2 (lines 858-861):** When crossing a profiled cable, only the first position from the stack is used: ```python position = position_stack.pop()[0] if position_stack else None term, position = cable_profile.get_peer_termination(terminations[0], position) remote_terminations = [term] position_stack.append([position]) ``` Even if the stack correctly contained `[1, 2]`, `pop()[0]` discards position 2. ### Expected Behavior The trace from IF1 should follow **both** positions through the entire path, producing a single CablePath that includes both strands of the duplex connection: ``` IF1 → C1 → FP1 → RP1 → C2 → RP2 → [FP2, FP3] → [C3, C6] → [FP4, FP5] → RP3 → C4 → RP4 → FP6 → C5 → IF2 ``` Both fiber strands (positions 1 and 2) should be visible in the trace, with the splice closure FrontPorts appearing as multi-node path steps. ### Observed Behavior Only position 1 is traced. Position 2 is silently discarded at the first profiled cable crossing. The resulting CablePath shows only one strand: ``` IF1 → C1 → FP1 → RP1 → C2 → RP2 → FP2 → C3 → FP4 → RP3 → C4 → RP4 → FP6 → C5 → IF2 ``` The second strand (FP3 → C6 → FP5) is completely missing from the trace. This affects any topology where a single interface carries multiple positions through profiled cables, which is the standard configuration for duplex fiber (TX/RX over a single SFP transceiver through one duplex LC connector). ### Tests The existing `test_102_cable_profile_single_1c2p` test uses separate interfaces per position (each with its own unprofiled patch cable to a single-position FrontPort). No single interface ever carries multiple positions, so the bug is never triggered.
Sign in to join this conversation.
No Label netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox netbox severity: medium severity: medium severity: medium severity: medium severity: medium status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted status: accepted type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug type: bug
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/netbox#163