From af4a35e63c7e35abe0a98d4304ef14260cc7715f Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Fri, 31 Jul 2020 18:50:07 -0600 Subject: [PATCH 01/17] Added a trace method to the Interfaces class --- pynetbox/core/endpoint.py | 31 ++++++++++++++++++++++++++++++- pynetbox/models/dcim.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/pynetbox/core/endpoint.py b/pynetbox/core/endpoint.py index f2f488b1..fbc36688 100644 --- a/pynetbox/core/endpoint.py +++ b/pynetbox/core/endpoint.py @@ -21,7 +21,36 @@ def response_loader(req, return_obj, endpoint): if isinstance(req, list): - return [return_obj(i, endpoint.api, endpoint) for i in req] + # interface/cable traces are lists of lists + if req and isinstance(req[0], list) and isinstance(return_obj, dict): + # we will have to build the response piecewise since the items contained + # within it are of varying types + ret = [] + for i in req: + this_sub_ret = [] + for ii in i: + # the individual items in a trace are comprised of [, , ]. + # The last trace can consist of [, + # None, None] if the last hop is not actually connected to anything + if ii: + this_sub_ret.append( + next( + ( + return_obj_class(ii, endpoint.api, endpoint) + for (return_obj_uri, return_obj_class) in return_obj.items() + if return_obj_uri in ii["url"] + ) + ) + ) + else: + # the last trace can consist of [cable_a, None, None] if there is no + this_sub_ret.append(None) + + ret.append(this_sub_ret) + return ret + else: + return [return_obj(i, endpoint.api, endpoint) for i in req] return return_obj(req, endpoint.api, endpoint) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index cae761cb..1e6bbec2 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -84,6 +84,33 @@ class Interfaces(Record): interface_connection = InterfaceConnection connected_endpoint = ConnectedEndpoint + @property + def trace(self): + """ Represents the ``trace`` detail endpoint. + + Returns a DetailEndpoint object that is the interface for + viewing response from the trace endpoint. + + :returns: :py:class:`.DetailEndpoint` + + :Examples: + + >>> interface = nb.dcim.interfaces.get(123) + >>> interface.trace.list() + {"get_facts": {"interface_list": ["ge-0/0/0"]}} + + """ + return RODetailEndpoint( + self, + "trace", + custom_return={ + "dcim/cables": Cables, + "dcim/interfaces": Interfaces, + "dcim/front-ports": FrontPorts, + "dcim/rear-ports": RearPorts, + }, + ) + class RackReservations(Record): def __str__(self): @@ -99,6 +126,14 @@ class RUs(Record): device = Devices +class FrontPorts(Record): + device = Devices + + +class RearPorts(Record): + device = Devices + + class Racks(Record): @property def units(self): From 0ff839f365d905344b2fe55ea09c8916302b0119 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Thu, 20 Aug 2020 12:07:03 -0600 Subject: [PATCH 02/17] Redesigned cable trace functionality --- pynetbox/core/endpoint.py | 31 +------------ pynetbox/models/dcim.py | 93 +++++++++++++++++++++++++++++---------- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/pynetbox/core/endpoint.py b/pynetbox/core/endpoint.py index fbc36688..f2f488b1 100644 --- a/pynetbox/core/endpoint.py +++ b/pynetbox/core/endpoint.py @@ -21,36 +21,7 @@ def response_loader(req, return_obj, endpoint): if isinstance(req, list): - # interface/cable traces are lists of lists - if req and isinstance(req[0], list) and isinstance(return_obj, dict): - # we will have to build the response piecewise since the items contained - # within it are of varying types - ret = [] - for i in req: - this_sub_ret = [] - for ii in i: - # the individual items in a trace are comprised of [, , ]. - # The last trace can consist of [, - # None, None] if the last hop is not actually connected to anything - if ii: - this_sub_ret.append( - next( - ( - return_obj_class(ii, endpoint.api, endpoint) - for (return_obj_uri, return_obj_class) in return_obj.items() - if return_obj_uri in ii["url"] - ) - ) - ) - else: - # the last trace can consist of [cable_a, None, None] if there is no - this_sub_ret.append(None) - - ret.append(this_sub_ret) - return ret - else: - return [return_obj(i, endpoint.api, endpoint) for i in req] + return [return_obj(i, endpoint.api, endpoint) for i in req] return return_obj(req, endpoint.api, endpoint) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 1e6bbec2..bb1c6564 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -13,12 +13,56 @@ 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): + @property + def trace(self): + req = Request( + key=str(self.id) + "/trace" if not self.url else None, + base=self.endpoint.url, + token=self.api.token, + session_key=self.api.session_key, + http_session=self.api.http_session, + ) + 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 + + url_path = urlsplit(hop_item_data["url"]).path + if url_path.startswith("/api/dcim/cables"): + return_obj_class = Cables + elif url_path.startswith("/api/dcim/front-ports"): + return_obj_class = FrontPorts + elif url_path.startswith("/api/dcim/interfaces"): + return_obj_class = Interfaces + elif url_path.startswith("/api/dcim/rear-ports"): + return_obj_class = RearPorts + else: + raise NotImplementedError( + "unable to unpack item data from endpoint '{}'".format(url_path) + ) + 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,36 +124,25 @@ class ConnectedEndpoint(Record): device = Devices -class Interfaces(Record): +class Interfaces(TraceableRecord): interface_connection = InterfaceConnection connected_endpoint = ConnectedEndpoint - @property - def trace(self): - """ Represents the ``trace`` detail endpoint. - Returns a DetailEndpoint object that is the interface for - viewing response from the trace endpoint. +class PowerOutlets(TraceableRecord): + device = Devices - :returns: :py:class:`.DetailEndpoint` - :Examples: +class PowerPorts(TraceableRecord): + device = Devices - >>> interface = nb.dcim.interfaces.get(123) - >>> interface.trace.list() - {"get_facts": {"interface_list": ["ge-0/0/0"]}} - """ - return RODetailEndpoint( - self, - "trace", - custom_return={ - "dcim/cables": Cables, - "dcim/interfaces": Interfaces, - "dcim/front-ports": FrontPorts, - "dcim/rear-ports": RearPorts, - }, - ) +class ConsolePorts(TraceableRecord): + device = Devices + + +class ConsoleServerPorts(TraceableRecord): + device = Devices class RackReservations(Record): @@ -189,7 +222,21 @@ def __str__(self): class Cables(Record): def __str__(self): - return "{} <> {}".format(self.termination_a, self.termination_b) + # populate the terminations to get the full names if they are not already + try: + termination_a_name = self.termination_a.name + except AttributeError: + self.termination_a.full_details(self) + termination_a_name = self.termination_a.name + + try: + termination_b_name = self.termination_b.name + except AttributeError: + self.termination_b.full_details(self) + termination_b_name = self.termination_b.name + + + return "{} <> {}".format(termination_a_name, termination_b_name) termination_a = Termination termination_b = Termination From c8cc839e3e4adf892707bbc56e25d9d20da2a803 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Thu, 20 Aug 2020 12:26:34 -0600 Subject: [PATCH 03/17] Re-blacked the ocde and fixed an instanciation issue I'm not sure if I fixed it in a proper way though --- pynetbox/models/dcim.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index bb1c6564..74990ac8 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -226,16 +226,21 @@ def __str__(self): try: termination_a_name = self.termination_a.name except AttributeError: - self.termination_a.full_details(self) + try: + self.termination_a.full_details() + except TypeError: + self.termination_a.full_details(self) termination_a_name = self.termination_a.name try: termination_b_name = self.termination_b.name except AttributeError: - self.termination_b.full_details(self) + try: + self.termination_b.full_details() + except TypeError: + self.termination_b.full_details(self) termination_b_name = self.termination_b.name - return "{} <> {}".format(termination_a_name, termination_b_name) termination_a = Termination From 74b6e536254f5186954b52d49175fe848611d244 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Thu, 20 Aug 2020 17:56:55 -0600 Subject: [PATCH 04/17] Fixed a test failure --- pynetbox/models/dcim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 74990ac8..f64822e8 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -230,7 +230,7 @@ def __str__(self): self.termination_a.full_details() except TypeError: self.termination_a.full_details(self) - termination_a_name = self.termination_a.name + termination_a_name = getattr(self.termination_a, "name", self.termination_a) try: termination_b_name = self.termination_b.name @@ -239,7 +239,7 @@ def __str__(self): self.termination_b.full_details() except TypeError: self.termination_b.full_details(self) - termination_b_name = self.termination_b.name + termination_b_name = getattr(self.termination_b, "name", self.termination_b) return "{} <> {}".format(termination_a_name, termination_b_name) From d020bcc8fa2db720cb6b1fe8dc564af0e71fc916 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Thu, 20 Aug 2020 18:12:07 -0600 Subject: [PATCH 05/17] Updated pytest tests --- tests/test_dcim.py | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 3403f93c..8247ec0a 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -5,9 +5,9 @@ from .util import Response if six.PY3: - from unittest.mock import patch + from unittest.mock import patch, call else: - from mock import patch + from mock import patch, call api = pynetbox.api("http://localhost:8000", token="abc123",) @@ -526,11 +526,32 @@ def test_get_circuit(self): self.assertTrue(isinstance(ret, self.ret)) self.assertTrue(isinstance(str(ret), str)) self.assertTrue(isinstance(dict(ret), dict)) - mock.assert_called_with( - "http://localhost:8000/api/{}/{}/1/".format( - self.app, self.name.replace("_", "-") - ), - headers=HEADERS, - params={}, - json=None, + mock.assert_has_calls( + [ + call( + "http://localhost:8000/api/{}/{}/1/".format( + self.app, self.name.replace("_", "-") + ), + headers=HEADERS, + params={}, + json=None, + ), + call( + "http://localhost:8000/api/{}/{}/1/".format( + "circuits", "circuit-terminations" + ), + headers=HEADERS, + params={}, + json=None, + ), + call( + "http://localhost:8000/api/{}/{}/1/".format( + "circuits", "circuit-terminations" + ), + headers=HEADERS, + params={}, + json=None, + ), + ] ) + From c670197979f8cbd51b1ef3c9c2c65f01d15c881a Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Thu, 20 Aug 2020 18:15:13 -0600 Subject: [PATCH 06/17] Re-blacked after updating my local black module --- tests/test_dcim.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 8247ec0a..91475f03 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -554,4 +554,3 @@ def test_get_circuit(self): ), ] ) - From 55cacaf8894d0758f1e49c9daf4064f8fd4978e0 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Wed, 2 Dec 2020 18:54:43 -0700 Subject: [PATCH 07/17] Updated with changes from last review and added dedicated test --- pynetbox/models/dcim.py | 60 +++++------- tests/fixtures/dcim/interface_trace.json | 118 +++++++++++++++++++++++ tests/test_dcim.py | 84 ++++++++++------ 3 files changed, 201 insertions(+), 61 deletions(-) create mode 100644 tests/fixtures/dcim/interface_trace.json diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index f64822e8..84027a8d 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -23,7 +23,6 @@ class TraceableRecord(Record): - @property def trace(self): req = Request( key=str(self.id) + "/trace" if not self.url else None, @@ -33,6 +32,8 @@ def trace(self): http_session=self.api.http_session, ) ret = [] + # from IPython import embed + # embed() 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): @@ -41,23 +42,33 @@ def trace(self): this_hop_ret.append(hop_item_data) continue - url_path = urlsplit(hop_item_data["url"]).path - if url_path.startswith("/api/dcim/cables"): - return_obj_class = Cables - elif url_path.startswith("/api/dcim/front-ports"): - return_obj_class = FrontPorts - elif url_path.startswith("/api/dcim/interfaces"): - return_obj_class = Interfaces - elif url_path.startswith("/api/dcim/rear-ports"): - return_obj_class = RearPorts - else: - raise NotImplementedError( - "unable to unpack item data from endpoint '{}'".format(url_path) - ) + uri_to_obj_class_map = { + "/api/dcim/cables": Cables, + "/api/dcim/front-ports": FrontPorts, + "/api/dcim/interfaces": Interfaces, + "/api/dcim/rear-ports": RearPorts, + } + + uri = urlsplit(hop_item_data["url"]).path + return_obj_class = uri_to_obj_class_map.get( + "/".join(uri.split("/")[:4]), # trim the uri down to it's base + Record, + ) + this_hop_ret.append( return_obj_class(hop_item_data, self.endpoint.api, self.endpoint) ) + # Try to access an attribute of the Cable object so that the Termination + # objects inside of it get populated properly via the .full_details method + # call. Without this the Cable object will be printed as: + # " <> " + # until it gets accessed. + try: + this_hop_ret[1].name + except (AttributeError, IndexError): + pass + ret.append(this_hop_ret) return ret @@ -222,26 +233,7 @@ def __str__(self): class Cables(Record): def __str__(self): - # populate the terminations to get the full names if they are not already - try: - termination_a_name = self.termination_a.name - except AttributeError: - try: - self.termination_a.full_details() - except TypeError: - self.termination_a.full_details(self) - termination_a_name = getattr(self.termination_a, "name", self.termination_a) - - try: - termination_b_name = self.termination_b.name - except AttributeError: - try: - self.termination_b.full_details() - except TypeError: - self.termination_b.full_details(self) - termination_b_name = getattr(self.termination_b, "name", self.termination_b) - - return "{} <> {}".format(termination_a_name, termination_b_name) + return "{} <> {}".format(self.termination_a, self.termination_b) 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..89415733 --- /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" + } + } + ] +] \ No newline at end of file diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 91475f03..78ffb0e2 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -325,6 +325,28 @@ 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"), + Response(fixture="dcim/cable.json"), + Response(fixture="dcim/cable.json"), + Response(fixture="dcim/cable.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) + self.assertTrue("Termination" not in str(hop[1])) + self.assertTrue(hasattr(hop[1], "termination_a")) + self.assertTrue(hasattr(hop[1].termination_a, "name")) + + + class RackTestCase(Generic.Tests): name = "racks" @@ -526,31 +548,39 @@ def test_get_circuit(self): self.assertTrue(isinstance(ret, self.ret)) self.assertTrue(isinstance(str(ret), str)) self.assertTrue(isinstance(dict(ret), dict)) - mock.assert_has_calls( - [ - call( - "http://localhost:8000/api/{}/{}/1/".format( - self.app, self.name.replace("_", "-") - ), - headers=HEADERS, - params={}, - json=None, - ), - call( - "http://localhost:8000/api/{}/{}/1/".format( - "circuits", "circuit-terminations" - ), - headers=HEADERS, - params={}, - json=None, - ), - call( - "http://localhost:8000/api/{}/{}/1/".format( - "circuits", "circuit-terminations" - ), - headers=HEADERS, - params={}, - json=None, - ), - ] + mock.assert_called_with( + "http://localhost:8000/api/{}/{}/1/".format( + self.app, self.name.replace("_", "-") + ), + headers=HEADERS, + params={}, + json=None, ) + # mock.assert_has_calls( + # [ + # call( + # "http://localhost:8000/api/{}/{}/1/".format( + # self.app, self.name.replace("_", "-") + # ), + # headers=HEADERS, + # params={}, + # json=None, + # ), + # call( + # "http://localhost:8000/api/{}/{}/1/".format( + # "circuits", "circuit-terminations" + # ), + # headers=HEADERS, + # params={}, + # json=None, + # ), + # call( + # "http://localhost:8000/api/{}/{}/1/".format( + # "circuits", "circuit-terminations" + # ), + # headers=HEADERS, + # params={}, + # json=None, + # ), + # ] + # ) From bb5ef86998832e3e12e108ee99a502aea794888f Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Wed, 2 Dec 2020 18:55:55 -0700 Subject: [PATCH 08/17] Reverted some now-unused additions --- tests/test_dcim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 78ffb0e2..4adb9f39 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -5,9 +5,9 @@ from .util import Response if six.PY3: - from unittest.mock import patch, call + from unittest.mock import patch else: - from mock import patch, call + from mock import patch api = pynetbox.api("http://localhost:8000", token="abc123",) From 754633c442b26570872aa8233b00efc5c41a013d Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Wed, 2 Dec 2020 19:00:07 -0700 Subject: [PATCH 09/17] Removed more dead code --- tests/test_dcim.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 4adb9f39..5bcf9512 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -556,31 +556,3 @@ def test_get_circuit(self): params={}, json=None, ) - # mock.assert_has_calls( - # [ - # call( - # "http://localhost:8000/api/{}/{}/1/".format( - # self.app, self.name.replace("_", "-") - # ), - # headers=HEADERS, - # params={}, - # json=None, - # ), - # call( - # "http://localhost:8000/api/{}/{}/1/".format( - # "circuits", "circuit-terminations" - # ), - # headers=HEADERS, - # params={}, - # json=None, - # ), - # call( - # "http://localhost:8000/api/{}/{}/1/".format( - # "circuits", "circuit-terminations" - # ), - # headers=HEADERS, - # params={}, - # json=None, - # ), - # ] - # ) From cc5210de807115f6b99c957e66d83889f0a664f7 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Wed, 2 Dec 2020 19:01:03 -0700 Subject: [PATCH 10/17] Added missing newline --- tests/fixtures/dcim/interface_trace.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/dcim/interface_trace.json b/tests/fixtures/dcim/interface_trace.json index 89415733..c7fd0ae3 100644 --- a/tests/fixtures/dcim/interface_trace.json +++ b/tests/fixtures/dcim/interface_trace.json @@ -115,4 +115,4 @@ } } ] -] \ No newline at end of file +] From 0a6e0a8ced3de028f585f3e6becdbb18f63cebd1 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Tue, 8 Dec 2020 19:16:51 -0700 Subject: [PATCH 11/17] Fixed a logic issue and removed a debug statement --- pynetbox/models/dcim.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 84027a8d..46fba721 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -25,15 +25,13 @@ class TraceableRecord(Record): def trace(self): req = Request( - key=str(self.id) + "/trace" if not self.url else None, + 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, ) ret = [] - # from IPython import embed - # embed() 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): From 8d69e4a04c4a14b21474a53f95db06ef4a27b102 Mon Sep 17 00:00:00 2001 From: "Ryan Addessi (raddessi)" Date: Mon, 28 Dec 2020 12:03:31 -0700 Subject: [PATCH 12/17] Fixed logic bug regarding the api url path --- pynetbox/models/dcim.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 46fba721..b34eed40 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -47,9 +47,8 @@ def trace(self): "/api/dcim/rear-ports": RearPorts, } - uri = urlsplit(hop_item_data["url"]).path return_obj_class = uri_to_obj_class_map.get( - "/".join(uri.split("/")[:4]), # trim the uri down to it's base + urlsplit(req.base).path, Record, ) From d8acca0e842be7623f646982f489267f98a25d9b Mon Sep 17 00:00:00 2001 From: zmoody Date: Tue, 29 Dec 2020 10:13:10 -0600 Subject: [PATCH 13/17] Update str method on Cables When we're processing an object from the traces detail route it doesn't include a/b terminations. This commit changes the str representation of cable objects when that's the case. --- pynetbox/models/dcim.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index b34eed40..a39c8800 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -48,8 +48,7 @@ def trace(self): } return_obj_class = uri_to_obj_class_map.get( - urlsplit(req.base).path, - Record, + urlsplit(req.base).path, Record, ) this_hop_ret.append( @@ -230,7 +229,9 @@ def __str__(self): class Cables(Record): def __str__(self): - return "{} <> {}".format(self.termination_a, self.termination_b) + if all(["name" in dict(i) 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 From c8b91e36cf60e28f14f4d46c5d7fae7ed9e49c3f Mon Sep 17 00:00:00 2001 From: zmoody Date: Tue, 29 Dec 2020 10:20:30 -0600 Subject: [PATCH 14/17] Remove population of cable str repr --- pynetbox/models/dcim.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index a39c8800..a6b21405 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -55,16 +55,6 @@ def trace(self): return_obj_class(hop_item_data, self.endpoint.api, self.endpoint) ) - # Try to access an attribute of the Cable object so that the Termination - # objects inside of it get populated properly via the .full_details method - # call. Without this the Cable object will be printed as: - # " <> " - # until it gets accessed. - try: - this_hop_ret[1].name - except (AttributeError, IndexError): - pass - ret.append(this_hop_ret) return ret From 997854f68d43e0247d705df25759c7fe0b3cfae9 Mon Sep 17 00:00:00 2001 From: zmoody Date: Tue, 29 Dec 2020 11:03:39 -0600 Subject: [PATCH 15/17] Fix str repr on Cables method If termination's are unintialized we won't be able to call them in dict(). So instead check to see if they're Termination types. --- pynetbox/models/dcim.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index a6b21405..4eacd334 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -219,7 +219,12 @@ def __str__(self): class Cables(Record): def __str__(self): - if all(["name" in dict(i) for i in (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) From 4a0d8caf9475e54c29a4850cb73fc603caba6903 Mon Sep 17 00:00:00 2001 From: zmoody Date: Tue, 29 Dec 2020 11:06:15 -0600 Subject: [PATCH 16/17] Fix object url-to-class parsing --- pynetbox/models/dcim.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pynetbox/models/dcim.py b/pynetbox/models/dcim.py index 4eacd334..792a4571 100644 --- a/pynetbox/models/dcim.py +++ b/pynetbox/models/dcim.py @@ -31,6 +31,12 @@ def trace(self): 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 = [] @@ -40,17 +46,15 @@ def trace(self): this_hop_ret.append(hop_item_data) continue - uri_to_obj_class_map = { - "/api/dcim/cables": Cables, - "/api/dcim/front-ports": FrontPorts, - "/api/dcim/interfaces": Interfaces, - "/api/dcim/rear-ports": RearPorts, - } - - return_obj_class = uri_to_obj_class_map.get( - urlsplit(req.base).path, Record, + # 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) ) From e6d2b78629c5508eab1e1e638e0228a437e6233a Mon Sep 17 00:00:00 2001 From: zmoody Date: Tue, 29 Dec 2020 11:12:38 -0600 Subject: [PATCH 17/17] Don't look for termination data for cables in trace test --- tests/test_dcim.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/test_dcim.py b/tests/test_dcim.py index 5bcf9512..00b9de31 100644 --- a/tests/test_dcim.py +++ b/tests/test_dcim.py @@ -330,9 +330,6 @@ def test_get_all(self, mock): side_effect=[ Response(fixture="dcim/interface.json"), Response(fixture="dcim/interface_trace.json"), - Response(fixture="dcim/cable.json"), - Response(fixture="dcim/cable.json"), - Response(fixture="dcim/cable.json"), ], ) def test_trace(self, mock): @@ -341,11 +338,6 @@ def test_trace(self, mock): self.assertTrue(len(trace) == 3) for hop in trace: self.assertTrue(len(hop) == 3) - self.assertTrue("Termination" not in str(hop[1])) - self.assertTrue(hasattr(hop[1], "termination_a")) - self.assertTrue(hasattr(hop[1].termination_a, "name")) - - class RackTestCase(Generic.Tests):