Using Pynetbox

Pynetbox is a Python API client library for NetBox. Here are some basic examples of using pynetbox.

In this page:

Here we assume that you already have NetBox running (I have NetBox 2.8.6 in this demo), and you have an API token set up for the user that you are using in NetBox.

Getting started

Let’s start with creating and activating a virtual environment for our project.

Note: If you don’t want to use venv, just skip over this step and continue with installing pynetbox.

markku@btest:~$ sudo apt install python3-venv
[sudo] password for markku:
Reading package lists... Done
Building dependency tree
Reading state information... Done
python3-venv is already the newest version (3.7.3-1).
0 upgraded, 0 newly installed, 0 to remove and 56 not upgraded.
markku@btest:~$ mkdir pynetboxdemo
markku@btest:~$ cd pynetboxdemo
markku@btest:~/pynetboxdemo$ python3 -m venv venv
markku@btest:~/pynetboxdemo$ source venv/bin/activate
(venv) markku@btest:~/pynetboxdemo$

Now we can install pynetbox:

(venv) markku@btest:~/pynetboxdemo$ pip install pynetbox
Collecting pynetbox
  Downloading pynetbox-4.3.1.tar.gz (47 kB)
     |████████████████████████████████| 47 kB 691 kB/s
Collecting requests<3.0,>=2.20.0
  Downloading requests-2.24.0-py2.py3-none-any.whl (61 kB)
     |████████████████████████████████| 61 kB 420 kB/s
Collecting six==1.*
  Downloading six-1.15.0-py2.py3-none-any.whl (10 kB)
Collecting chardet<4,>=3.0.2
  Using cached chardet-3.0.4-py2.py3-none-any.whl (133 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2020.4.5.2-py2.py3-none-any.whl (157 kB)
     |████████████████████████████████| 157 kB 4.5 MB/s
Collecting idna<3,>=2.5
  Downloading idna-2.9-py2.py3-none-any.whl (58 kB)
     |████████████████████████████████| 58 kB 4.3 MB/s
Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
  Downloading urllib3-1.25.9-py2.py3-none-any.whl (126 kB)
     |████████████████████████████████| 126 kB 4.5 MB/s
Using legacy setup.py install for pynetbox, since package 'wheel' is not installed.
Installing collected packages: chardet, certifi, idna, urllib3, requests, six, pynetbox
    Running setup.py install for pynetbox ... done
Successfully installed certifi-2020.4.5.2 chardet-3.0.4 idna-2.9 pynetbox-4.3.1 requests-2.24.0 six-1.15.0 urllib3-1.25.9
(venv) markku@btest:~/pynetboxdemo$

The dependencies were installed automatically. Let’s start the Python shell and import pynetbox to run our examples.

(venv) markku@btest:~/pynetboxdemo$ python3
Python 3.7.3 (default, Apr  3 2019, 05:39:12)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pynetbox
>>>

We need a pynetbox Api instance that we will use to interact with NetBox (obviously you need to replace the URL and the token with your own):

>>> netbox = pynetbox.api("http://netbox-test.example.com/", token="7d754ac3d257fcead9d3bad92dfa46c5e1238f9c")
>>> netbox.version
'2.8'
>>>

(Here netbox is just an object name I’ll be using in these examples, you can use nb or whatever as you want.) netbox.version does not show the exact NetBox version but it returns the NetBox API version.

Now we are ready for the actual examples.

Creating and changing something

First we’ll create a site:

>>> site = netbox.dcim.sites.create({"name": "Testsite"})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/endpoint.py", line 287, in create
    ).post(args[0] if args else kwargs)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 381, in post
    return self._make_call(verb="post", data=data)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 274, in _make_call
    raise RequestError(req)
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'slug': ['This field is required.']}
>>>

That’s ugly but it resulted with an exception due to a missing field. When creating or modifying object with pynetbox, if unsure, check the NetBox GUI for fields that are mandatory, or use the API browser that is the “API” link in the bottom of the NetBox GUI.

But anyway, it raised a RequestError since the request was not valid. In your own code you would handle it like this:

>>> try:
...     site = netbox.dcim.sites.create({"name": "Testsite"})
... except pynetbox.RequestError as e:
...     print("Could not create the site, error: {}".format(str(e)))
...
Could not create the site, error: The request failed with code 400 Bad Request: {'slug': ['This field is required.']}
>>>

Let’s create the site again, this time for real:

>>> site = netbox.dcim.sites.create({"name": "Testsite", "slug": "testsite"})
>>> site
Testsite
>>> type(site)
<class 'pynetbox.core.response.Record'>
>>>

The site that was created is now a Record object. You don’t have to think about the exact object type, just know that it is an object, not just a name or ID.

But since it is an object, you can see the various attributes or methods by pressing tab key after writing “site.” (works in Linux at least):

>>> site.<tab>
site.api               site.default_ret(      site.last_updated      site.shipping_address
site.asn               site.delete(           site.latitude          site.slug
site.comments          site.description       site.longitude         site.status
site.contact_email     site.endpoint          site.name              site.tags
site.contact_name      site.facility          site.physical_address  site.tenant
site.contact_phone     site.full_details(     site.region            site.time_zone
site.created           site.has_details       site.save(             site.update(
site.custom_fields     site.id                site.serialize(        site.url
>>> site.

So you can check for example:

>>> site.status
Active
>>>

The site status is Active because that is the default when creating a site (in GUI as well).

For printing out all the data about the object we can use pprint and dict:

>>> from pprint import pprint
>>> pprint(dict(site))
{'asn': None,
 'comments': '',
 'contact_email': '',
 'contact_name': '',
 'contact_phone': '',
 'created': '2020-06-20',
 'custom_fields': {},
 'description': '',
 'facility': '',
 'id': 3,
 'last_updated': '2020-06-20T16:33:09.605354+03:00',
 'latitude': None,
 'longitude': None,
 'name': 'Testsite',
 'physical_address': '',
 'region': None,
 'shipping_address': '',
 'slug': 'testsite',
 'status': {'id': 1, 'label': 'Active', 'value': 'active'},
 'tags': [],
 'tenant': None,
 'time_zone': None}
>>>

Not much new information there, just the defaults, as we didn’t enter anything else.

We can change the object properties and save the object:

>>> site.description = "This is a test site"
>>> site.save()
True
>>>

There is also an update() method in the object, which works by giving it a dict of all the changed properties in one shot and it will save automatically:

>>> site.update({"contact_email": "admin@example.com", "contact_name": "Local Admin"})
True
>>>

Verification:

>>> pprint(dict(site))
{'asn': None,
 'comments': '',
 'contact_email': 'admin@example.com',
 'contact_name': 'Local Admin',
 'contact_phone': '',
 'created': '2020-06-20',
 'custom_fields': {},
 'description': 'This is a test site',
 'facility': '',
 'id': 3,
 'last_updated': '2020-06-20T16:33:09.605354+03:00',
 'latitude': None,
 'longitude': None,
 'name': 'Testsite',
 'physical_address': '',
 'region': None,
 'shipping_address': '',
 'slug': 'testsite',
 'status': {'id': 1, 'label': 'Active', 'value': 'active'},
 'tags': [],
 'tenant': None,
 'time_zone': None}
>>>

You can use either way to change the objects in pynetbox.

Btw, what’s a slug anyway: it is just an URL-friendly string that is unique for each object of the same kind. When creating a site in NetBox GUI it will generate the slug for you automatically. In Python you need to generate it yourself.

Retrieving objects

Let’s check that the previous site object changes were actually saved in NetBox by retrieving the data again to another object and pprinting it:

>>> same_site = netbox.dcim.sites.get(name="Testsite")
>>> pprint(dict(same_site))
{'asn': None,
 'circuit_count': None,
 'comments': '',
 'contact_email': 'admin@example.com',
 'contact_name': 'Local Admin',
 'contact_phone': '',
 'created': '2020-06-20',
 'custom_fields': {},
 'description': 'This is a test site',
 'device_count': None,
 'facility': '',
 'id': 3,
 'last_updated': '2020-06-20T16:48:23.317548+03:00',
 'latitude': None,
 'longitude': None,
 'name': 'Testsite',
 'physical_address': '',
 'prefix_count': None,
 'rack_count': None,
 'region': None,
 'shipping_address': '',
 'slug': 'testsite',
 'status': {'id': 1, 'label': 'Active', 'value': 'active'},
 'tags': [],
 'tenant': None,
 'time_zone': None,
 'virtualmachine_count': None,
 'vlan_count': None}
>>>

Looks good.

We used the get() method of the sites model in the dcim app of NetBox (netbox.dcim.sites.get()). There is a data model behind all this, and an easy way to find out “where” all the things are is to just browse around the NetBox GUI and see what’s in the address bar. The most commonly used ones for me are (using the previously created netbox API object again as the start):

  • netbox.tenancy.tenants
  • netbox.dcim.sites
  • netbox.dcim.devices
  • netbox.dcim.interfaces
  • netbox.ipam.prefixes
  • netbox.ipam.ip_addresses

The last one in the list is interesting in that sense that in NetBox GUI URL it is http://netbox-test.example.com/ipam/ip-addresses/ so it is with a dash (-) but the NetBox app name in pynetbox is with an underscore (_). The reason for that is that in Python you cannot have dashes in the object names, so pynetbox uses underscores and translates them to dashes when doing the actual API calls.

get() only returns one object at most, so you need to give it such arguments that there is only one match, otherwise you will get an empty object (None) or it will raise a ValueError. Let’s create two more sites and then test get() again:

>>> site2 = netbox.dcim.sites.create({"name": "Second testsite", "slug": "second-testsite", "contact_email": "admin@example.com"})
>>> site3 = netbox.dcim.sites.create({"name": "Customer site", "slug": "customer-site", "contact_email": "user@customer.com"})
>>> get_site = netbox.dcim.sites.get(contact_email="admin@example.com")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/endpoint.py", line 143, in get
    "get() returned more than one result. "
ValueError: get() returned more than one result. Check that the kwarg(s) passed are valid for this endpoint or use filter() or all() instead.
>>>

There you go, it recommends to use filter() or all() instead.

But before going to them, let’s show that get() can also be used with the object ID number. In our “Customer site” the object ID is (your value can be different):

>>> site3.id
5
>>>

So, if at any point later we need to get that same object from NetBox again, we can use the ID with get():

>>> customer_site = netbox.dcim.sites.get(5)
>>> customer_site.name
'Customer site'
>>>

Basically you can either give the object ID number (and nothing else), or you can give one or more keyword arguments describing the exact match for the single object you want to retrieve.

Ok, let’s go to filter() and all() now. We’ll try all() first:

>>> sites = netbox.dcim.sites.all()
>>> type(sites)
<class 'list'>
>>> len(sites)
3
>>> sites
[Customer site, Second testsite, Testsite]
>>>

all() returns a list of all the requested objects in the model.

When growing your NetBox data you obviously don’t want to always retrieve all objects but you want to filter the results:

>>> admins_sites = netbox.dcim.sites.filter(contact_email="admin@example.com")
>>> admins_sites
[Second testsite, Testsite]
>>>

By giving the appropriate arguments to filter() you get only the matching objects.

You can also give a plain string as an argument:

>>> netbox.dcim.sites.filter("testsite")
[Second testsite, Testsite]
>>> netbox.dcim.sites.filter("admin")
[Second testsite, Testsite]
>>>

As you see it didn’t only search from the site names but also from other fields, so when using this string search you may need to also validate your search results in your code if you are looking for something specific.

One more thing about filter(). You need to be careful with the arguments that you give to it because the arguments are sent almost as-is to NetBox and it does not complain about extra (unknown) arguments. So, if you try to get all sites that have the specific contact email address:

>>> admins_sites = netbox.dcim.sites.filter(email="admin@example.com")
>>> admins_sites
[Customer site, Second testsite, Testsite]
>>>

You may be surprised with the results as the contact email is not correct match for Customer site. The problem is that “email” is not a recognized keyword or field name in NetBox, and thus the request effectively became “give me all objects with no filtering”. The correct field name is “contact_email”:

>>> real_admins_sites = netbox.dcim.sites.filter(contact_email="admin@example.com")
>>> real_admins_sites
[Second testsite, Testsite]
>>>

Finally, when using get(), be sure to check that you got what you intended:

>>> cust_site = netbox.dcim.sites.get(name="Customer sit")
>>> cust_site.name
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'name'
>>> type(cust_site)
<class 'NoneType'>
>>>

So you may want to use something like this:

>>> cust_site = netbox.dcim.sites.get(name="Customer sit")
>>> if cust_site:
...     print(cust_site.name)
... else:
...     print("Site not found")
...
Site not found
>>>

Deleting objects

Objects can be deleted as well. First we need to get the object we want to delete, and then we can call delete() on it:

>>> cust_site = netbox.dcim.sites.get(name="Customer site")
>>> cust_site
Customer site
>>> cust_site.delete()
True
>>>

Note that after you called delete() on the object the object is still there but it is not valid anymore:

>>> type(cust_site)
<class 'pynetbox.core.response.Record'>
>>> cust_site.name
'Customer site'
>>> cust_site.description = "The customer site"
>>> cust_site.save()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/response.py", line 397, in save
    if req.patch({i: serialized[i] for i in diff}):
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 407, in patch
    return self._make_call(verb="patch", data=data)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 274, in _make_call
    raise RequestError(req)
pynetbox.core.query.RequestError: The requested url: http://netbox-test.example.com/api/dcim/sites/5/ could not be found.
>>>

So, do not try to modify the objects in Python after deleting them in NetBox.

Handling object status

Let’s see the status of our Testsite:

>>> testsite = netbox.dcim.sites.get(name="Testsite")
>>> testsite.status
Active
>>> pprint(dict(testsite))
(...)
 'status': {'id': 1, 'label': 'Active', 'value': 'active'},
(...)
>>>

The human-friendly status (the label) is Active, but the actual status value is “active”. “id” is for legacy reasons, it was used in NetBox 2.6.x and earlier.

If you want to create a new site with status of Planned, you can do it:

>>> newsite = netbox.dcim.sites.create({"name": "New site", "slug": "new-site", "status": "planned"})
>>> newsite.status
Planned
>>>

Note that the status value is “planned”, not “Planned” (it will raise RequestError with “Planned is not a valid choice”).

If you want to list all possible values from API, there is a choices() method in pynetbox for each model:

>>> pprint(netbox.dcim.sites.choices())
{'status': [{'display_name': 'Active', 'value': 'active'},
            {'display_name': 'Planned', 'value': 'planned'},
            {'display_name': 'Retired', 'value': 'retired'}]}
>>> pprint(netbox.ipam.ip_addresses.choices())
{'role': [{'display_name': 'Loopback', 'value': 'loopback'},
          {'display_name': 'Secondary', 'value': 'secondary'},
          {'display_name': 'Anycast', 'value': 'anycast'},
          {'display_name': 'VIP', 'value': 'vip'},
          {'display_name': 'VRRP', 'value': 'vrrp'},
          {'display_name': 'HSRP', 'value': 'hsrp'},
          {'display_name': 'GLBP', 'value': 'glbp'},
          {'display_name': 'CARP', 'value': 'carp'}],
 'status': [{'display_name': 'Active', 'value': 'active'},
            {'display_name': 'Reserved', 'value': 'reserved'},
            {'display_name': 'Deprecated', 'value': 'deprecated'},
            {'display_name': 'DHCP', 'value': 'dhcp'}]}
>>>

The shown display_names are the human-friendly labels found in the objects.

As usual, changing the object status works as well:

>>> newsite.status = "active"
>>> newsite.save()
True
>>>

Using tags

Note: This section describes using tags with NetBox 2.8.x or earlier where tags were simply strings and they were automatically created when assigning to objects. NetBox 2.9.0 changed them: tags have to exist to be able to add them to objects, and they are now represented as objects in the lists. Thus, the code below does not work with NetBox 2.9.x or newer (except the filtering, that still works with the slugs). See my later post about using tags with NetBox 2.9+.

Tags can be used to add some specific information to objects. For example:

>>> newsite = netbox.dcim.sites.get(name="New site")
>>> newsite.tags
[]
>>> newsite.tags = ["Special", "To be completed"]
>>> newsite.save()
True
>>> newsite.tags
['Special', 'To be completed']
>>>

Tags field is a list of strings, and the can be manipulated with the usual Python list methods, like:

>>> newsite.tags.append("Low prio")
>>> newsite.tags.remove("Special")
>>> newsite.tags
['To be completed', 'Low prio']
>>> newsite.save()
True
>>>

Since the tags are strings they are always enclosed in quotes when handling them in Python, regardless of them containing one or multiple words.

Filtering objects using tags is tricky:

>>> low_prio_sites = netbox.dcim.sites.filter(tag="Low prio")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/endpoint.py", line 227, in filter
    ret = [self._response_loader(i) for i in req.get()]
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 350, in get
    return req_all()
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 303, in req_all
    req = self._make_call(add_params=add_params)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 274, in _make_call
    raise RequestError(req)
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'tag': ['Select a valid choice. Low prio is not one of the available choices.']}
>>>

Basically it says that tag “Low prio” is not valid.

The trick is that the filtering needs to be done with the slug of the tag. A tag’s slug is usually the same as the tag but all lower case and spaces replaced with dashes. Let’s try it:

>>> low_prio_sites = netbox.dcim.sites.filter(tag="low-prio")
>>> low_prio_sites
[New site]
>>>

That one is working correctly. To actually see the slug:

>>> pprint(dict(netbox.extras.tags.get(name="Low prio")))
{'color': '9e9e9e',
 'description': '',
 'id': 6,
 'name': 'Low prio',
 'slug': 'low-prio',
 'tagged_items': 1}
>>>

Earlier I mentioned about the caveat that incorrect filter() keyword arguments are ignored. With tags it is specially dangerous because:

>>> low_prio_sites = netbox.dcim.sites.filter(tags="low-prio")
>>> low_prio_sites
[New site, Second testsite, Testsite]
>>>

Do you see the problem? “Tags” is not the correct filtering argument even though the field name in the object is tags. The correct filtering keyword is “tag“, as shown a couple of lines earlier.

Referring to other objects

In our examples above we haven’t had any object relationships. Let’s expand on that. First, let’s create a tenant for our test sites.

>>> test_tenant = netbox.tenancy.tenants.create({"name": "Test Corp", "slug": "test-corp"})
>>> pprint(dict(test_tenant))
{'comments': '',
 'created': '2020-06-20',
 'custom_fields': {},
 'description': '',
 'group': None,
 'id': 2,
 'last_updated': '2020-06-20T19:25:48.049927+03:00',
 'name': 'Test Corp',
 'slug': 'test-corp',
 'tags': []}
>>>

Now we can edit our test sites to belong to that tenant:

>>> test_sites = netbox.dcim.sites.filter("testsite")
>>> test_sites
[Second testsite, Testsite]
>>> for site in test_sites:
...     site.tenant = test_tenant.id
...     site.save()
...
True
True
>>>

Here we first just checked quickly that the filtered list looks correct and then looped over the sites to modify them one by one.

The important detail here is that the tenant was assigned to the site by using the tenant ID. Let’s verify that the assignment went correctly:

>>> test_site = netbox.dcim.sites.get(name="Testsite")
>>> pprint(dict(test_site))
{'asn': None,
 'circuit_count': None,
 'comments': '',
 'contact_email': 'admin@example.com',
 'contact_name': 'Local Admin',
 'contact_phone': '',
 'created': '2020-06-20',
 'custom_fields': {},
 'description': 'This is a test site',
 'device_count': None,
 'facility': '',
 'id': 3,
 'last_updated': '2020-06-20T19:30:54.641205+03:00',
 'latitude': None,
 'longitude': None,
 'name': 'Testsite',
 'physical_address': '',
 'prefix_count': None,
 'rack_count': None,
 'region': None,
 'shipping_address': '',
 'slug': 'testsite',
 'status': {'id': 1, 'label': 'Active', 'value': 'active'},
 'tags': [],
 'tenant': {'id': 2,
            'name': 'Test Corp',
            'slug': 'test-corp',
            'url': 'http://netbox-test.example.com/api/tenancy/tenants/2/'},
 'time_zone': None,
 'virtualmachine_count': None,
 'vlan_count': None}
>>>

Here we can see the correct tenant details.

The tenant can also be assigned by giving detailed enough information instead of the tenant ID. Let’s create a new site for that tenant by using the tenant name:

>>> third_site = netbox.dcim.sites.create({"name": "Third testsite", "slug": "third-testsite", "tenant": {"name": "Test Corp"}})
>>> third_site.tenant.name
'Test Corp'
>>>

So instead of tenant ID we used a dict that described the correct tenant. Note that the tenant must still exist, it will not be created on the fly:

>>> faulty_site = netbox.dcim.sites.create({"name": "Faulty testsite", "slug": "faulty-testsite", "tenant": {"name": "Faulty Corp"}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/endpoint.py", line 287, in create
    ).post(args[0] if args else kwargs)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 381, in post
    return self._make_call(verb="post", data=data)
  File "/home/markku/pynetboxdemo/venv/lib/python3.7/site-packages/pynetbox/core/query.py", line 274, in _make_call
    raise RequestError(req)
pynetbox.core.query.RequestError: The request failed with code 400 Bad Request: {'tenant': ["Related object not found using the provided attributes: {'name': 'Faulty Corp'}"]}
>>>

Leave a Reply