diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index cae761cb..792a4571 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -13,12 +13,57 @@ See the License for the specific language governing permissions and limitations under the License. """ +from six.moves.urllib.parse import urlsplit + +from pynetbox.core.query import Request from pynetbox.core.response import Record, JsonField from pynetbox.core.endpoint import RODetailEndpoint from pynetbox.models.ipam import IpAddresses from pynetbox.models.circuits import Circuits +class TraceableRecord(Record): + def trace(self): + req = Request( + key=str(self.id) + "/trace", + base=self.endpoint.url, + token=self.api.token, + session_key=self.api.session_key, + http_session=self.api.http_session, + ) + uri_to_obj_class_map = { + "dcim/cables": Cables, + "dcim/front-ports": FrontPorts, + "dcim/interfaces": Interfaces, + "dcim/rear-ports": RearPorts, + } + ret = [] + for (termination_a_data, cable_data, termination_b_data) in req.get(): + this_hop_ret = [] + for hop_item_data in (termination_a_data, cable_data, termination_b_data): + # if not fully terminated then some items will be None + if not hop_item_data: + this_hop_ret.append(hop_item_data) + continue + + # TODO: Move this to a more general function. + app_endpoint = "/".join( + urlsplit(hop_item_data["url"][len(self.api.base_url) :]).path.split( + "/" + )[1:3] + ) + + return_obj_class = uri_to_obj_class_map.get(app_endpoint, Record,) + + this_hop_ret.append( + return_obj_class(hop_item_data, self.endpoint.api, self.endpoint) + ) + + ret.append(this_hop_ret) + + return ret + + class DeviceTypes(Record): def __str__(self): return self.model @@ -80,11 +125,27 @@ class ConnectedEndpoint(Record): device = Devices -class Interfaces(Record): +class Interfaces(TraceableRecord): interface_connection = InterfaceConnection connected_endpoint = ConnectedEndpoint +class PowerOutlets(TraceableRecord): + device = Devices + + +class PowerPorts(TraceableRecord): + device = Devices + + +class ConsolePorts(TraceableRecord): + device = Devices + + +class ConsoleServerPorts(TraceableRecord): + device = Devices + + class RackReservations(Record): def __str__(self): return self.description @@ -99,6 +160,14 @@ class RUs(Record): device = Devices +class FrontPorts(Record): + device = Devices + + +class RearPorts(Record): + device = Devices + + class Racks(Record): @property def units(self): @@ -154,7 +223,14 @@ def __str__(self): class Cables(Record): def __str__(self): - return "{} <> {}".format(self.termination_a, self.termination_b) + if all( + [ + isinstance(i, Termination) + for i in (self.termination_a, self.termination_b) + ] + ): + return "{} <> {}".format(self.termination_a, self.termination_b) + return "Cable #{}".format(self.id) termination_a = Termination termination_b = Termination diff --git a/tests/fixtures/dcim/interface_trace.json b/tests/fixtures/dcim/interface_trace.json new file mode 100644 index 00000000..c7fd0ae3 --- /dev/null +++ b/tests/fixtures/dcim/interface_trace.json @@ -0,0 +1,118 @@ +[ + [ + { + "id": 39126, + "url": "http://localhost:8000/api/dcim/interfaces/39126/", + "device": { + "id": 4747, + "url": "http://localhost:8000/api/dcim/devices/4747/", + "name": "test1-core1", + "display_name": "test1-core1" + }, + "name": "em1", + "cable": 9911, + "connection_status": { + "value": false, + "label": "Not Connected" + } + }, + { + "id": 9911, + "url": "http://localhost:8000/api/dcim/cables/9911/", + "type": "", + "status": "planned", + "label": "", + "color": "", + "length": null, + "length_unit": "" + }, + { + "id": 5583, + "url": "http://localhost:8000/api/dcim/front-ports/5583/", + "device": { + "id": 4430, + "url": "http://localhost:8000/api/dcim/devices/4430/", + "name": "test1-patchpanel1", + "display_name": "test1-patchpanel1" + }, + "name": "pair-11 (ports 21-22)", + "cable": 9911 + } + ], + [ + { + "id": 3736, + "url": "http://localhost:8000/api/dcim/rear-ports/3736/", + "device": { + "id": 4430, + "url": "http://localhost:8000/api/dcim/devices/4430/", + "name": "test1-patchpanel1", + "display_name": "test1-patchpanel1" + }, + "name": "port-2", + "cable": 9229 + }, + { + "id": 9229, + "url": "http://localhost:8000/api/dcim/cables/9229/", + "type": "mmf-om4", + "status": "planned", + "label": "", + "color": "", + "length": null, + "length_unit": "" + }, + { + "id": 3768, + "url": "http://localhost:8000/api/dcim/rear-ports/3768/", + "device": { + "id": 4436, + "url": "http://localhost:8000/api/dcim/devices/4436/", + "name": "test1-patchpanel2", + "display_name": "test1-patchpanel2" + }, + "name": "port-2", + "cable": 9229 + } + ], + [ + { + "id": 5655, + "url": "http://localhost:8000/api/dcim/front-ports/5655/", + "device": { + "id": 4436, + "url": "http://localhost:8000/api/dcim/devices/4436/", + "name": "test1-patchpanel2", + "display_name": "test1-patchpanel2" + }, + "name": "pair-11 (ports 21-22)", + "cable": 9240 + }, + { + "id": 9240, + "url": "http://localhost:8000/api/dcim/cables/9240/", + "type": "mmf-om4", + "status": "planned", + "label": "", + "color": "", + "length": null, + "length_unit": "" + }, + { + "id": 35473, + "url": "http://localhost:8000/api/dcim/interfaces/35473/", + "device": { + "id": 3930, + "url": "http://localhost:8000/api/dcim/devices/3930/", + "name": "test1-core2", + "display_name": "test1-core2" + }, + "name": "Ethernet11", + "cable": 9240, + "connection_status": { + "value": false, + "label": "Not Connected" + } + } + ] +] diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 3403f93c..00b9de31 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -325,6 +325,20 @@ def test_get_all(self, mock): headers=HEADERS, ) + @patch( + "pynetbox.core.query.requests.sessions.Session.get", + side_effect=[ + Response(fixture="dcim/interface.json"), + Response(fixture="dcim/interface_trace.json"), + ], + ) + def test_trace(self, mock): + ret = nb.interfaces.get(1) + trace = ret.trace() + self.assertTrue(len(trace) == 3) + for hop in trace: + self.assertTrue(len(hop) == 3) + class RackTestCase(Generic.Tests): name = "racks"