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
consolidate inventory attributes #224
Conversation
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.
Other than host
everything looks good to me.
nornir/core/inventory.py
Outdated
def hostname(self): | ||
"""String used to connect to the device. Either ``hostname`` or ``self.name``""" | ||
return self.get("hostname", self.name) | ||
|
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 think we agreed on host
, 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.
I don't want to use host because host
usually refers to the Host
object so it's confusing, ie, task.host.host
vs task.host.hostname
. I am also fine naming it something else, I'd just avoid host
.
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 think host
makes sense in the way that it can refer both to an IP address and a hostname. Though I agree that task.host.host
looks a bit strange.
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 am also fine with naming it something else, but what? :) I have no other ideas.
The reason I don't like hostname
is because it can be also an IP address. IP address is not a hostname.
The reason I prefer host
is not because I like it, but because it is used by majority of libraries:
netmiko uses host
and ip
pyvmomi uses host
ncclient uses host
napalm uses hostname
django uses ALLOWED_HOSTS
While task.host.host
does not look very beautiful, I would still prefer 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.
I am fine with address
, ip
, connection_address
, mgmt_ip
, quantum_connection
, others but I am going to veto host
for consistency purposes :D
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 vote for address
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 vote host
or hostname
. I pretty strongly dislike address.
I am fine with hostname
. I would say we just leave it as that and move on.
nornir/core/inventory.py
Outdated
nos: Optional[str] = None, | ||
connection_options: Optional[Dict[str, Any]] = None, | ||
port: Optional[int] = None, | ||
platform: Optional[int] = 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.
Type hint should be Optional[str] not int
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.
Correct, the problems of copy&paste.
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.
Still an issue with the latest commit. Should be platform: Optional[str]
nornir/core/inventory.py
Outdated
connection_options: Optional[Dict[str, Any]] = None, | ||
port: Optional[int] = None, | ||
platform: Optional[int] = None, | ||
connection_options: Optional[int] = 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.
Should be Optional[Dict[str, Dict]] ?
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.
Correct again.
nornir/plugins/connections/napalm.py
Outdated
network_api_port: int, | ||
operating_system: str, | ||
nos: str, | ||
port: int, |
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.
Couldn't port
also be passed by Nornir Core as None? This is just from a Python type checking perspective.
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.
And again :D
network_api_port: int, | ||
operating_system: str, | ||
nos: str, | ||
port: int, |
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.
Same item of whether port
can be 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.
There were a couple of other places with respect to Port where I had this same concern.
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.
Will try to dig them out.
def get_connection_parameters( | ||
self, connection: Optional[str] = None | ||
) -> Dict[str, Any]: | ||
if not connection: |
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 was thinking about this and I have mixed feelings. My concern is that it is a little bit too much magic which can be not so intuitive, but I also understand the reasoning behind including this.
What I think could be less magic:
To form a dictionary with default Host attributes hostname
, port
, username
, password
, platform
and then just update with connection_options
. connection_options
would always be provided in a format understandable by the backend library (for the plugins we are shipping with nornir, user-defined plugins can do anything). For example, if the backend library uses pass
instead of password
, if you want to override Host password
, in connection_options
you should define pass
. Then to pass one resulting dictionary to the plugin and let it form its own args. In the connection plugin it would be something like:
conn_params['pass'] = options.get('pass', options.get('password'))
This looks a little ugly, but it feels a little more explicit.
I don't know what is best here, 50/50.
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.
conn_params['pass'] = options.get('pass', options.get('password'))
The problems I see with that are:
- It's going to bring inconsistent behaviors between drivers as everybody has to remember to implement exactly the same behavior.
- It's going to confuse users as in some plugins "password" works and in others they have to use "pass".
- That means I can't override the connection with code. I am talking about the possibility of calling
Host.open_connection("netmiko", my_user, my_pass,...)
directly. Even though I am being explicit the plugin might end up not using what I passed and revert to the inventory options.
To form a dictionary with default Host attributes hostname, port, username, password, platform and then just update with connection_options
Isn't that exactly what this is doing? :D
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.
1.It's going to bring inconsistent behaviors between drivers as everybody has to remember to implement exactly the same behavior.
If you need to override connection options, you should always refer to the backend library docs. Users would need to manually install the library already.
2.It's going to confuse users as in some plugins "password" works and in others they have to use "pass".
Partially, override should be used only with the library docs.
If I use ncclient
and currently my platform is set to ios
, but I need to pass cisco_iosxe
, I would want to put it in the ncclient
format:
sw1:
...
platform: ios
ncclient_options:
device_params:
name: cisco_iosxe
If I override netmiko, I would put:
sw1:
...
platform: ios
netmiko_serial_options:
device_type: terminal_server
Doesn't it make sense?
- That means I can't override the connection with code. I am talking about the possibility of calling Host.open_connection("netmiko", my_user, my_pass,...) directly.
Override by providing a new connection_options
dict formatted how the library expects it.
Isn't that exactly what this is doing? :D
Almost...
See my alternative proposals below
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 think @dbarrosop behavior is the right behavior. I initially had reservations about his solution (last night), but I thought about it more and thought this was the right behavior.
The point that convinced me was his item #1 above (i.e. nornir-core needs to enforce consistent behavior that $connection_options take precedence over top-level arguments).
My alternative proposals:
Connection plugin would take the dictionary and extract relevant parts.
then plugin would do:
|
As discussed already, the plugins' only purpose is to connect to the devices, not to implement any logic to resolve the arguments correctly, that's the mission of nornir-core, hence, plugins should already receive correct parameters. Otherwise we end up duplicating code and bringing inconsistencies. I have also renamed connection_options to "advanced_options" as I think people were confused about the purpose of it and for good reasons. This commit should illustrate how this works:
|
Yes, I disagree with this:
I think correctly connecting to devices will imply plugins have logic. For example, I think we will still need logic in the napalm-plugin to resolve the nxos_ssh issue. We will need logic to resolve naming differences like I agree with your implementation, but I think Nornir-core's job is to provide a consistent set of arguments to plugins and also to ensure $connection_options have precedence. After that it is the plugins responsibility to have any logic needed to properly make the connection (from those arguments Nornir-core provides). |
@ktbyers Question for everyone. In case of discrepancies between the nornir naming and libraries, like this:
etc. How would you specify the override in the connection options?
|
nornir/core/inventory.py
Outdated
"username": conn_params.get("username", self.username), | ||
"password": conn_params.get("password", self.password), | ||
"platform": conn_params.get("platform", self.platform), | ||
"advanced_options": conn_params.get("advanced_options", {}), |
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.
Okay, I think I misunderstood what you were doing here and the name change highlighted that? What is advanced_options, and how does it relate to what we had specified in #202?
I misunderstood a significant aspect of David's original PR which his renaming of connection_options to advanced_options has highlighted. Now I think this is different, or more likely there was miscommunication, from what we discussed in #202. I thought in #202 we agreed to the following (updated to reflect current inventory names)?
I was expecting the following arguments would be passed to napalm plugin:
Where connection options is the following dictionary unmodified (i.e. whatever was specified in connection_options['napalm'] would be passed as-is to the plugin.
The same would apply for Netmiko:
where connection_options would be the following dictionary
I then generally expected we would do the following in the plugin (using Netmiko as an example):
I generally thought that was what you were doing in this PR except that you were doing an override of the top-level parameters to ensure they came from connection_options if they were specified there...but that is clearly not the case. I need to test this PR with some real code on Netmiko and NAPALM, but I don't think I like the solution proposed in this PR. |
I think the main ambiguity/confusion from #202 is that with Netmiko connection_options I was expecting this (i.e. generally arguments that could be passed directly to Netmiko's
Whereas with NAPALM you already have a dictionary of 'additional_arguments' in optional_args so you already have a nesting of arguments:
Looking at the specification we created in #202 I see how you could infer either of the two ways. |
Yes, I think my comment wasn't clear. What I meant is that nornir should handle the resolutions of the standard attributes, then it's up to the plugin to do with them what needs to be done. Same with |
@ktbyers, I think you are describing the same behavior I wanted to implemented. The way I see this working is like this:
Then netmiko's implementation could be (current one is buggy, just saw that, but I will fix it):
I just added an extra level of indentation to separate the standard attributes override from the free-form advanced options so we can do safely The renaming of "connection_options" to "advanced_options" was just to avoid ambiguity as we kept talking about connection options but sometimes we were referring to the ${connection}_options and sometimes to the inner content that gets mapped into the underlying library. Sorry if my initial implementation, which was bad, confused everybody. I will fix it once I am sure we are on the same page. |
which would be consistent with the behavior described in my previous comment about netmiko. |
@dmfigol See my last two comments. I think we should support a standard way of overriding parameters while being completely flexible in the |
Okay, that all makes sense. Yes, there was a previous PR on the Netmiko plugin to change the order of applying the parameters versus advanced_options (whatever they were termed earlier). But I just put that on hold while we were resolving this...so we can just fix that issue as part of this. |
The main thing I am concerned about is that we are pushing complexity onto the inventory structure:
I would rather have this:
Or, I would like this even better (but this is worse from the nornir-core perspective i.e. there would have to be more magic in core to make this work)
I think you would probably have to do something like the following to make either of these work...if 'hostname', 'port', 'username', 'password', 'platform' exist as first-level keys inside the connection_options.netmiko, then they will override any argument of the same name that happens directly under the host (and they would have to get 'popped' off from 'connection_options.netmiko'). Otherwise, you generally would be forced to remove them in the plugin (i.e. 'platform' can't be fed into Netmiko ConnectHandler as **kwargs so it would have to be removed). |
I like this idea! This makes sense. If you want to override standard parameters, you use nornir naming. If you want to do something specific, you would put that under
I guess I don't really like mixing standard attributes and plugin attributes on the same level. What if there is a naming discrepancy: Another thing to think about: does |
FYI, this is what is currently implemented in this PR:
There was a commit a day or two ago that renamed an attribute to
Yes, that is a downside...I think it would just end up being similar to this (in the plugin):
You would probably do it using dictionaries and update(), but the pattern would be the same (there would be some other magic you would have to do in core regarding the case if someone put 'password' or other first-level arguments inside the connection_options). I am probably fine with either way (i.e. to leave it as-is i.e. what is implemented here) or to eliminate connection_options key. I mainly disliked 'advanced_options' so if those are staying then it doesn't matter too much to me. I probably have a slight preference for your second proposal (but David would have more insight and how much problems that causes in core, if any). Port I would probably vote to move out of first-level arguments (i.e. not have port as a first-level argument). |
@dmfigol's second proposal is fine, I don't think it adds more complexity. I added the extra indentation actually because of the "defaults" grouping I originally described but that you didn't like. For clarity the proposal I am talking about is:
Yeah, that's correct. Standard attributes are resolved by nornir but then how to use those and the advanced_options is up to the library. I only wanted to avoid having to delegate the overrides to the plugins.
I will implement the second one and evaluate how problematic might be to remove altogether the advanced_options key. I like it for OCD reasons but I think it's worth at least checking if it's viable or not. |
Sounds good to me. |
Ok, I think this one is ready. It's a lot but I think I am quite satisfied with the result. I looked at removing the "advanced_options" subblock and is not terrible. The
so from an implementation perspective is not bad. I just have mixed feelings about mixing standard and non-standard attributes. I understand how it's slightly easier if just skip that indentation but it feels ugly and slightly harder to document/explain. In summary, I am 50-50 here so if anyone has strong opinions let me know. Otherwise I will just leave it as it is now. |
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.
Just a couple of small things. I like implementation and public API.
nornir/core/inventory.py
Outdated
@@ -395,7 +389,7 @@ def __init__( | |||
for group_name, group_details in groups.items(): | |||
if group_details is None: | |||
group = Group(name=group_name, nornir=nornir) | |||
elif isinstance(group_details, dict): | |||
elif isinstance(group_details, (dict, CommentedMap)): |
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 propose we use more generic collections.Mapping
:
elif isinstance(group_details, collections.Mapping):
nornir/core/inventory.py
Outdated
@@ -413,7 +407,7 @@ def __init__( | |||
|
|||
self.hosts = {} | |||
for n, h in hosts.items(): | |||
if isinstance(h, dict): | |||
if isinstance(h, (dict, CommentedMap)): |
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.
and here :)
nornir/core/inventory.py
Outdated
nos: Optional[str] = None, | ||
connection_options: Optional[Dict[str, Any]] = None, | ||
port: Optional[int] = None, | ||
platform: Optional[int] = 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.
Still an issue with the latest commit. Should be platform: Optional[str]
nornir/core/inventory.py
Outdated
connection_options: Optional[Dict[str, Any]] = None, | ||
port: Optional[int] = None, | ||
platform: Optional[int] = None, | ||
advanced_options: Optional[int] = 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.
Optional[Dict[str, Any]]
I don't have a strong opinion about |
I am fine with the advanced_options either way (either being there or not in inventory structure). I wonder if we should use a different term for it though if it stays (since advanced_options could have basic arguments) Maybe back to |
I think for the plugins/connections/napalm.py you would want these changes;
timeout is already handled by the Here is the inventory I was testing with (this was groups.yaml):
|
We should merge what we have into the 2.0 branch (fairly soon). So we have a new base to work from. |
You are right, that was a leftover from the old behavior.
I will address the comments, rename "advanced_options" back to "connection_options" and merge. If there are other details we can fix later on. |
This PR: