New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added a trace property to the traceable interface classes #280
Added a trace property to the traceable interface classes #280
Conversation
I would also like to add this for the other items that have |
pynetbox/core/endpoint.py
Outdated
) | ||
) | ||
else: | ||
# the last trace can consist of [cable_a, None, None] if there is no |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy pasta, I will remove
Interesting, thanks for the PR. I haven't really dug into the trace API yet. I feel like it's unique enough that I'd probably like to deal with it in the |
Ah ok that makes good sense. I'll play around with it and send something over. Thanks! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added some comments/questions
pynetbox/models/dcim.py
Outdated
elif url_path.startswith("/api/dcim/rear-ports"): | ||
return_obj_class = RearPorts | ||
else: | ||
raise NotImplementedError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this how you would want to handle this situation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably make a dict here that mapped the endpoint name it might encounter with the custom object instead of an if/else tree (really wish switches were a thing in python). e.g.
{
"cables": Cables,
"front-ports": FrontPorts,
...
}
You'll probably want to add some other endpoint/objects you might come across in the traces as well like ConsolePort/ConsoleServerPort and PowerPort/Outlets. I'd probably default to just a simple Record
object at the end of it instead of raising.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect. I'll update to a map and return a Record
instead of raising. It's been a while.. I think I verified all possible objects returned are listed here but I'll check once more, good call.
re: the failing tests.. yeah I have never seen python complain that
I'll dig in to why it's wanting that locally and fix that issue. Comments would still be appreciated, thanks! |
I'm not sure if I fixed it in a proper way though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will add tests for the trace property if this looks good so far
pynetbox/models/dcim.py
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like this logic but I'm not sure of a better method. Ideas welcomed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the goal here? Both of those should be Termination
objects which return .name
(assuming they're not a circuit) anyways. I suspect this might be the source of the excessive calls to NetBox in your tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a change to force each of the returned items along the trace to get fully populated so that when you printed their name in the trace it wasn't just displayed as a Record
item with no data. Once each item was accessed they would then print their name properly, but having a trace full of unlabeled hops was not very useful. It's not very clear, and I don't like it at all. I think it may be better to do something like this in the trace function itself (I think maybe you mentioned that before?) so that only on a trace will it try to prefetch the data of each item in the hops so they print out nicely.
EDIT: When I said It's not very clear, and I don't like it at all.
I was referring to my first attempt to patch this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's weird. Do you have an example you can show of the old behavior? Those should be Termination objects, but either way even Record's default __str__
behavior is to return .name, iirc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, with this section of code reverted this is what I'm getting now:
In [1]: iface.trace()
Out[1]:
[[em1,
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
pair-11 (ports 21-22)],
[port-2,
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
port-2],
[pair-11 (ports 21-22),
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
Ethernet11]]
So it looks like maybe I was mistaken or confused about only the middle (not the first or last) having the issue, it may be all of the items returned from the trace until they are accessed in some way. You can see here how the info is filled out once the item is accessed directly:
In [2]: trace = iface.trace()
In [3]: trace
Out[3]:
[[em1,
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
pair-11 (ports 21-22)],
[port-2,
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
port-2],
[pair-11 (ports 21-22),
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
Ethernet11]]
In [4]: trace[0][1]
Out[4]: em1 <> pair-11 (ports 21-22)
In [5]: trace
Out[5]:
[[em1, em1 <> pair-11 (ports 21-22), pair-11 (ports 21-22)],
[port-2,
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
port-2],
[pair-11 (ports 21-22),
<class 'pynetbox.models.dcim.Termination'> <> <class 'pynetbox.models.dcim.Termination'>,
Ethernet11]]
You are correct, they are Termination objects. I'm sorry it's been a while since I worked on this so I've forgotten a little of the details. It seems like none of the cable connection object information at all is set until the item is accessed directly for some reason though. I have not dug in to that much yet.
EDIT: Actually to be more clear, it seems like the Termination
objects referenced by the Cable
connection object are not being loaded until the Cable
object is itself referenced directly. The Cable
object is always printing in the correct format, just the Termination
objects inside of it have no data in them yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By moving this to inside the trace function it is muuuuch clearer now.
tests/test_dcim.py
Outdated
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, | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't love the idea that each call for a cable will now result in 3 calls.. I didn't realize when fixing this test that that is essentially what changed in this test. It doesn't feel right that this needs to happen for each cable returned.. I want to dig in to this because I think this should only be needed for the cables between either the first or last cable returned in a trace since those are the only ones I have ever seen that exhibited the problem of their names not being initialized until you actually interact with the cable object (then it calling it's full_details
method) so it seems wrong that this would be called 3 times for this simple query. I'll dig later when I have time.
Alternatively.. we could move this logic to the trace property and have it done there only for cables returned from traces but that feels less consistent somehow. I'm on the fence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, have a look at the previous comment, the __str__()
method might have something to do with it. Either way, we'd definitely want some tests that are specifically testing .trace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I'll also add testing with the next round of changes.
@zachmoody do you have any thoughts? |
Yeah, been meaning to review it, sorry to leave you hanging. Will do my best to get to it this week. |
No worries, this is working as needed for my current work just fine so as long as it's still on your radar I'm happy. Thanks :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great progress, made some comments below. Was thinking about maybe changing this from a property to a method, it's a bit of a nit but you only get one shot at getting that part right without cutting a new release 😄.
pynetbox/models/dcim.py
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the goal here? Both of those should be Termination
objects which return .name
(assuming they're not a circuit) anyways. I suspect this might be the source of the excessive calls to NetBox in your tests.
pynetbox/models/dcim.py
Outdated
elif url_path.startswith("/api/dcim/rear-ports"): | ||
return_obj_class = RearPorts | ||
else: | ||
raise NotImplementedError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd probably make a dict here that mapped the endpoint name it might encounter with the custom object instead of an if/else tree (really wish switches were a thing in python). e.g.
{
"cables": Cables,
"front-ports": FrontPorts,
...
}
You'll probably want to add some other endpoint/objects you might come across in the traces as well like ConsolePort/ConsoleServerPort and PowerPort/Outlets. I'd probably default to just a simple Record
object at the end of it instead of raising.
tests/test_dcim.py
Outdated
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, | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, have a look at the previous comment, the __str__()
method might have something to do with it. Either way, we'd definitely want some tests that are specifically testing .trace
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for all the great feedback. I'm sorry I dropped off the map, life got in the way.
Was thinking about maybe changing this from a property to a method, it's a bit of a nit but you only get one shot at getting that part right without cutting a new release
I'll definitely change this from a property to a method in the next update as well unless I hear back from you, I think that's probably better anyway but I was trying to match styles.
pynetbox/models/dcim.py
Outdated
elif url_path.startswith("/api/dcim/rear-ports"): | ||
return_obj_class = RearPorts | ||
else: | ||
raise NotImplementedError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect. I'll update to a map and return a Record
instead of raising. It's been a while.. I think I verified all possible objects returned are listed here but I'll check once more, good call.
pynetbox/models/dcim.py
Outdated
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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a change to force each of the returned items along the trace to get fully populated so that when you printed their name in the trace it wasn't just displayed as a Record
item with no data. Once each item was accessed they would then print their name properly, but having a trace full of unlabeled hops was not very useful. It's not very clear, and I don't like it at all. I think it may be better to do something like this in the trace function itself (I think maybe you mentioned that before?) so that only on a trace will it try to prefetch the data of each item in the hops so they print out nicely.
EDIT: When I said It's not very clear, and I don't like it at all.
I was referring to my first attempt to patch this :)
tests/test_dcim.py
Outdated
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, | ||
), | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah. I'll also add testing with the next round of changes.
Ha, no worries, been in the same boat myself. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK I think this is much better.
Right on! Is there anything left you would like done here then? If not then I feel like it's ready to go in. |
Hold on this actually, I broke functionality in the last update. Will fix asap. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another change
OK that should be resolved now. I'm not sure why I had the else statement in there to begin with. |
Nice work! I'd like to put it through its paces as well before merging. This month's pretty crazy for me, but I should be able to get to it before month's end. |
Yeah ok no problem. Do you have any plans for an integration testing suite? |
No serious plans, but that'd be an improvement over the fixtures we're using now. 🤔 |
Right on.. how would you feel about me starting a WIP PR to try to set up a basic system using pytest-docker then? If its beyond what you want to deal with right now that's fine it can wait until some point in the future, no big deal. |
A side note: does the proposed code work if the NetBox server address is like https://appserver.example.com/netbox? The URL paths will then be Checking because we fixed that kind of issue somewhere else in the code recently. |
Oh now that is a great point. You're right that would break this. I'll amend asap. |
@zachmoody An idea (that you maybe already had in the earlier discussion): maybe we should parse and store the "NetBox path" in an Edit: actually normalizing the base URL (= drop http:80 or https:443) should be quite enough, to make it possible to just remove the start of the URL and continue there, if I'm correct. |
Yeah, go for it. Was thinking about tackling it myself once I cleared out some of the PR backlog. Will try and help out once you get a PR going, assuming I don't run into any surprises this week. |
This is actually much better as I didn't before realize that the uri I needed is already being passed in as |
Nice, been playing around with your PR today. I noticed this change caused the Cable objects in the trace to start coming across as generic Records. Might be worth testing that in some unit tests. Also been thinking about ways we can avoid requiring additional calls to the cables endpoint since we only get a summarized cable object in the trace response. I'm leaning towards adjusting the Cable object's str method to return something that doesn't require terminations be present. E.g. def __str__(self):
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) That's a little more inline with the UI experience, and would be more useful in any other situations (if they exist) where a cable object is sent without terminations. Though if anyone has a better approach, I'm all for it. Just don't like making any more calls to NetBox than are necessary. |
Oh wow, ok I did not see that yet. Yeah I'll try to play around with it to reproduce.
That is really nice since as you said it does match with the ui experience. I like it, if we need the full info for a report we can just query the terminations instead of printing the cable. Muuuch cleaner. Also, many fewer calls like you said - it's an all around win. Feel free to push up unless you want me to. FYI I sent in a PR for integration testing at #315 - this change may be something that be integration tested especially with the upcoming cable tracing changes..? |
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.
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.
Ok, this is fixed up in 4a0d8ca.
.trace() output looks like this now.
Calling
Nice! Had a cursory look, seemed off to a good start. Hope to dig into it more later. |
I have this in a working state here, but the code is not solid enough I would feel happy about merging it. More I would like your feedback on how you would like to approach this. The main issue is the
response_loader
function needing to be able to handle multiplereturn_obj
types since the trace endpoint returns multiple types of items from the api - cables, interfaces, front ports, console ports, console server ports, etc.I saw some places where you already inspected the uri to determine object type so that is what I've done here.. I'm not married to it though. Thanks for all the work on this project! I love the module, it's been a huge help :)