8000 Adding inventory plugin for netbox by dbarrosop · Pull Request #180 · nornir-automation/nornir · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Adding inventory plugin for netbox #180

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

Merged
merged 7 commits into from
Jul 30, 2018
Merged

Adding inventory plugin for netbox #180

merged 7 commits into from
Jul 30, 2018

Conversation

dbarrosop
Copy link
Contributor

No description provided.

@dbarrosop
Copy link
Contributor Author

@bdlamprecht this is what I wanted to do with "requests-mock". Right now we just need to mock the response of netbox by putting the contents we expect in a file:

https://github.com/nornir-automation/nornir/pull/180/files#diff-62b8d7e6b6ab84dc213beb67c0081642

And then another json blob with the expected inventory:

https://github.com/nornir-automation/nornir/pull/180/files#diff-1a9b65979e19e3ccdaf998204841d7cd

I apologize for making you implement initially the containerized test, I thought it was going to be easier :(

},
"2-Distribution": {
"name": "2-Distribution",
"nornir_host": {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bdlamprecht This doesn't seem correct, I guess you wanted to put here just the address, not the object itself, is that correct?

Choose a reason for hiding this comment

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

I don't quite follow your question here.

Choose a reason for hiding this comment

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

Ah, yes, I see what you're referring to.

This line...

temp["nornir_host"] = d["primary_ip"]

Should actually look like this:

                    temp["nornir_host"] = d["primary_ip"]["address"]

I saw that before, but must have missed it when committing the changes. 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, will update :)

8000

# Copy temporary dict to outer dict
devices[d["name"]] = temp

except KeyError:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the reason this could happen?

Copy link
@bdlamprecht bdlamprecht Jul 12, 2018

Choose a reason for hiding this comment

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

This is for when the nb_token is incorrect, it basically returns a dict such as {results:"Invalid token"}. Did know how else to do this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you get a status_code error? Something like a 403? Because then Response.raise_for_status might be a better alternative. Can you check and tell me which status code it returns?

# Attempt to add 'platform' based of value in 'slug'
try:
temp["nornir_nos"] = d["platform"]["slug"]
except TypeError:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the reason this could happen?

Choose a reason for hiding this comment

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

Similar to the above try / except clause above for nornir_host, this one is for when a platform hasn't been assigned and use_slugs is True. Without a platform, there is no attribute for slug.

When use_slugs is False and no platform is defined, it automatically returns a None value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so this should be temp["nornir_nos"] = d.get("platform", {}).get("slug") instead of the try...except... block.

# Add value for IP address
try:
temp["nornir_host"] = d["primary_ip"]
except TypeError:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What is the reason this could happen?

Choose a reason for hiding this comment

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

A device can have many interfaces, each with many ip_addresses.
One interface / ip_address combination needs to be selected as the primary_ip for each device.

This is try/except clause is for when that hasn't happened, then it will return an object with a None value for nornir_host.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, will replace with temp["nornir_host"] = d.get("primary_ip", None)

@coveralls
Copy link
coveralls commented Jul 12, 2018

Pull Request Test Coverage Report for Build 835

  • 29 of 35 (82.86%) changed or added relevant lines in 1 file are covered.
  • 9 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.3%) to 92.022%

Changes Missing Coverage Covered Lines Changed/Added Lines %
nornir/plugins/inventory/netbox.py 29 35 82.86%
Files with Coverage Reduction New Missed Lines %
nornir/plugins/tasks/commands/remote_command.py 1 94.44%
nornir/plugins/tasks/connections/paramiko_connection.py 1 92.31%
nornir/core/inventory.py 7 94.7%
Totals Coverage Status
Change from base Build 757: -0.3%
Covered Lines: 1015
Relevant Lines: 1103

💛 - Coveralls

@dbarrosop
Copy link
Contributor Author

@bdlamprecht I added a few comments inline for the PR.

@bdlamprecht
Copy link
bdlamprecht commented Jul 12, 2018

I thought it would be helpful to provide the output from the import for all of the required objects in the proper order (i.e. sites, device_roles, manufacturers, device_types, platforms, devices, interfaces, ip_addresses, and finally, setting the primary_ip for each device).

It may or may not be what you wanted, but better safe than sorry. 😄

JSON output from creating sites:

