Pynetbox API calls using variables

using variables for netbox api urls and filters

2 March 2022   4 min read

Recently whilst using pynetbox to create NetBox environment objects I had a need to use variables in the URL of the API calls to allow for reusable functions to perform API calls based on the URL and data fed in as arguments. The reason the URL needs to be fed in as an argument when calling the function is because each NetBox element uses a different API URL.

For all for these examples I am using the NetBox docker image with the default API token, pynetbox first needs to imported and the NetBox connection initialized.

import pynetbox
netbox_url = "http://10.10.10.104:8000"
token = "0123456789abcdef0123456789abcdef01234567"
nb = pynetbox.api(url=netbox_url, token=token)

Using a variable for the API call

The following will create a new VLAN using the standard API URL (VLAN group mandatory dictionary can be None as it supports x-nullable: true).

new_vlan = dict(vid=400, name="DATA_VL400", group=None)
nb.ipam.vlans.create(new_vlan)

If you tried this using a variable for the URL (ipam.vlans) it will return an AttributeError as it tries to use the variable name (api_attr) rather than transposing the variable value.

api_attr = "ipam.vlans"
new_vlan = dict(vid=401, name="DATA_VL401", group=None)
nb.api_attr.create(new_vlan)

AttributeError: 'Api' object has no attribute 'api_attr'

To get round this use operator.attrgetter where the variable is first called (operator.attrgetter()) and joined to the API call (nb) with the action then added on afterwards (.create()).

import operator

api_attr = "ipam.vlans"
new_vlan = dict(vid=401, name="DATA_VL401", group=None)
operator.attrgetter(api_attr)(nb).create(new_vlan)

Using this methodology in a function we can create any type of NetBox object by feeding in the pynetbox connection (nb), API URL (api_attr), data-model (obj_notexist_dm) and friendly name used for stdout messages (output_name).

def obj_create(nb, output_name, api_attr, obj_notexist_dm) -> None:
    try:
        result = operator.attrgetter(api_attr)(nb).create(obj_notexist_dm)
        print(f"✅ {output_name}: '{str(result).replace('[', '').replace(']', '')}' successfully created")
    except RequestError as e:
        err_msg = ast.literal_eval(e.error)
        for err in err_msg:
            if len(err) != 0:
                print(f"❌ {output_name} '{list(err.keys())[0]}' - {', '.join(list(err.values())[0])}")

The below example calls the function to create a new VLAN and prefix.

new_vlan = dict(vid=402, name="DATA_VL402", group=None)
obj_create(nb, "VLAN", "ipam.vlans", new_vlan)
new_prefix = dict(prefix="192.168.168.0/24")
obj_create(nb, "Prefix", "ipam.prefixes", new_prefix)

 VLAN: 'DATA_VL402' successfully created
 Prefix: '192.168.168.0/24' successfully created

Using a variable for get() or filter() dictionary key

Another issue I came across when using pynetbox was that a variable cannot be used as the dictionary key to match on in the get and filter methods. In a similar manner to creating objects I wanted to use the same one function to gather the different NetBox object attributes.

The following will get a specific VLANs attributes based on the VLAN name, to printout the objects attributes it must be converted into a dictionary.

print(dict(nb.ipam.vlans.get(name="DATA_VL402")))
{'id': 13, 'url': 'http://10.10.10.104:8000/api/ipam/vlans/13/', 'display': 'DATA_VL402 (402)', 'site': None, 'group': None, 'vid': 402, 'name': 'DATA_VL402', 'tenant': None, 'status': {'value': 'active', 'label': 'Active'}, 'role': None, 'description': '', 'tags': [], 'custom_fields': {}, 'created': '2022-03-02', 'last_updated': '2022-03-02T14:49:32.256993Z', 'prefix_count': 0}

Like with API calls when using a variable it tries to use the variable name and therefore fails with a ValueError as it did not match anything meaning pynetbox will return all VLAN objects (get can only have one).

obj_fltr = 'name'
print(dict(nb.ipam.vlans.get(obj_fltr="DATA_VL402")))

ValueError: get() returned more than one result. Check that the kwarg(s) passed are valid for this endpoint or use filter() or all() instead.

The workaround for this is to use dictionary unpacking (**), you could even use multiple variables to match on multiple attributes if required.

obj_fltr = 'name'
print(dict(nb.ipam.vlans.get(**{obj_fltr: "DATA_VL402"})))
{'id': 13, 'url': 'http://10.10.10.104:8000/api/ipam/vlans/13/', 'display': 'DATA_VL402 (402)', 'site': None, 'group': None, 'vid': 402, 'name': 'DATA_VL402', 'tenant': None, 'status': {'value': 'active', 'label': 'Active'}, 'role': None, 'description': '', 'tags': [], 'custom_fields': {}, 'created': '2022-03-02', 'last_updated': '2022-03-02T14:49:32.256993Z', 'prefix_count': 0}

Using this methodology in a function we can create a list of existing and non-existing NetBox objects by feeding in the pynetbox connection (nb), API URL (api_attr), the dictionary key to match on (obj_fltr) and a list of data-models (obj_dm).

def obj_check(nb, api_attr, obj_fltr, obj):
    obj_notexist, obj_exist = ([] for i in range(2))
    for each_obj in obj:
        if operator.attrgetter(api_attr)(nb).get(**{obj_fltr: each_obj}) == None:
            obj_notexist.append(each_obj)
        else:
            obj_exist.append(each_obj)
    return dict(notexist=obj_notexist, exist=obj_exist)

The below example calls the function to create lists of VLANs and prefixes that already exist or don’t exist within NetBox (they use different filters of name and prefix respectively).

vlans_dm = ["DATA_VL402", "VOICE_VL403"]
print(obj_check(nb, "ipam.vlans", "name", vlans_dm))
prefixes_dm = ["192.168.168.0/24", "192.168.178.0/24"]
print(obj_check(nb, "ipam.prefixes", "prefix", prefixes_dm))

{'notexist': ['VOICE_VL403'], 'exist': ['DATA_VL402']}
{'notexist': ['192.168.178.0/24'], 'exist': ['192.168.168.0/24']}

In the same manner can use filter rather than get to return a list of all matching NetBox objects rather than getting a specific one, must convert pynetbox object to a list to view them. Can drill down further to see object attributes by looping through it or specifying the list element number and converting the object to a dictionary.

api_attr = "ipam.vlans"
obj_fltr = 'name'

print(operator.attrgetter(api_attr)(nb).filter(**{obj_fltr: "DATA_VL402"}))
<pynetbox.core.response.RecordSet object at 0x1121a1a90>

print(list(operator.attrgetter(api_attr)(nb).filter(**{obj_fltr: "DATA_VL402"})))
[DATA_VL402]

print(dict(list(operator.attrgetter(api_attr)(nb).filter(**{obj_fltr: "DATA_VL402"}))[0]))
{'id': 13, 'url': 'http://10.10.10.104:8000/api/ipam/vlans/13/', 'display': 'DATA_VL402 (402)', 'site': None, 'group': None, 'vid': 402, 'name': 'DATA_VL402', 'tenant': None, 'status': {'value': 'active', 'label': 'Active'}, 'role': None, 'description': '', 'tags': [], 'custom_fields': {}, 'created': '2022-03-02', 'last_updated': '2022-03-02T14:49:32.256993Z', 'prefix_count': 0}