Skip to content

Commit 85e527b

Browse files
authored
Merge pull request #741 from netbox-community/434-port-trace
434 Add /path to front and rear ports and Virtual Circuits
2 parents 51b0276 + 991df67 commit 85e527b

File tree

12 files changed

+824
-97
lines changed

12 files changed

+824
-97
lines changed

docs/api.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# API Core Classes
2+
3+
This page documents the core classes that form pyNetBox's API structure.
4+
5+
## Overview
6+
7+
PyNetBox uses a layered architecture to interact with NetBox:
8+
9+
1. **Api** - The main entry point that creates connections to NetBox
10+
2. **App** - Represents NetBox applications (dcim, ipam, circuits, etc.)
11+
3. **Endpoint** - Provides CRUD operations for specific API endpoints
12+
13+
```python
14+
import pynetbox
15+
16+
# Create API connection (Api class)
17+
nb = pynetbox.api('http://localhost:8000', token='your-token')
18+
19+
# Access an app (App class)
20+
nb.dcim # Returns an App instance
21+
22+
# Access an endpoint (Endpoint class)
23+
nb.dcim.devices # Returns an Endpoint instance
24+
25+
# Use endpoint methods
26+
devices = nb.dcim.devices.all()
27+
```
28+
29+
## Api Class
30+
31+
The `Api` class is the main entry point for interacting with NetBox. It manages the HTTP session, authentication, and provides access to NetBox applications.
32+
33+
::: pynetbox.core.api.Api
34+
handler: python
35+
options:
36+
members:
37+
- __init__
38+
- create_token
39+
- openapi
40+
- status
41+
- version
42+
- activate_branch
43+
show_source: true
44+
show_root_heading: true
45+
heading_level: 3
46+
47+
## App Class
48+
49+
The `App` class represents a NetBox application (such as dcim, ipam, circuits). When you access an attribute on the `Api` object, it returns an `App` instance. Accessing attributes on an `App` returns `Endpoint` objects.
50+
51+
::: pynetbox.core.app.App
52+
handler: python
53+
options:
54+
members:
55+
- config
56+
show_source: true
57+
show_root_heading: true
58+
heading_level: 3
59+
60+
## Relationship to Endpoints
61+
62+
When you access an attribute on an `App` object, it returns an [Endpoint](endpoint.md) instance:
63+
64+
```python
65+
# nb.dcim is an App instance
66+
# nb.dcim.devices is an Endpoint instance
67+
devices_endpoint = nb.dcim.devices
68+
69+
# Endpoint provides CRUD methods
70+
all_devices = devices_endpoint.all()
71+
device = devices_endpoint.get(1)
72+
new_device = devices_endpoint.create(name='test', site=1, device_type=1, device_role=1)
73+
```
74+
75+
See the [Endpoint documentation](endpoint.md) for details on available methods.

docs/circuits.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Circuits
2+
3+
This page documents special methods available for Circuits models in pyNetBox.
4+
5+
!!! note "Standard API Operations"
6+
Standard CRUD operations (`.all()`, `.filter()`, `.get()`, `.create()`, `.update()`, `.delete()`) follow NetBox's REST API patterns. Refer to the [NetBox API documentation](https://demo.netbox.dev/api/docs/) for details on available endpoints and filters.
7+
8+
## Circuit Terminations
9+
10+
### Cable Path Tracing
11+
12+
Circuit terminations support cable path tracing through the `paths()` method. This method returns all cable paths that traverse through the circuit termination, showing the complete connectivity from origin to destination.
13+
14+
**Example:**
15+
```python
16+
# Get a circuit termination
17+
circuit_term = nb.circuits.circuit_terminations.get(circuit_id=123, term_side='A')
18+
19+
# Get all cable paths through this termination
20+
paths = circuit_term.paths()
21+
22+
# Each path contains origin, destination, and path segments
23+
for path_info in paths:
24+
print(f"Origin: {path_info['origin']}")
25+
print(f"Destination: {path_info['destination']}")
26+
print("Path segments:")
27+
for segment in path_info['path']:
28+
for obj in segment:
29+
print(f" - {obj}")
30+
31+
# Example: Find what a circuit connects to
32+
circuit = nb.circuits.circuits.get(cid='CIRCUIT-001')
33+
terminations = nb.circuits.circuit_terminations.filter(circuit_id=circuit.id)
34+
35+
for term in terminations:
36+
print(f"\nTermination {term.term_side}:")
37+
paths = term.paths()
38+
if paths:
39+
for path in paths:
40+
if path['destination']:
41+
print(f" Connected to: {path['destination']}")
42+
else:
43+
print(" No destination (incomplete path)")
44+
else:
45+
print(" No cable paths")
46+
```
47+
48+
**Path Structure:**
49+
50+
The `paths()` method returns a list of dictionaries, where each dictionary represents a complete cable path:
51+
52+
- `origin`: The starting endpoint of the path (Record object or None if unconnected)
53+
- `destination`: The ending endpoint of the path (Record object or None if unconnected)
54+
- `path`: A list of path segments, where each segment is a list of Record objects representing the components in that segment (cables, terminations, interfaces, etc.)
55+
56+
## Virtual Circuits
57+
58+
### Overview
59+
60+
Virtual circuits also support cable path tracing through the `paths()` method.
61+
62+
**Example:**
63+
```python
64+
# Get a virtual circuit
65+
vcircuit = nb.circuits.virtual_circuits.get(cid='VPLS-001')
66+
print(f"Virtual Circuit: {vcircuit.cid}")
67+
print(f"Provider Network: {vcircuit.provider_network.name}")
68+
print(f"Type: {vcircuit.type.name}")
69+
70+
# List all terminations for a virtual circuit
71+
terminations = nb.circuits.virtual_circuit_terminations.filter(
72+
virtual_circuit_id=vcircuit.id
73+
)
74+
for term in terminations:
75+
print(f"Termination Role: {term.role}")
76+
```
77+
78+
### Virtual Circuit Termination Path Tracing
79+
80+
Virtual circuit terminations also support cable path tracing through the `paths()` method.
81+
82+
**Example:**
83+
```python
84+
# Get a virtual circuit termination
85+
vterm = nb.circuits.virtual_circuit_terminations.get(
86+
virtual_circuit_id=123, role='hub'
87+
)
88+
89+
# Get all cable paths through this termination
90+
paths = vterm.paths()
91+
92+
# Analyze the connectivity
93+
for path_info in paths:
94+
print(f"Origin: {path_info['origin']}")
95+
print(f"Destination: {path_info['destination']}")
96+
print("Path segments:")
97+
for segment in path_info['path']:
98+
for obj in segment:
99+
print(f" - {obj}")
100+
101+
# Example: Find all devices connected via a virtual circuit
102+
vcircuit = nb.circuits.virtual_circuits.get(cid='VPLS-001')
103+
terminations = nb.circuits.virtual_circuit_terminations.filter(
104+
virtual_circuit_id=vcircuit.id
105+
)
106+
107+
print(f"Virtual Circuit {vcircuit.cid} connectivity:")
108+
for term in terminations:
109+
paths = term.paths()
110+
if paths and paths[0]['destination']:
111+
print(f" {term.role}: {paths[0]['destination']}")
112+
```

docs/dcim.md

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@ Several DCIM models support cable path tracing through the `trace()` method.
105105
- PowerPorts
106106
- PowerOutlets
107107
- PowerFeeds
108-
- FrontPorts
109-
- RearPorts
110108

111109
**Example:**
112110
```python
@@ -135,3 +133,47 @@ console_trace = console.trace()
135133
power_port = nb.dcim.power_ports.get(name='PSU1', device='server1')
136134
power_trace = power_port.trace()
137135
```
136+
137+
## Cable Path Tracing (Pass-Through Ports)
138+
139+
Front ports and rear ports use the `paths()` method instead of `trace()`.
140+
141+
**Models with cable path tracing:**
142+
- FrontPorts
143+
- RearPorts
144+
145+
**Example:**
146+
```python
147+
# Get paths through a front port
148+
front_port = nb.dcim.front_ports.get(name='FrontPort1', device='patch-panel-1')
149+
paths = front_port.paths()
150+
151+
# Each path contains origin, destination, and path segments
152+
for path_info in paths:
153+
print(f"Origin: {path_info['origin']}")
154+
print(f"Destination: {path_info['destination']}")
155+
print("Path segments:")
156+
for segment in path_info['path']:
157+
for obj in segment:
158+
print(f" - {obj}")
159+
160+
# Get paths through a rear port
161+
rear_port = nb.dcim.rear_ports.get(name='RearPort1', device='patch-panel-1')
162+
rear_paths = rear_port.paths()
163+
164+
# Access the complete path from origin to destination
165+
if rear_paths:
166+
first_path = rear_paths[0]
167+
if first_path['origin']:
168+
print(f"Cable path starts at: {first_path['origin']}")
169+
if first_path['destination']:
170+
print(f"Cable path ends at: {first_path['destination']}")
171+
```
172+
173+
**Path Structure:**
174+
175+
The `paths()` method returns a list of dictionaries, where each dictionary represents a complete cable path:
176+
177+
- `origin`: The starting endpoint of the path (Record object or None if unconnected)
178+
- `destination`: The ending endpoint of the path (Record object or None if unconnected)
179+
- `path`: A list of path segments, where each segment is a list of Record objects representing the components in that segment (cables, terminations, etc.)

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@ nav:
3434
- Endpoint: endpoint.md
3535
- Response: response.md
3636
- Request: request.md
37-
- IPAM: ipam.md
37+
- Circuits: circuits.md
3838
- DCIM: dcim.md
39+
- IPAM: ipam.md
3940
- Virtualization: virtualization.md
4041
- Advanced Topics:
4142
- Advanced Usage: advanced.md

pynetbox/core/response.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,55 @@ def __eq__(self, other):
373373
return self.__key__() == other.__key__()
374374
return NotImplemented
375375

376+
def _extract_app_endpoint(self, url):
377+
"""Extract app/endpoint from a NetBox API URL.
378+
379+
Extracts the app and endpoint portion from a URL like:
380+
https://netbox/api/dcim/rear-ports/12761/
381+
Returns:
382+
String like "dcim/rear-ports"
383+
"""
384+
app_endpoint = "/".join(
385+
urlsplit(url).path[len(urlsplit(self.api.base_url).path) :].split("/")[1:3]
386+
)
387+
return app_endpoint
388+
389+
def _get_obj_class(self, url):
390+
"""Map API URL to corresponding Record class for cable tracing.
391+
392+
Used by TraceableRecord and PathableRecord to deserialize objects
393+
encountered in cable trace/path responses.
394+
"""
395+
# Import here to avoid circular dependency
396+
from pynetbox.models.circuits import CircuitTerminations
397+
from pynetbox.models.dcim import (
398+
Cables,
399+
ConsolePorts,
400+
ConsoleServerPorts,
401+
FrontPorts,
402+
Interfaces,
403+
PowerFeeds,
404+
PowerOutlets,
405+
PowerPorts,
406+
RearPorts,
407+
)
408+
409+
uri_to_obj_class_map = {
410+
"circuits/circuit-terminations": CircuitTerminations,
411+
"dcim/cables": Cables,
412+
"dcim/console-ports": ConsolePorts,
413+
"dcim/console-server-ports": ConsoleServerPorts,
414+
"dcim/front-ports": FrontPorts,
415+
"dcim/interfaces": Interfaces,
416+
"dcim/power-feeds": PowerFeeds,
417+
"dcim/power-outlets": PowerOutlets,
418+
"dcim/power-ports": PowerPorts,
419+
"dcim/rear-ports": RearPorts,
420+
}
421+
422+
app_endpoint = self._extract_app_endpoint(url)
423+
return uri_to_obj_class_map.get(app_endpoint, Record)
424+
376425
def _add_cache(self, item):
377426
key, value = item
378427
self._init_cache.append((key, get_return(value)))
@@ -647,6 +696,64 @@ def delete(self):
647696
return True if req.delete() else False
648697

649698

699+
class PathableRecord(Record):
700+
"""Record class for objects that support cable path tracing via /paths endpoint.
701+
702+
Front ports, rear ports, and circuit terminations use the /paths endpoint
703+
to show complete cable paths from origin to destination.
704+
"""
705+
706+
def _build_endpoint_object(self, endpoint_data):
707+
if not endpoint_data:
708+
return None
709+
710+
return_obj_class = self._get_obj_class(endpoint_data["url"])
711+
return return_obj_class(endpoint_data, self.endpoint.api, self.endpoint)
712+
713+
def paths(self):
714+
"""Return all cable paths traversing this pass-through port.
715+
716+
Returns a list of dictionaries, each containing:
717+
- origin: The starting endpoint of the path (or None if not connected)
718+
- destination: The ending endpoint of the path (or None if not connected)
719+
- path: List of path segments, where each segment is a list of Record objects
720+
(similar to the trace() endpoint structure)
721+
"""
722+
req = Request(
723+
key=str(self.id) + "/paths",
724+
base=self.endpoint.url,
725+
token=self.api.token,
726+
http_session=self.api.http_session,
727+
).get()
728+
729+
ret = []
730+
for path_data in req:
731+
path_segments = []
732+
for segment_data in path_data.get("path", []):
733+
segment_objects = []
734+
if isinstance(segment_data, list):
735+
for item_data in segment_data:
736+
segment_obj = self._build_endpoint_object(item_data)
737+
if segment_obj:
738+
segment_objects.append(segment_obj)
739+
else:
740+
segment_obj = self._build_endpoint_object(segment_data)
741+
if segment_obj:
742+
segment_objects.append(segment_obj)
743+
path_segments.append(segment_objects)
744+
745+
origin = self._build_endpoint_object(path_data.get("origin"))
746+
destination = self._build_endpoint_object(path_data.get("destination"))
747+
748+
ret.append({
749+
"origin": origin,
750+
"destination": destination,
751+
"path": path_segments,
752+
})
753+
754+
return ret
755+
756+
650757
class GenericListObject:
651758
def __init__(self, record):
652759
from pynetbox.models.mapper import TYPE_CONTENT_MAPPER

0 commit comments

Comments
 (0)