{'region': None, 'contact_name': '', 'created': '2018-07-13', 'id': 1, 'last_updated': '2018-07-13T00:01:38.769801Z', 'name': 'Santa Clara, CA', 'status': 1, 'asn': None, 'slug': 'santa-clara-ca', 'facility': '', 'shipping_address': '', 'contact_phone': '', 'comments': '', 'contact_email': '', 'physical_address': '', 'time_zone': None, 'tenant': None, 'description': ''}
{'region': None, 'contact_name': '', 'created': '2018-07-13', 'id': 2, 'last_updated': '2018-07-13T00:01:38.793181Z', 'name': 'San Jose, CA', 'status': 1, 'asn': None, 'slug': 'san-jose-ca', 'facility': '', 'shipping_address': '', 'contact_phone': '', 'comments': '', 'contact_email': '', 'physical_address': '', 'time_zone': None, 'tenant': None, 'description': ''}
{'region': None, 'contact_name': '', 'created': '2018-07-13', 'id': 3, 'last_updated': '2018-07-13T00:01:38.815386Z', 'name': 'Sunnyvale, CA', 'status': 1, 'asn': None, 'slug': 'sunnyvale-ca', 'facility': '', 'shipping_address': '', 'contact_phone': '', 'comments': '', 'contact_email': '', 'physical_address: '', 'time_zone': None, 'tenant': None, 'description': ''}

JSON output from creating device_roles:

{'color': '2196f3', 'name': 'Router', 'slug': 'rt', 'vm_role': False, 'id': 1}
{'color': '4caf50', 'name': 'Switch', 'slug': 'sw', 'vm_role': False, 'id': 2}

JSON output from creating manufacturers:

{'name': 'Arista', 'slug': 'arista', 'id': 1}
{'name': 'Cisco', 'slug': 'cisco', 'id': 2}
{'name': 'Juniper', 'slug': 'juniper', 'id': 3}

JSON output from creating device_types (most likely don't need this many, but anyways):

{'is_full_depth': False, 'is_network_device': True, 'id': 1, 'comments': '', 'is_console_server': False, 'slug': '2960x-24ps', 'u_height': 1, 'manufacturer': 2, 'model': '2960X-24PS', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'WS-C2960X-24PS'}
{'is_full_depth': False, 'is_network_device': True, 'id': 2, 'comments': '', 'is_console_server': False, 'slug': '3650-48tq-l', 'u_height': 1, 'manufacturer': 2, 'model': '3650-48TQ-L', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'WS-C3650-48TQ-L'}
{'is_full_depth': False, 'is_network_device': True, 'id': 3, 'comments': '', 'is_console_server': False, 'slug': '3850-12xs', 'u_height': 1, 'manufacturer': 2, 'model': '3850-12XS', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'WS-C3850-12XS'}
{'is_full_depth': False, 'is_network_device': True, 'id': 4, 'comments': '', 'is_console_server': False, 'slug': '6880-x-le', 'u_height': 4, 'manufacturer': 2, 'model': '6880-X-LE', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'C6880-X-LE'}
{'is_full_depth': False, 'is_network_device': True, 'id': 5, 'comments': '', 'is_console_server': False, 'slug': 'isr4431', 'u_height': 1, 'manufacturer': 2, 'model': 'ISR4431', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'ISR4431/K9'}
{'is_full_depth': False, 'is_network_device': True, 'id': 6, 'comments': '', 'is_console_server': False, 'slug': 'ex2200-24t', 'u_height': 1, 'manufacturer': 3, 'model': 'EX2200-24T', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'EX2200-24T'}
{'is_full_depth': False, 'is_network_device': True, 'id': 7, 'comments': '', 'is_console_server': False, 'slug': 'ex4200-24f', 'u_height': 1, 'manufacturer': 3, 'model': 'EX4200-24F', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'EX4200-24F'}
{'is_full_depth': False, 'is_network_device': True, 'id': 8, 'comments': '', 'is_console_server': False, 'slug': 'ex4300-48p', 'u_height': 1, 'manufacturer': 3, 'model': 'EX4300-48P', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'EX4300-48P'}
{'is_full_depth': False, 'is_network_device': True, 'id': 9, 'comments': '', 'is_console_server': False, 'slug': 'ex4550-32f', 'u_height': 1, 'manufacturer': 3, 'model': 'EX4550-32F', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'EX4550-32F'}
{'is_full_depth': False, 'is_network_device': True, 'id': 10, 'comments': '', 'is_console_server': False, 'slug': 'mx240', 'u_height': 2, 'manufacturer': 3, 'model': 'MX240', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'MX240'}
{'is_full_depth': False, 'is_network_device': True, 'id': 11, 'comments': '', 'is_console_server': False, 'slug': 'mx480', 'u_height': 4, 'manufacturer': 3, 'model': 'MX480', 'interface_ordering': 1, 'subdevice_role': None, 'is_pdu': False, 'part_number': 'MX480'}

JSON output from creating platforms (included some which don't have a napalm_driver):

{'rpc_client': '', 'manufacturer': None, 'napalm_driver': 'eos', 'id': 1, 'name': 'Arista EOS', 'slug': 'eos'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': 'ios', 'id': 2, 'name': 'Cisco IOS', 'slug': 'ios'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': 'nxos_ssh', 'id': 3, 'name': 'Cisco NX-OS', 'slug': 'nxos'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': 'iosxr', 'id': 4, 'name': 'Cisco IOS-XR', 'slug': 'iosxr'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': 'junos', 'id': 5, 'name': 'Juniper Junos', 'slug': 'junos'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': '', 'id': 6, 'name': 'RedHat Linux', 'slug': 'rhel'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': '', 'id': 7, 'name': 'CentOS Linux', 'slug': 'centos'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': '', 'id': 8, 'name': 'Ubuntu Linux', 'slug': 'ubuntu'}
{'rpc_client': '', 'manufacturer': None, 'napalm_driver': '', 'id': 9, 'name': 'Fedora Linux', 'slug': 'fedora'}

JSON output from creating devices:

{'device_role': 1, 'virtual_chassis': None, 'id': 1, 'primary_ip6': None, 'status': 1, 'cluster': None, 'device_type': 11, 'vc_priority': None, 'rack': None, 'platform': None, 'position': None, 'face': None, 'tenant': None, 'serial': '', 'vc_position': None, 'name': '1-Core', 'primary_ip4': None, 'created': '2018-07-13', 'asset_tag': None, 'comments': '', 'last_updated': '2018-07-13T00:01:40.065452Z', 'site': 3}
{'device_role': 1, 'virtual_chassis': None, 'id': 2, 'primary_ip6': None, 'status': 1, 'cluster': None, 'device_type': 9, 'vc_priority': None, 'rack': None, 'platform': None, 'position': None, 'face': None, 'tenant': None, 'serial': '', 'vc_position': None, 'name': '2-Distribution', 'primary_ip4': None, 'created': '2018-07-13', 'asset_tag': None, 'comments': '', 'last_updated': '2018-07-13T00:01:40.108345Z', 'site': 3}
{'device_role': 2, 'virtual_chassis': None, 'id': 3, 'primary_ip6': None, 'status': 1, 'cluster': None, 'device_type': 2, 'vc_priority': None, 'rack': None, 'platform': None, 'position': None, 'face': None, 'tenant': None, 'serial': '', 'vc_position': None, 'name': '3-Access', 'primary_ip4': None, 'created': '2018-07-13', 'asset_tag': None, 'comments': '', 'last_updated': '2018-07-13T00:01:40.149306Z', 'site': 2}

JSON output from creating interfaces:

{'tagged_vlans': [], 'mgmt_only': False, 'device': 1, 'mtu': None, 'name': 'lo0.0', 'id': 1, 'untagged_vlan': None, 'mac_address': None, 'mode': None, 'lag': None, 'form_factor': 1200, 'description': '', 'enabled': True}
{'tagged_vlans': [], 'mgmt_only': False, 'device': 2, 'mtu': None, 'name': 'lo0.0', 'id': 2, 'untagged_vlan': None, 'mac_address': None, 'mode': None, 'lag': None, 'form_factor': 1200, 'description': '', 'enabled': True}
{'tagged_vlans': [], 'mgmt_only': False, 'device': 3, 'mtu': None, 'name': 'Loopback0', 'id': 3, 'untagged_vlan': None, 'mac_address': None, 'mode': None, 'lag': None, 'form_factor': 1200, 'description': '', 'enabled': True}

JSON output from creating ip_addresses:

{'nat_inside': None, 'role': None, 'last_updated': '2018-07-13T00:01:40.261970Z', 'interface': 1, 'id': 1, 'tenant': None, 'address': '10.0.1.1/32', 'vrf': None, 'status': 1, 'description': '', 'created': '2018-07-13'}
{'nat_inside': None, 'role': None, 'last_updated': '2018-07-13T00:01:40.283004Z', 'interface': 2, 'id': 2, 'tenant': None, 'address': '172.16.2.1/32', 'vrf': None, 'status': 1, 'description': '', 'created': '2018-07-13'}
{'nat_inside': None, 'role': None, 'last_updated': '2018-07-13T00:01:40.301024Z', 'interface': 3, 'id': 3, 'tenant': None, 'address': '192.168.3.1/32', 'vrf': None, 'status': 1, 'description': '', 'created': '2018-07-13'}

Non-JSON output from setting primary_ip of existing devices:

True
True
True

# Assign temporary dict to outer dict
devices[d["name"]] = temp

except KeyError:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bdlamprecht when this happens, what status code does nsot return? I am wondering if we can just do raise_for_status.

Copy link
@bdlamprecht bdlamprecht Jul 18, 2018

Choose a reason for hiding this comment

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

I pretty sure you meant NetBox instead of nsot here (:smile:) :

what status code does nsot return?

I don't think it returns a status code.
When I add the following code after passing an invalid nb_token:

        except KeyError:
            
            # DEBUG
            print(nb_devices)

I get the following dict back:

{'detail': 'Invalid token'}

Hope that answers your question.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It should return either a 401 or 403, otherwise, that's clearly a bug on Netbox and not very nice from an API perspective.

I think it's better if we test though. Would you mind changing:

nb_devices = requests.get(
      "{}/api/dcim/devices/?limit=0".format(nb_url), headers=headers
).json() 

with:

r = requests.get("{}/api/dcim/devices/?limit=0".format(nb_url), headers=headers)
r.raise_for_status()
nb_devices = r.json()

And test it with both a wrong and a correct token?

Choose a reason for hiding this comment

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

With that change, it does appear to return a 403 with an invalid token:

HTTPError: 403 Client Error: Forbidden for url: http://localhost:8080/api/dcim/devices/?limit=0

Is that all you needed me to check on?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! That's what I was hoping for. I will commit a couple of more changes and I think we will be ready to go here.

Choose a reason for hiding this comment

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

Awesome! 💯
I'm happy to test it out once you get it all cleaned up the way that you'd like.
Cheers

@bdlamprecht
Copy link

@dbarrosop Hoping to get this inventory plugin merged soon so I can continue my testing to eventually migrate away from Ansible sooner rather than later.

NetBox is great and I'd like to still use it as my source-of-truth for 10,000+ devices (and at least another 20,000 will be added by this time next year).

Keep me updated, and again, thanks for nornir and your willingness to help so far!

@dbarrosop
Copy link
Contributor Author

@bdlamprecht this one should be ready, please test it works as expected.

@bdlamprecht
Copy link
bdlamprecht commented Jul 27, 2018

Yeah, it appears to work fine, both with and without invalid tokens (works and raises a 403 exception respectively).

I attempted to look into how you're using requests-mock to test it, but still don't understand how that works.
Perhaps if I spent more time review it I would, but don't know that I need to know.

Thanks again!

@bdlamprecht

One last thought, as I mentioned, the NetBox instance that I'm testing this against has only 624 devices in it and it takes several seconds (~5) to create an NBInventory with that relatively small number of hosts.
Once you start pushing 10,000+, I imagine it could potentially significantly more time.

I'm wondering if it would be possible to code up a method to only query against certain site, certain role, or certain model (all of which are possible by altering the URL from this:

"{}/api/dcim/devices/?limit=0".format(nb_url)"

to this (respective to the order mentioned above):

"{}/api/dcim/devices/?site=foo&limit=0".format(nb_url)"
"{}/api/dcim/devices/?role=bar&limit=0".format(nb_url)"
"{}/api/dcim/devices/?model=baz&limit=0".format(nb_url)"

Or do you think the current implementation is good and it is up to the user to then filter on what devices they are specifically concerned with?

@dmfigol
Copy link
Collaborator
dmfigol commented Jul 27, 2018

sorry for barging in,
I think it could be done, but not necessarily should be done at this point - premature optimization is not always good. I am pretty sure if someone comes with 10k inventory, slow inventory will not be the only problem.
That said, would be it be possible for you to run proposed queries and measure the running time and number of devices?

@dbarrosop
Copy link
Contributor Author
dbarrosop commented Jul 28, 2018

That’s easy to implement, however, I’d like you to check first why it takes 5s for only 624 devices. Can you run time curl netbox_url_that_retrieves_all_hosts and time your_code_that_only_creates_nornir_inventory?

@bdlamprecht
Copy link

@dbarrosop Yeah, that is a good idea about measuring the time.

Honestly, I may have exaggerated ( 😄 ) the amount of time by saying ~5 seconds, but in all actuality is most likely ~2 seconds.

I'll do that on Monday when I'm working again and let you know.

Cheers!

@dbarrosop
Copy link
Contributor Author

Should we merge this one? We can send a different PR later on to filter hosts by site or any other attribute later on if needed.

@dmfigol
Copy link
Collaborator
dmfigol commented Jul 30, 2018

LGTM!

@bdlamprecht
Copy link

Yeah, I say merge it and then it can be enhanced later.

@dbarrosop dbarrosop merged commit eba2a71 into develop Jul 30, 2018
@dbarrosop dbarrosop deleted the net branch October 27, 2018 11:36
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.

4 participants
0