Skip to content
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

Merged
merged 17 commits into from Dec 29, 2020

Conversation

raddessi
Copy link
Contributor

@raddessi raddessi commented Aug 1, 2020

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 multiple return_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 :)

@raddessi
Copy link
Contributor Author

raddessi commented Aug 1, 2020

I would also like to add this for the other items that have trace methods, but most of those will need to be created in the dcim submodule first so I have not done that yet, pending your thoughts on how to proceed.

)
)
else:
# the last trace can consist of [cable_a, None, None] if there is no
Copy link
Contributor Author

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

@zachmoody
Copy link
Contributor

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 .trace property instead of messing with the more universal response_loader. We could call RODetailEndpoint and then return what we needed to from its return.

@raddessi
Copy link
Contributor Author

raddessi commented Aug 6, 2020

Ah ok that makes good sense. I'll play around with it and send something over. Thanks!

Copy link
Contributor Author

@raddessi raddessi left a 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 Show resolved Hide resolved
elif url_path.startswith("/api/dcim/rear-ports"):
return_obj_class = RearPorts
else:
raise NotImplementedError(
Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@raddessi raddessi changed the title WIP: Added a trace method to the Interfaces class Added a trace method to the Interfaces class Aug 20, 2020
@raddessi
Copy link
Contributor Author

re: the failing tests.. yeah I have never seen python complain that self actually needs to be passed back in to a function when calling it but I got this error when testing locally and that seemed to fix it in my case:

~/dev/pynetbox/pynetbox/core/response.py in __repr__(self)
    215 
    216     def __repr__(self):
--> 217         return str(self)
    218 
    219     def __getstate__(self):

~/dev/pynetbox/pynetbox/models/dcim.py in __str__(self)
    227             termination_a_name = self.termination_a.name
    228         except AttributeError:
--> 229             self.termination_a.full_details()
    230             termination_a_name = self.termination_a.name
    231 

TypeError: full_details() missing 1 required positional argument: 'self'

I'll dig in to why it's wanting that locally and fix that issue. Comments would still be appreciated, thanks!

Copy link
Contributor Author

@raddessi raddessi left a 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

Comment on lines 226 to 233
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)
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

@raddessi raddessi Dec 1, 2020

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 :)

Copy link
Contributor

@zachmoody zachmoody Dec 2, 2020

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.

Copy link
Contributor Author

@raddessi raddessi Dec 3, 2020

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.

Copy link
Contributor Author

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.

Comment on lines 529 to 555
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,
),
]
Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@raddessi raddessi changed the title Added a trace method to the Interfaces class Added a trace property to the traceable interface classes Aug 22, 2020
@zachmoody zachmoody self-requested a review September 2, 2020 02:26
@raddessi
Copy link
Contributor Author

@zachmoody do you have any thoughts?

@zachmoody
Copy link
Contributor

@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.

@raddessi
Copy link
Contributor Author

raddessi commented Sep 16, 2020

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 :)

Copy link
Contributor

@zachmoody zachmoody left a 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 😄.

Comment on lines 226 to 233
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)
Copy link
Contributor

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 Show resolved Hide resolved
elif url_path.startswith("/api/dcim/rear-ports"):
return_obj_class = RearPorts
else:
raise NotImplementedError(
Copy link
Contributor

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.

pynetbox/models/dcim.py Show resolved Hide resolved
Comment on lines 529 to 555
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,
),
]
Copy link
Contributor

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.

Copy link
Contributor Author

@raddessi raddessi left a 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.

elif url_path.startswith("/api/dcim/rear-ports"):
return_obj_class = RearPorts
else:
raise NotImplementedError(
Copy link
Contributor Author

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 Show resolved Hide resolved
Comment on lines 226 to 233
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)
Copy link
Contributor Author

@raddessi raddessi Dec 1, 2020

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 :)

Comment on lines 529 to 555
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,
),
]
Copy link
Contributor Author

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
Copy link
Contributor

Thanks for all the great feedback. I'm sorry I dropped off the map, life got in the way.

Ha, no worries, been in the same boat myself.

Copy link
Contributor Author

@raddessi raddessi left a 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.

tests/fixtures/dcim/interface_trace.json Show resolved Hide resolved
tests/test_dcim.py Outdated Show resolved Hide resolved
@raddessi
Copy link
Contributor Author

raddessi commented Dec 7, 2020

Right on! Is there anything left you would like done here then? If not then I feel like it's ready to go in.

@raddessi
Copy link
Contributor Author

raddessi commented Dec 9, 2020

Hold on this actually, I broke functionality in the last update. Will fix asap.

Copy link
Contributor Author

@raddessi raddessi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another change

pynetbox/models/dcim.py Outdated Show resolved Hide resolved
@raddessi
Copy link
Contributor Author

raddessi commented Dec 9, 2020

OK that should be resolved now. I'm not sure why I had the else statement in there to begin with.

@zachmoody
Copy link
Contributor

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.

@raddessi
Copy link
Contributor Author

raddessi commented Dec 9, 2020

Yeah ok no problem. Do you have any plans for an integration testing suite?

@zachmoody
Copy link
Contributor

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. 🤔

@raddessi
Copy link
Contributor Author

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.

@markkuleinio
Copy link
Contributor

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 /netbox/api/dcim/something/something.

Checking because we fixed that kind of issue somewhere else in the code recently.

@raddessi
Copy link
Contributor Author

Oh now that is a great point. You're right that would break this. I'll amend asap.

@markkuleinio
Copy link
Contributor

markkuleinio commented Dec 28, 2020

@zachmoody An idea (that you maybe already had in the earlier discussion): maybe we should parse and store the "NetBox path" in an Api instance attribute or something like that? To prevent the need to parse the same thing in several places.

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.

@zachmoody
Copy link
Contributor

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.

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.

@raddessi
Copy link
Contributor Author

This is actually much better as I didn't before realize that the uri I needed is already being passed in as self.endpoint.url, so this issue should be fixed now and is less confusing as well.

@zachmoody
Copy link
Contributor

This is actually much better as I didn't before realize that the uri I needed is already being passed in as self.endpoint.url, so this issue should be fixed now and is less confusing as well.

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.

@raddessi
Copy link
Contributor Author

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.

Oh wow, ok I did not see that yet. Yeah I'll try to play around with it to reproduce.

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.

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..?

zmoody added 5 commits December 29, 2020 10:13
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.
@zachmoody
Copy link
Contributor

Oh wow, ok I did not see that yet. Yeah I'll try to play around with it to reproduce.

Ok, this is fixed up in 4a0d8ca.

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.

.trace() output looks like this now.

In [28]: x.trace()
Out[28]: [[eth2, Cable #2, fp0], [rp0, Cable #5, rp0], [fp0, Cable #4, eth2]]

Calling x.trace()[1] or x.trace()[1].full_details() will populate the terminations, but that's kind of a round about way of getting data that's already in x.trace()[0] and x.trace()[2].

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..?

Nice! Had a cursory look, seemed off to a good start. Hope to dig into it more later.

@zachmoody zachmoody merged commit acb7670 into netbox-community:master Dec 29, 2020
@raddessi raddessi deleted the master.cable-traces branch December 29, 2020 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants