If you’ve been watching this space, you’ve seen me talking about Nautobot’s GraphQL capabilities and how GraphQL helps you:
This post is a case study in those aspects. It will empirically demonstrate how GraphQL:
I will be running this case study using https://demo.nautobot.com/. Readers are encouraged to follow along, using the scripts below.
In this case study, the goal is to gather specific information for certain network elements from Nautobot. Specifically, we want a data structure with the following information:
The GraphQL solution will leverage the pynautobot
Python package, which provides a customized and efficient way to programmatically query Nautobot via GraphQL.
Also, from an earlier blog post in this GraphQL series, recall that you can craft GraphQL queries in Nautobot’s GraphiQL interface.
Here is the script we will use to accomplish our task using GraphQL:
import pynautobot
from pprint import pprint
from time import time
start_time = time()
# Nautobot URL and auth info
url = "https://demo.nautobot.com"
token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
# GraphQL query
query = """
query {
devices(site:"ams") {
name
device_role {
name
}
interfaces {
name
ip_addresses {
address
}
}
}
}
"""
print("Querying Nautobot via pynautobot.")
print()
print("url is: {}".format(url))
print()
print("query is:")
print(query)
print()
nb = pynautobot.api(url, token)
response = nb.graphql.query(query=query)
response_data = response.json
print("Here is the response data in json:")
pprint(response_data)
print()
end_time = time()
run_time = end_time - start_time
print("Run time = {}".format(run_time))
Here are the results when the script runs (snipped in places for brevity):
blogs/graphql_vs_restful % python3 -i graphql_query_ams_device_ints_pynautobot.py
Querying Nautobot via pynautobot.
url is: https://demo.nautobot.com
query is:
query {
devices(site:"ams") {
name
device_role {
name
}
interfaces {
name
ip_addresses {
address
}
}
}
}
Here is the response data in json:
{'data': {'devices': [{'device_role': {'name': 'edge'},
'interfaces': [{'ip_addresses': [{'address': '10.11.192.0/32'}],
'name': 'Ethernet1/1'},
{'ip_addresses': [{'address': '10.11.192.2/32'}],
'name': 'Ethernet2/1'},
{'ip_addresses': [{'address': '10.11.192.4/32'}],
'name': 'Ethernet3/1'},
{'ip_addresses': [{'address': '10.11.192.8/32'}],
'name': 'Ethernet4/1'},
< --- snip for brevity --- >
{'ip_addresses': [],
'name': 'Ethernet60/1'},
{'ip_addresses': [{'address': '10.11.128.1/32'}],
'name': 'Loopback0'},
{'ip_addresses': [],
'name': 'Management1'}],
'name': 'ams-edge-01'},
{'device_role': {'name': 'edge'},
'interfaces': [{'ip_addresses': [{'address': '10.11.192.1/32'}],
'name': 'Ethernet1/1'},
< --- snip for brevity --- >
{'ip_addresses': [{'address': '10.11.128.2/32'}],
'name': 'Loopback0'},
{'ip_addresses': [],
'name': 'Management1'}],
'name': 'ams-edge-02'},
{'device_role': {'name': 'leaf'},
'interfaces': [{'ip_addresses': [{'address': '10.11.192.5/32'}],
'name': 'Ethernet1'},
{'ip_addresses': [], 'name': 'Ethernet3'},
< --- snip for brevity --- >
{'ip_addresses': [{'address': '10.11.64.0/32'}],
'name': 'vlan99'},
{'ip_addresses': [{'address': '10.11.0.0/32'}],
'name': 'vlan1000'}],
'name': 'ams-leaf-01'},
{'device_role': {'name': 'leaf'},
'interfaces': [{'ip_addresses': [{'address': '10.11.192.9/32'}],
'name': 'Ethernet1'},
{'ip_addresses': [{'address': '10.11.192.11/32'}],
'name': 'Ethernet2'},
{'ip_addresses': [], 'name': 'Ethernet3'},
< --- snip for brevity --- >
{'ip_addresses': [], 'name': 'Management1'},
{'ip_addresses': [{'address': '10.11.1.0/32'}],
'name': 'vlan1000'}],
< --- snip for brevity --- >
{'device_role': {'name': 'leaf'},
'interfaces': [{'ip_addresses': [{'address': '10.11.192.33/32'}],
'name': 'Ethernet1'},
< --- snip for brevity --- >
{'ip_addresses': [{'address': '10.11.7.0/32'}],
'name': 'vlan1000'}],
'name': 'ams-leaf-08'}]}}
Run time = 1.8981318473815918
>>>
Take specific note of the following GraphQL features demonstrated above:
Running the script six times produced an average of 2.23 seconds, returning data for ten devices in the ams
site.
For the RESTful solution, we’re not concerned about matching the exact data structure returned by GraphQL. We’re only concerned with getting the same data into a structure that can be parsed for programmatic use.
The GraphQL results were grouped by device, and the RESTful solution will do that as well, but will have some small format changes.
Here is the format for the data structure that the RESTful solution will return:
{
<device_1_name>: {
'role': <device_1_role>,
'interface_info': {
<interface_1_name>: [list of ip addresses for interface_1],
<interface_2_name>: [list of ip addresses for interface_2],
. . .
}
}
. . .
<device_n_name>: {
'role': <device_n_role>,
'interface_info': {
<interface_1_name>: [list of ip addresses for interface_1],
<interface_2_name>: [list of ip addresses for interface_2],
. . .
}
}
}
The format above is slightly different than that of the GraphQL results, but is still programmatically parsable.
The RESTful script that returns the data is below. When examining it, take note of the following:
ams
site returns so much extraneous information, Nautobot applies the default limit
of 50 results per call
limit
constraint reduces the load on the Nautobot serverams
site"""
Use REST API calls to get the following info for each device in 'ams' site:
- device name
- device role
- interface info
- interface name
- ip address
"""
import json
import requests
from pprint import pprint
from time import time
start_time = time()
# Looking at the Nautobot API:
# - /api/dcim/devices gives you name and role
# - /api/dcim/interfaces gives you all the interface info
# - /api/addresses gets IP address info
# Define general request components
payload = {}
headers = {
"Content-Type": "application/json",
"Authorization": "Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
}
##########################################
# Define devices url, query for 'ams' site devices
ams_dev_url = "https://demo.nautobot.com/api/dcim/devices?site=ams"
# Query Nautobot for the ams devices
ams_dev_resp = requests.get(ams_dev_url, headers=headers, data=payload)
# Turn the response text string into json
ams_devices_json = ams_dev_resp.json()
# Device info dict
device_info = {}
# Create a dict with device names as keys; the value for each key will be a dict.
for device in ams_devices_json["results"]:
role = device['device_role']['display']
dev_name = device["name"]
device_info[dev_name] = {
'role': role,
'interface_info': {},
}
print("device_info is:")
pprint(device_info)
print()
print()
##########################################
print("The GraphQL query returned all interfaces for a device, regardless of whether ")
print("an ip address was configured; we will match that here.")
print()
print("Gathering interface info for `ams` site.")
# Define url for device interfaces in 'ams' site
ams_interface_url = "https://demo.nautobot.com/api/dcim/interfaces?site=ams"
# Define a list to hold the interface info for `ams` site
ams_interface_info = []
# Account for ams_interface_url results limit; iterate url until 'next' url is None
while ams_interface_url is not None:
ams_interface_resp = requests.get(ams_interface_url, headers=headers, data=payload)
ams_interface_json = ams_interface_resp.json()
ams_interface_url = ams_interface_json["next"]
print("ams_interface_url is {}".format(ams_interface_url))
ams_interface_info.extend(ams_interface_json["results"])
print()
print("Adding interface names to device_info for the appropriate device.")
# Filter out the interface names and add them in device_info
for interface_entry in ams_interface_info:
dev_name = interface_entry["device"]["name"]
interface_name = interface_entry["name"]
device_info[dev_name]['interface_info'][interface_name] = []
print()
#####################################
print("Finally, gather the IP address info for each interface.")
print("This RESTful call returns only interfaces that have IP addresses configured.")
print()
ip_info_list = []
for device in device_info.keys():
ip_url = "https://demo.nautobot.com/api/ipam/ip-addresses?device={}".format(device)
# Account for ip_url results limit; iterate url until 'next' url is None
while ip_url is not None:
print("ip_url = {}".format(ip_url))
ip_url_response = requests.get(ip_url, headers=headers, data=payload)
ip_json = ip_url_response.json()
ip_url = ip_json["next"]
ip_info_list.extend(ip_json["results"])
print()
print("Add the IP address info to device_info.")
print()
for item in ip_info_list:
device = item["assigned_object"]["device"]["name"]
interface = item["assigned_object"]["name"]
address = item["address"]
device_info[device]['interface_info'][interface].append(address)
print("Here is the completed data structure:")
pprint(device_info)
print()
end_time = time()
run_time = end_time - start_time
print("Run time = {}".format(run_time))
Here are the results of the RESTful script:
blogs/graphql_vs_restful % python3 -i restful_api_query_ams_device_ints.py
device_info is:
{'ams-edge-01': {'interface_info': {}, 'role': 'edge'},
'ams-edge-02': {'interface_info': {}, 'role': 'edge'},
'ams-leaf-01': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-02': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-03': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-04': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-05': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-06': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-07': {'interface_info': {}, 'role': 'leaf'},
'ams-leaf-08': {'interface_info': {}, 'role': 'leaf'}}
The GraphQL query returned all interfaces for a device, regardless of whether
an ip address was configured; we will match that here.
Gathering interface info for `ams` site.
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=50&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=100&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=150&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=200&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=250&site=ams
ams_interface_url is https://demo.nautobot.com/api/dcim/interfaces/?limit=50&offset=300&site=ams
ams_interface_url is None
Adding interface names to device_info for the appropriate device.
Finally, gather the IP address info for each interface.
This RESTful call returns only interfaces that have IP addresses configured.
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-edge-01
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-edge-02
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-01
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-02
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-03
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-04
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-05
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-06
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-07
ip_url = https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-leaf-08
Add the IP address info to device_info.
Here is the completed data structure:
{'ams-edge-01': {'interface_info': {'Ethernet1/1': ['10.11.192.0/32'],
'Ethernet10/1': ['10.11.192.32/32'],
'Ethernet11/1': [],
'Ethernet12/1': [],
< --- snip for brevity --- >
'Ethernet9/1': ['10.11.192.28/32'],
'Loopback0': ['10.11.128.1/32'],
'Management1': []},
'role': 'edge'},
'ams-edge-02': {'interface_info': {'Ethernet1/1': ['10.11.192.1/32'],
'Ethernet10/1': ['10.11.192.34/32'],
< --- snip for brevity --- >
'Loopback0': ['10.11.128.2/32'],
'Management1': []},
'role': 'edge'},
'ams-leaf-01': {'interface_info': {'Ethernet1': ['10.11.192.5/32'],
< --- snip for brevity --- >
'vlan99': ['10.11.64.0/32']},
'role': 'leaf'},
< --- some devices snipped for brevity --- >
'ams-leaf-07': {'interface_info': {'Ethernet1': ['10.11.192.29/32'],
'Ethernet10': [],
< --- snip for brevity --- >
'vlan99': ['10.11.70.0/32']},
'role': 'leaf'},
'ams-leaf-08': {'interface_info': {'Ethernet1': ['10.11.192.33/32'],
< --- snip for brevity --- >
'vlan99': ['10.11.71.0/32']},
'role': 'leaf'}}
Run time = 13.60936713218689
>>>
Running the script six times produced an average run time of 14.9 seconds.
This script created a data structure that is not identical to the structure created by GraphQL, but is similar in nature and is still parsable.
Method | Average Run Time | # of Queries | Time to Create Script |
---|---|---|---|
GraphQL | 2.2 seconds | 1 | ~ 20 minutes |
RESTful | 14.9 seconds | 17 | ~ 200 minutes+ |
NOTE: These results are based on the baseline data in the Nautobot demo sandbox. If someone has modified the database, your actual results may vary a bit.
By any measure, GraphQL is the clear choice here! GraphQL allows a much simpler script that is much more efficient than REST.
Imagine your automation task being able to run an average of 12.2 seconds faster (14.9 – 2.2 seconds) by using GraphQL.
I also don’t want to undersell the amount of time and headache required to create the RESTful script, including parsing the REST data and crafting the data structure: it was not pleasant, and we should not talk about it again. Ever.
Querying with GraphQL results in much less coding and post-processing for the user and is generally much more efficient than RESTful calls that achieve the same result.
However, the load on the Nautobot server must still be considered. Depending on the data you are after and your use case, it may make sense to:
Depending on how many sites and devices you have, the example query below may put undue load on the Nautobot server:
query {
devices {
name
device_role {
name
}
interfaces {
name
ip_addresses {
address
}
}
}
}
This script could cause a lot of undue load on the server because it is not targeted to a site or group of sites.
To ease the load, you could instead do the following:
1. Make a GraphQL query to return all the site names
query {
sites {
name
}
}
2. Make additional GraphQL queries to programmatically iterate over each site name within the query parameter devices(site:"<site_name>")
:
query {
devices(site:"ams") {
name
device_role {
name
}
interfaces {
name
ip_addresses {
address
}
}
}
}
query {
devices(site:"bkk") {
name
device_role {
name
}
interfaces {
name
ip_addresses {
address
}
}
}
}
et cetera . . .
This case study validates the clear advantages GraphQL offers: simpler and faster automation, higher query efficiency, less time swimming in extraneous data, and thus less time coding. The great part is that Nautobot delivers GraphQL capabilities that you can leverage right now. Please do give it a try.
If you have questions, you can check out these Network to Code resources for more info:
You can also hit us up on the #nautobot channel on NTC Slack.
Thank you, and have a great day!
-Tim
Share details about yourself & someone from our team will reach out to you ASAP!