Using the Python Requests Module to Work with REST APIs

Blog Detail

In this post we’ll review one of the most widely used Python modules for interacting with web-based services such as REST APIs, the Python requests module. If you were ever wondering what magic is going on behind the scenes when running one of the thousands of Ansible networking modules, or many of the Python-based SDKs that are available from vendors, there’s a good chance the underlying operations are being performed by requests. The Python requests module is a utility that emulates the operations of a web browser using code. It enables programs to interact with a web-based service across the network, while abstracting and handling the lower-level details of opening up a TCP connection to the remote system. Like a web browser, the requests module allows you to programmatically:

  • Initiate HTTP requests such as GET, PUT, POST, PATCH, and DELETE
  • Set HTTP headers to be used in the outgoing request
  • Store and access the web server content in various forms (HTML, XML, JSON, etc.)
  • Store and access cookies
  • Utilize either HTTP or HTTPS

Retrieving Data

The most basic example of using requests is simply retrieving the contents of a web page using an HTTP GET:

import requests
response = requests.get('https://google.com')

The resulting response object will contain the actual HTML code that would be seen by a browser in the text object, which can be accessed by typing response.text.

<span role="button" tabindex="0" data-code=">>> response.text '<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head><meta content="Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for." name="description"><meta content="noodp" name="robots">
>>> response.text
'<!doctype html><html itemscope="" itemtype="http://schema.org/WebPage" lang="en"><head><meta content="Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for." name="description"><meta content="noodp" name="robots"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
-- output omitted for brevity --

There are a lot of great utilities for parsing HTML, but in most cases we will not be doing that when working with networking vendor APIs. In the majority of cases, the data will come back structured as XML or JSON.

response = requests.get('https://nautobot.demo.networktocode.com/api')

>>> response.content
b'{"circuits":"https://nautobot.demo.networktocode.com/api/circuits/","dcim":"https://nautobot.demo.networktocode.com/api/dcim/","extras":"https://nautobot.demo.networktocode.com/api/extras/","graphql":"https://nautobot.demo.networktocode.com/api/graphql/","ipam":"https://nautobot.demo.networktocode.com/api/ipam/","plugins":"https://nautobot.demo.networktocode.com/api/plugins/","status":"https://nautobot.demo.networktocode.com/api/status/","tenancy":"https://nautobot.demo.networktocode.com/api/tenancy/","users":"https://nautobot.demo.networktocode.com/api/users/","virtualization":"https://nautobot.demo.networktocode.com/api/virtualization/"}'

Notice that the above output is in bytes format. This is indicated by the lowercase “b” in front of the response text. We could convert this into a string using response.content.decode() and then use the Python json module to load it into a Python dictionary. However, because json is one of the most common data formats, the requests module has a convenience method that will automatically convert the response from bytes to a Python dictionary. Simply call response.json():

<span role="button" tabindex="0" data-code=">>> response.json() {'circuits': 'https://nautobot.demo.networktocode.com/api/circuits/', 'dcim': 'https://nautobot.demo.networktocode.com/api/dcim/', 'extras': 'https://nautobot.demo.networktocode.com/api/extras/', 'graphql': 'https://nautobot.demo.networktocode.com/api/graphql/', 'ipam': 'https://nautobot.demo.networktocode.com/api/ipam/', 'plugins': 'https://nautobot.demo.networktocode.com/api/plugins/', 'status': 'https://nautobot.demo.networktocode.com/api/status/', 'tenancy': 'https://nautobot.demo.networktocode.com/api/tenancy/', 'users': 'https://nautobot.demo.networktocode.com/api/users/', 'virtualization': 'https://nautobot.demo.networktocode.com/api/virtualization/'} >>> type(response.json())
>>> response.json()
{'circuits': 'https://nautobot.demo.networktocode.com/api/circuits/', 'dcim': 'https://nautobot.demo.networktocode.com/api/dcim/', 'extras': 'https://nautobot.demo.networktocode.com/api/extras/', 'graphql': 'https://nautobot.demo.networktocode.com/api/graphql/', 'ipam': 'https://nautobot.demo.networktocode.com/api/ipam/', 'plugins': 'https://nautobot.demo.networktocode.com/api/plugins/', 'status': 'https://nautobot.demo.networktocode.com/api/status/', 'tenancy': 'https://nautobot.demo.networktocode.com/api/tenancy/', 'users': 'https://nautobot.demo.networktocode.com/api/users/', 'virtualization': 'https://nautobot.demo.networktocode.com/api/virtualization/'}

>>> type(response.json())
<class 'dict'>

In some cases, we will have to specify the desired data format by setting the Accept header. For example:

headers = {'Accept': 'application/json'}
response = requests.get('https://nautobot.demo.networktocode.com/api', headers=headers)

In this example, we are informing the API that we would like the data to come back formatted as JSON. If the API provides the content as XML, we would specify the header as {'Accept': 'application/xml'}. The appropriate content type to request should be spelled out in the vendor API documentation. Many APIs use a default, so you may not need to specify the header. Nautobot happens to use a default of application/json, so it isn’t necessary to set the header. If you do not set the Accept header, you can find out the type of returned content by examining the Content-Type header in the response:

>>> response.headers['Content-Type']
'application/json'

Although we are using Nautobot for many of the examples of requests module usage, there is a very useful SDK called pynautobot that can handle a lot of the heavy lifting for you, so definitely check that out!

Authentication

Most APIs are protected by an authentication mechanism which can vary from product to product. The API documentation is your best resource in determining the method of authentication in use. We’ll review a few of the more common methods with examples below.

API Key

With API key authentication you typically must first access an administrative portal and generate an API key. Think of the API key the same way as you would your administrative userid/password. In some cases it will provide read/write administrative access to the entire system, so you want to protect it as such. This means don’t store it in the code or in a git repository where it can be seen in clear text. Commonly the API keys are stored as environment variables and imported at run time, or are imported from password vaults such as Hashicorp or Ansible vault. Once an API key is generated, it will need to be included in some way with all requests. Next we’ll describe a few common methods for including the API key in requests and provide example code.

Token in Authorization Header

One method that is used across a wide variety of APIs is to include the API key as a token in the Authorization header. A few examples of this are in the authentication methods for Nautobot and Cisco Webex. The two examples below are very similar, with the main difference being that Nautobot uses Token {token} in the Authorization header whereas Cisco Webex uses Bearer {token} in the Authorization header. Implementation of this is not standardized, so the API documentation should indicate what the format of the header should be.

Nautobot API

First, it is necessary to generate an API key from the Nautobot GUI. Sign into Nautobot and select your username in the upper right-hand corner, and then view your Profile. From the Profile view, select API Tokens and click the button to add a token. The token will then need to be specified in the Authorization header in all requests as shown below.

import requests
import os

# Get the API token from an environment variable
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# This is the base URL for all Nautobot API calls
base_url = 'https://nautobot.demo.networktocode.com/api'

# Get the list of devices from Nautobot using the requests module and passing in the authorization header defined above
response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/devices/', headers=headers)

>>> response.json()
{'count': 511, 'next': 'https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=50', 'previous': None, 'results': [{'id': 'fd94038c-f09f-4389-a51b-ffa03e798676', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/devices/fd94038c-f09f-4389-a51b-ffa03e798676/', 'name': 'ams01-edge-01', 'device_type': {'id': '774f7008-3a75-46a2-bc75-542205574cee', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/device-types/774f7008-3a75-46a2-bc75-542205574cee/', 'manufacturer': {'id': 'e83e2d58-73e2-468b-8a86-0530dbf3dff9', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/manufacturers/e83e2d58-73e2-468b-8a86-0530dbf3dff9/', 'name': 'Arista', 'slug': 'arista', 'display': 'Arista'}, 'model': 'DCS-7280CR2-60', 'slug': 'dcs-7280cr2-60', 'display': 'Arista DCS-7280CR2-60'}, 'device_role': {'id': 'bea7cc02-e254-4b7d-b871-6438d1aacb76', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/device-roles/bea7cc02-e254-4b7d-b871-6438d1aacb76/'
--- OUTPUT TRUNCATED FOR BREVITY ---

Cisco Webex API

When working with the Webex API, a bot must be created to get an API key. First create a bot in the dashboard https://developer.webex.com/docs/. Upon creating the bot you are provided a token which is good for 100 years. The token should then be included in the Authorization header in all requests as shown below.

import requests
import os

# Get the API token from an environment variable
token = os.environ.get('WEBEX_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Bearer {token}'}

# This is the base URL for all Webex API calls
base_url = 'https://webexapis.com'

# Get list of rooms
response = requests.get(f'{base_url}/v1/rooms', headers=headers)

>>> response.json()
{'items': [{'id': 'Y2lzY29zcGFyazovL3VzL1JPT00vNjZlNmZjYTAtMjIxZS0xMWVjLTg2Y2YtMzk0NmQ2YTMzOWVi', 'title': 'nautobot-chatops', 'type': 'group', 'isLocked': False, 'lastActivity': '2021-10-22T19:37:38.091Z', 'creatorId': 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9iYmRiZDljNC1hMTRkLTQwMTYtYjVjZi1jOGExNzY0MWI1YWQ', 'created': '2021-09-30T18:44:11.242Z', 'ownerId': 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8zZjE3OTcwNi1mMTFhLTRhYjctYmEzZS01N2E0YTk2YjA4OWY'}, {'id': 'Y2lzY29zcGFyazovL3VzL1JPT00vNzBjZTgwYTAtMjIxMi0xMWVjLWEwMDAtZjcyZTAyM2Q2MDIx', 'title': 'Webex space for Matt', 'type': 'group', 'isLocked': False, 'lastActivity': '2021-09-30T17:18:33.898Z', 'creatorId': 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9iYmRiZDljNC1hMTRkLTQwMTYtYjVjZi1jOGExNzY0MWI1YWQ', 'created': '2021-09-30T17:18:33.898Z', 'ownerId': 'Y2lzY29zcGFyazovL3VzL09SR0FOSVpBVElPTi8zZjE3OTcwNi1mMTFhLTRhYjctYmEzZS01N2E0YTk2YjA4OWY'}, {'id': 'Y2lzY29zcGFyazovL3VzL1JPT00vOWIwN2FmMjYtYmQ4Ny0zYmYwLWI2YzQtNTdlNmY1OGQwN2E2', 'title': 'Jason Belk', 'type': 'direct', 'isLocked': False, 'lastActivity': '2021-01-26T19:53:01.306Z', 'creatorId': 'Y2lzY29zcGFyazovL3VzL1BFT1BMRS9jNzg2YjVmOC1hZTdjLTQyMzItYjRiNS1jNzQxYTU3MjU4MzQ', 'created': '2020-12-10T17:53:01.202Z'}, {'id': 'Y2lzY29zcGFyazovL3VzL1JPT00vNTYwNzhhNTAtMTNjMi0xMWViLWJiNjctMTNiODIxYWUyMjE1', 'title': 'NTC NSO Projects', 'type': 'group', 'isLocked': False, 'lastActivity': '2021-05-28T17:46:16.727Z', 'creatorId': 'Y2lzY29zcGFyazovL3VzL1BFT1BMR
--- OUTPUT TRUNCATED FOR BREVITY ---

Custom Token Header

Some APIs require that the API key be provided in a custom header that is included with all requests. The key and the format to use for the value should be spelled out in the API documentation.

Cisco Meraki

Cisco Meraki requires that all requests have an X-Cisco-Meraki-API-Key header with the API key as the value. As with the Token in Authorization Header method discussed previously, you must first go to the API dashboard and generate an API key. This is done in the Meraki Dashboard under your profile settings. The key should then be specified in the X-Cisco-Meraki-API-Key for all requests.

import requests
import os

# Get the API key from an environment variable
api_key = os.environment.get('MERAKI_API_KEY')

# The base URI for all requests
base_uri = "https://api.meraki.com/api/v0"

# Set the custom header to include the API key
headers = {'X-Cisco-Meraki-API-Key': api_key}

# Get a list of organizations
response = requests.get(f'{base_uri}/organizations', headers=headers)

>>> response.json()
[{'id': '681155', 'name': 'DeLab', 'url': 'https://n392.meraki.com/o/49Gm_c/manage/organization/overview'}, {'id': '575334852396583536', 'name': 'TNF - The Network Factory', 'url': 'https://n22.meraki.com/o/K5Faybw/manage/organization/overview'}, {'id': '573083052582914605', 'name': 'Jacks_test_net', 'url': 'https://n18.meraki.com/o/22Uqhas/manage/organization/overview'}, {'id': '549236', 'name': 'DevNet Sandbox', 'url': 'https://n149.meraki.com/o/-t35Mb/manage/organization/overview'}, {'id': '575334852396583264', 'name': 'My organization', 'url': 'https://n22.meraki.com/o/
--- OUTPUT TRUNCATED FOR BREVITY ---

HTTP Basic Authentication w/ Token

Some APIs require that you first issue an HTTP POST to a login url using HTTP Basic Authentication. A token that must be used on subsequent requests is then issued in the response. This type of authentication does not require going to an administrative portal first to generate the token; the token is automatically generated upon successful login.

HTTP Basic Authentication/Token – Cisco DNA Center

The Cisco DNA Center login process requires that a request first be sent to a login URL with HTTP Basic Authentication, and upon successful authentication issues a token in the response. The token must then be sent in an X-Auth-Token header in subsequent requests.

import requests
from requests.auth import HTTPBasicAuth
import os

username = os.environ.get('DNA_USERNAME')
password = os.environ.get('DNA_PASSWORD')

hostname = 'sandboxdnac2.cisco.com'

# Create an HTTPBasicAuth object that will be passed to requests
auth = HTTPBasicAuth(username, password)

# Define the login URL to get the token
login_url = f"https://{hostname}/dna/system/api/v1/auth/token"

# Issue a login request
response = requests.post(login_url, auth=auth)

# Parse the token from the response if the response was OK 
if response.ok:
    token = response.json()['Token']
else:
    print(f'HTTP Error {response.status_code}:{response.reason} occurred')

# Define the X-Auth-Token header to be used in subsequent requests
headers = {'X-Auth-Token': token}

# Define the url for getting network health information from DNA Center
url = f"https://{hostname}/dna/intent/api/v1/network-health"

# Retrieve network health information from DNA Center
response = requests.get(url, headers=headers, auth=auth)

>>> response.json()
{'version': '1.0', 'response': [{'time': '2021-10-22T19:40:00.000+0000', 'healthScore': 100, 'totalCount': 14, 'goodCount': 14, 'unmonCount': 0, 'fairCount': 0, 'badCount': 0, 'entity': None, 'timeinMillis': 1634931600000}], 'measuredBy': 'global', 'latestMeasuredByEntity': None, 'latestHealthScore': 100, 'monitoredDevices': 14, 'monitoredHealthyDevices': 14, 'monitoredUnHealthyDevices': 0, 'unMonitoredDevices': 0, 'healthDistirubution': [{'category': 'Access', 'totalCount': 2, 'healthScore': 100, 'goodPercentage': 100, 'badPercentage': 0, 'fairPercentage': 0, 'unmonPercentage': 0, 'goodCount': 2, 'badCount': 0, 'fairCount': 0, 'unmonCount': 0}, {'category': 'Distribution', 'totalCount': 1, 'healthScore': 100, 'good
--- OUTPUT TRUNCATED FOR BREVITY ---

POST with JSON Payload

With this method of authentication, the user must first issue a POST to a login URL and include a JSON (most common), XML, or other type of payload that contains the user credentials. A token that must be used with subsequent API requests is then returned. In some cases the token is returned as a cookie in the response. When that is the case, a shortcut is to use a requests.session object. By using a session object, the token in the cookie can easily be reused on subsequent requests by sourcing the requests from the session object. This is the strategy used in the Cisco ACI example below.

POST with JSON Payload – Cisco ACI

Cisco ACI requires a JSON payload to be posted to the /aaaLogin URL endpoint with the username/password included. The response includes a cookie with key APIC-cookie and a token in the value that can be used on subsequent requests.

import requests
import os

username = os.environ.get('USERNAME')
password = os.environ.get('PASSWORD')
hostname = 'sandboxapicdc.cisco.com'

# Build the JSON payload with userid/password
payload = {"aaaUser": {"attributes": {"name": username, "pwd" : password }}}

# Create a Session object
session = requests.session()

# Specify the login URL
login_url = f'https://{hostname}/api/aaaLogin.json'

# Issue the login request. The cookie will be stored in session.cookies. 
response = session.post(login_url, json=payload, verify=False)

# Use the session object to get ACI tenants
if response.ok:
    response = session.get(f'https://{hostname}/api/node/class/fvTenant.json', verify=False)
else:
    print(f"HTTP Error {response.status_code}:{response.reason} occurred.")

>>> response.json()
{'totalCount': '4', 'imdata': [{'fvTenant': {'attributes': {'annotation': '', 'childAction': '', 'descr': '', 'dn': 'uni/tn-common', 'extMngdBy': '', 'lcOwn': 'local', 'modTs': '2021-10-08T15:31:47.480+00:00', 'monPolDn': 'uni/tn-common/monepg-default', 'name': 'common', 'nameAlias': '', 'ownerKey': '', 'ownerTag': '', 'status': '', 'uid': '0', 'userdom': 'all'}}}, {'fvTenant': {'attributes': {'annotation': '', 'childAction': '', 'descr': '', 'dn': 'uni/tn-infra', 'extMngdBy': '', 'lcOwn': 'local', 'modTs': '2021-10-08T15:31:55.077+00:00', 'monPolDn': 'uni/tn-common/monepg-default', 'name': 'infra', 'nameAlias': '', 'ownerKey': '', 'ownerTag': '', 'status': '', 'uid': '0', 'userdom': 'all'}}},
--- OUTPUT TRUNCATED FOR BREVITY ---

Certificate Checking

Note the verify=False in the above example. This can be used to turn off certificate checking when the device or API you are targeting is using a self-signed or invalid SSL certificate. This will cause a log message similar to the following to be generated:

InsecureRequestWarning: Unverified HTTPS request is being made to host ‘sandboxapicdc.cisco.com’. Adding certificate verification is strongly advised. See: https://urllib3.readthedocs.io/en/latest/advanced-usage.html#ssl-warnings`

The solution that should be used for a production deployment would be to install a valid SSL certificate, and don’t use verify=False. However, if you are dealing with lab devices that may never have a valid certificate then the message can be disabled using the following snippet:

import urllib3

urllib3.disable_warnings()

Handling Errors

It is helpful when working with requests to understand HTTP status codes and some of the common triggers for them when working with APIs. HTTP status codes indicate the success or failure of a request, and when errors occur, can give a hint toward what the problem might be. Here are some common HTTP status codes that you might see when working with APIs and potential causes:

200 OK: The request was successful

201 Created: Indicates a POST or PUT request was successful

204 Deleted: Indicates a successful DELETE request

400 Bad Request: Usually indicates there was a problem with the payload in the case of a POST, PUT, or PATCH request

401 Unauthorized: Invalid or missing credentials

403 Forbidden: An authenticated user does not have permission to the requested resource

404 Not Found: The URL was not recognized

429 Too Many Requests: The API may have rate limiting in effect. Check the API docs to see if there is a limit on number of requests per second or per minute.

500 Internal Server Error: The server encountered an error processing your request. Like a 400, this can also be caused by a bad payload on a POST, PUT or PATCH.

When the requests module receives the above status codes in the response, it returns a response object and populates the status_code and reason fields in the response object. If a connectivity error occurs, such as a hostname that is unreachable or unresolvable, requests will throw an exception. However, requests will not throw an exception by default for HTTP-based errors such as the 4XX and 5XX errors above. Instead it will return the failure status code and reason in the response. A common strategy in error handling is to use the raise_for_status() method of the response object to also throw an exception for HTTP-based errors as well. Then a Python try/except block can be used to catch any of the errors and provide a more human-friendly error message to the user, if desired.

Note that HTTP status codes in the 2XX range indicate success, and thus raise_for_status() will not raise an exception.

<span role="button" tabindex="0" data-code="# Example of error for which Requests would throw an exception # Define a purposely bad URL url = 'https://badhostname' # Implement a try/except block to handle the error try: response = requests.get(url, json=data) response.raise_for_status() except requests.exceptions.RequestException as e: print(f"Error while connecting to {url}: {e}") Error while connecting to https://badhostname: HTTPSConnectionPool(host='badhostname', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('
# Example of error for which Requests would throw an exception

# Define a purposely bad URL
url = 'https://badhostname'

# Implement a try/except block to handle the error
try:
    response = requests.get(url, json=data)
    response.raise_for_status()
except requests.exceptions.RequestException as e:
    print(f"Error while connecting to {url}: {e}")

Error while connecting to https://badhostname: HTTPSConnectionPool(host='badhostname', port=443): Max retries exceeded with url: / (Caused by NewConnectionError('<urllib3.connection.HTTPSConnection object at 0x108890d60>: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known'))

# Example of HTTP error, no exception thrown but we force one to be triggered with raise_for_status()

# Define a purposely bad URL
url = 'https://nautobot.demo.networktocode.com/api/dcim/regions/bogus'

# Get the API token from an environment variable.
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# Implement a try/except block to handle the error
try:
   response = requests.get(url, headers=headers)
   response.raise_for_status()
except requests.exceptions.RequestException as e:
   print(f"Error while connecting to {url}: {e}")

Error while connecting to https://nautobot.demo.networktocode.com/api/dcim/regions/bogus: 404 Client Error: Not Found for url: https://nautobot.demo.networktocode.com/api/dcim/regions/bogus/

CRUD (Create, Replace, Update, Delete) API Objects

So far we have mostly discussed retrieving data from an API using HTTP GET requests. When creating/updating objects, HTTP POST, PUT, and PATCH are used. A DELETE request would be used to remove objects from the API.

  • POST: Used when creating a new object
  • PATCH: Update an attribute of an object
  • PUT: Replaces an object with a new one
  • DELETE: Delete an object

It should be noted that some APIs support both PUT and PATCH, while some others may support only PUT or only PATCH. The Meraki API that we’ll be using for the following example supports only PUT requests to change objects.

POST

When using a POST request with an API, you typically must send a payload along with the request in the format required by the API (usually JSON, sometimes XML, very rarely something else). The format needed for the payload should be documented in the API specification. When using JSON format, you can specify the json argument when making the call to requests.post. For example, requests.post(url, headers=headers, json=payload). Other types of payloads such as XML would use the data argument. For example, requests.post(url, headers=headers, data=payload).

Create a Region in Nautobot

With Nautobot, we can determine the required payload by looking at the Swagger docs that are on the system itself at /api/docs/. Let’s take a look at the Swagger spec to create a Region in Nautobot.

Create a Region in Nautobot

The fields marked with a red * above indicate that they are required fields, the other fields are optional. If we click the Try it out button as shown above, it gives us an example payload.

Create a Region in Nautobot

Since the name and slug are the only required fields, we can form a payload from the example omitting the other fields if desired. The below code snippet shows how we can create the Region in Nautobot using requests.post.

<span role="button" tabindex="0" data-code="import requests import os # Get the API token from an environment variable. token = os.environ.get('NAUTOBOT_TOKEN') # Add the Authorization header headers = {'Authorization': f'Token {token}'} # This is the base URL for all Nautobot API calls base_url = 'https://nautobot.demo.networktocode.com/api' # Form the payload for the request, per the API specification payload = { "name": "Asia Pacific", "slug": "asia-pac", } # Create the region in Nautobot response = requests.post('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload) >>> response
import requests
import os

# Get the API token from an environment variable.
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# This is the base URL for all Nautobot API calls
base_url = 'https://nautobot.demo.networktocode.com/api'

# Form the payload for the request, per the API specification
payload = {
    "name": "Asia Pacific",
    "slug": "asia-pac",
}

# Create the region in Nautobot
response = requests.post('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload)

>>> response
<Response [201]>
>>> response.reason
'Created'

PATCH

PATCH request can be used to update an attribute of an object. For example, in this next snippet we will change the description of the Region we just created in the POST request. It was omitted in the previous POST request so it is currently a blank string. Although it is not called out in the Swagger API specification, the PATCH request for Nautobot requires the id field to be defined in the payload. The id can be looked up for our previously created Region by doing a requests.get on /regions?slug=asia-pac. The ?slug=asia-pac at the end of the URL is a query parameter that is used to filter the request for objects having a field matching a specific value. In this case, we filtered the objects for the one with the slug field set to asia-pac to grab the ID. In addition, the payload needs to be in the form of a list of dictionaries rather than a single dictionary as is shown in the Swagger example.

Update a Region Description in Nautobot

<span role="button" tabindex="0" data-code="import requests import os # Get the API token from an environment variable. token = os.environ.get('NAUTOBOT_TOKEN') # Add the Authorization header headers = {'Authorization': f'Token {token}'} # This is the base URL for all Nautobot API calls base_url = 'https://nautobot.demo.networktocode.com/api' # First we get the region_id from our previously created region response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=asia-pac', headers=headers) >>> response.json() {'count': 1, 'next': None, 'previous': None, 'results': [{'id': 'be2c22a2-56ce-4d84-8ac9-5a68c6a39d62', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/', 'name': 'Asia Pacific', 'slug': 'asia-pac', 'parent': None, 'description': 'Test region created from the API!', 'site_count': 0, '_depth': 0, 'custom_fields': {}, 'created': '2021-10-22', 'last_updated': '2021-10-22T21:20:07.628690', 'display': 'Asia Pacific'}]} # Parse the above response for the region identifier region_id = response.json()['results'][0]['id'] # Form the payload for the request, per the API specification (see preceding paragraph for some nuances!) payload = [{ "name": "Asia Pacific", "slug": "asia-pac", "description": "Test region created from the API!", "id": region_id }] # Update the region in Nautobot response = requests.patch('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload) >>> response
import requests
import os

# Get the API token from an environment variable.
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# This is the base URL for all Nautobot API calls
base_url = 'https://nautobot.demo.networktocode.com/api'

# First we get the region_id from our previously created region
response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=asia-pac', headers=headers)

>>> response.json()
{'count': 1, 'next': None, 'previous': None, 'results': [{'id': 'be2c22a2-56ce-4d84-8ac9-5a68c6a39d62', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/', 'name': 'Asia Pacific', 'slug': 'asia-pac', 'parent': None, 'description': 'Test region created from the API!', 'site_count': 0, '_depth': 0, 'custom_fields': {}, 'created': '2021-10-22', 'last_updated': '2021-10-22T21:20:07.628690', 'display': 'Asia Pacific'}]}

# Parse the above response for the region identifier
region_id = response.json()['results'][0]['id']

# Form the payload for the request, per the API specification (see preceding paragraph for some nuances!)
payload = [{
    "name": "Asia Pacific",
    "slug": "asia-pac",
    "description": "Test region created from the API!",
    "id": region_id
}]

# Update the region in Nautobot
response = requests.patch('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload)

>>> response
<Response [200]>

PUT

PUT request is typically used to replace an an entire object including all attributes of the object.

Replace a Region Object in Nautobot

Let’s say we want to replace the entire Region object that we created previously, giving it a completely new name, slug and description. For this we can use a PUT request, specifying the id of the previously created Region and providing new values for the name, slug, and description attributes.

<span role="button" tabindex="0" data-code="import requests import os # Get the API token from an environment variable token = os.environ.get('NAUTOBOT_TOKEN') # Add the Authorization header headers = {'Authorization': f'Token {token}'} # This is the base URL for all Nautobot API calls base_url = 'https://nautobot.demo.networktocode.com/api' # First we get the region_id from our previously created region response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=asia-pac', headers=headers) >>> response.json() {'count': 1, 'next': None, 'previous': None, 'results': [{'id': 'be2c22a2-56ce-4d84-8ac9-5a68c6a39d62', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/', 'name': 'Asia Pacific', 'slug': 'asia-pac', 'parent': None, 'description': 'Test region created from the API!', 'site_count': 0, '_depth': 0, 'custom_fields': {}, 'created': '2021-10-22', 'last_updated': '2021-10-22T21:20:07.628690', 'display': 'Asia Pacific'}]} # Parse the above response for the region identifier region_id = response.json()['results'][0]['id'] # Form the payload for the request, per the API specification (see preceding paragraph for some nuances!) payload = [{ "name": "Test Region", "slug": "test-region-1", "description": "Asia Pac region updated with a PUT request!", "id": region_id }] # Update the region in Nautobot response = requests.put('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload) >>> response
import requests
import os

# Get the API token from an environment variable
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# This is the base URL for all Nautobot API calls
base_url = 'https://nautobot.demo.networktocode.com/api'

# First we get the region_id from our previously created region
response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=asia-pac', headers=headers)

>>> response.json()
{'count': 1, 'next': None, 'previous': None, 'results': [{'id': 'be2c22a2-56ce-4d84-8ac9-5a68c6a39d62', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/', 'name': 'Asia Pacific', 'slug': 'asia-pac', 'parent': None, 'description': 'Test region created from the API!', 'site_count': 0, '_depth': 0, 'custom_fields': {}, 'created': '2021-10-22', 'last_updated': '2021-10-22T21:20:07.628690', 'display': 'Asia Pacific'}]}

# Parse the above response for the region identifier
region_id = response.json()['results'][0]['id']

# Form the payload for the request, per the API specification (see preceding paragraph for some nuances!)
payload = [{
    "name": "Test Region",
    "slug": "test-region-1",
    "description": "Asia Pac region updated with a PUT request!",
    "id": region_id
}]

# Update the region in Nautobot
response = requests.put('https://nautobot.demo.networktocode.com/api/dcim/regions/', headers=headers, json=payload)

>>> response
<Response [200]>

# Search for the region using the new slug
response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=test-region-1', headers=headers)

# This returns the replaced object, while retaining the same identifier
>>> response.json()
{'count': 1, 'next': None, 'previous': None, 'results': [{'id': 'be2c22a2-56ce-4d84-8ac9-5a68c6a39d62', 'url': 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/', 'name': 'Test Region', 'slug': 'test-region-1', 'parent': None, 'description': 'Asia Pac region updated with a PUT request!', 'site_count': 0, '_depth': 0, 'custom_fields': {}, 'created': '2021-10-22', 'last_updated': '2021-10-25T17:31:04.003235', 'display': 'Test Region'}]}

Enable an SSID in Meraki

Let’s look at another example of using a PUT to enable a wireless SSID in the Cisco Meraki dashboard. For this we will use a PUT request including the appropriate JSON payload to enable SSID 14.

import requests
import os

# Get the API key from an environment variable
api_key = os.environment.get('MERAKI_API_KEY')

# The base URI for all requests
base_uri = "https://api.meraki.com/api/v0"

# Set the custom header to include the API key
headers = {'X-Cisco-Meraki-API-Key': api_key}

net_id = 'DNENT2-mxxxxxdgmail.com' 
ssid_number = 14

url = f'{base_uri}/networks/{net_id}/ssids/{ssid_number}'

# Initiate the PUT request to enable an SSID. You must have a reservation in the Always-On DevNet sandbox to gain authorization for this. 
response = requests.put(url, headers=headers, json={"enabled": True})

DELETE

An object can be removed by making a DELETE request to the URI (Universal Resource Indicator) of an object. The URI is the portion of the URL that refers to the object, for example /dcim/regions/{id} in the case of the Nautobot Region.

Remove a Region from Nautobot

Let’s go ahead and remove the Region that we previously added. To do that, we’ll send a DELETE request to the URI of the region. The URI can be seen when doing a GET request in the url attribute of the Region object. We can also see in the API specification for DELETE that the call should be made to /regions/{id}

<span role="button" tabindex="0" data-code="# Search for the region using the slug response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=test-region-1', headers=headers) # Parse the URL from the GET request url = response.json()['results'][0]['url'] >>> url 'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/' # Delete the Region object response = requests.delete(url, headers=headers) # A status code of 204 indicates successful deletion >>> response
# Search for the region using the slug
response = requests.get('https://nautobot.demo.networktocode.com/api/dcim/regions/?slug=test-region-1', headers=headers)

# Parse the URL from the GET request
url = response.json()['results'][0]['url']

>>> url
'https://nautobot.demo.networktocode.com/api/dcim/regions/be2c22a2-56ce-4d84-8ac9-5a68c6a39d62/'

# Delete the Region object
response = requests.delete(url, headers=headers)

# A status code of 204 indicates successful deletion
>>> response
<Response [204]>

Rate Limiting

Some APIs implement a throttling mechanism to prevent the system from being overwhelmed with requests. This is usually implemented as a rate limit of X number of requests per minute. When the rate limit is hit, the API returns a status code 429: Too Many Requests. To work around this, your code must implement a backoff timer in order to avoid hitting the threshold. Here’s an example working around the Cisco DNA Center rate limit of 5 requests per minute:

<span role="button" tabindex="0" data-code="import requests from requests.auth import HTTPBasicAuth import time from pprint import pprint import os # Pull in credentials from environment variables username = os.environ.get('USERNAME') password = os.environ.get('PASSWORD') hostname = "sandboxdnac2.cisco.com" headers = {"Content-Type": "application/json"} # Use Basic Authentication auth = HTTPBasicAuth(username, password) # Request URL for the token login_url = f"https://{hostname}/dna/system/api/v1/auth/token" # Retrieve the token resp = requests.post(login_url, headers=headers, auth=auth) token = resp.json()['Token'] # Add the token to subsequent requests headers['X-Auth-Token'] = token url = f"https://{hostname}/dna/intent/api/v1/network-device" resp = requests.get(url, headers=headers, auth=auth) count = 0 # Loop over devices and get device by id # Each time we reach five requests, pause for 60 seconds to avoid the rate limit for i, device in enumerate(resp.json()['response']): count += 1 device_count = len(resp.json()['response']) print (f"REQUEST #{i+1}") url = f"https://{hostname}/dna/intent/api/v1/network-device/{device['id']}" response = requests.get(url, headers=headers, auth=auth) pprint(response.json(), indent=2) if count == 5 and (i+1)
import requests
from requests.auth import HTTPBasicAuth
import time
from pprint import pprint
import os

# Pull in credentials from environment variables  
username = os.environ.get('USERNAME')
password = os.environ.get('PASSWORD')
hostname = "sandboxdnac2.cisco.com"

headers = {"Content-Type": "application/json"}
# Use Basic Authentication
auth = HTTPBasicAuth(username, password)

# Request URL for the token
login_url = f"https://{hostname}/dna/system/api/v1/auth/token"

# Retrieve the token
resp = requests.post(login_url, headers=headers, auth=auth)
token = resp.json()['Token']

# Add the token to subsequent requests
headers['X-Auth-Token'] = token

url = f"https://{hostname}/dna/intent/api/v1/network-device"
resp = requests.get(url, headers=headers, auth=auth)

count = 0
# Loop over devices and get device by id
# Each time we reach five requests, pause for 60 seconds to avoid the rate limit
for i, device in enumerate(resp.json()['response']):
    count += 1
    device_count = len(resp.json()['response'])
    print (f"REQUEST #{i+1}")
    url = f"https://{hostname}/dna/intent/api/v1/network-device/{device['id']}"
    response = requests.get(url, headers=headers, auth=auth)
    pprint(response.json(), indent=2)
    if count == 5 and (i+1) < device_count:
      print("Sleeping for 60 seconds...")
      time.sleep(60)
      count = 0

Pagination

Some API calls may set a limit on the number of objects that are returned in a single call. In this case, the API should return paging details in the JSON body including the URL to request the next set of data as well as the previous set. If Previous is empty, we are on the first set of data. If Next is empty, we know we have reached the end of the dataset. Some API implementations follow RFC5988, which includes a Link header in the format:

Link: https://webexapis.com/v1/people?displayName=Harold&max=10&before&after=Y2lzY29zcGFyazovL3VzL1BFT1BMRS83MTZlOWQxYy1jYTQ0LTRmZWQtOGZjYS05ZGY0YjRmNDE3ZjU;

The above example is from the Webex API, which implements RFC5988. This is described in the API documentation here: https://developer.webex.com/docs/api/basics

Keep in mind that not all implementations use the RFC, however. The API documentation should explain how pagination is handled.

Handling Pagination in Nautobot

A good example of pagination can be seen when making a GET request to retrieve all Devices from Nautobot. Nautobot includes a countnext, and previous attribute in responses that are paginated. By default, the API will return a maximum of 50 records. The limit value as well as an offset value are indicated in the next value of the response. For example: 'next': 'https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=50'. In the URL, the limit indicates the max amount of records, and the offset indicates where the next batch of records begins. The previous attribute indicates the url for the previous set of records. If previous is None, it means we are on the first set of records. And if next is None, it means we are on the last set of records.

In the below snippet, we first retrieve the first set of 50 records and store them in a device_list variable. We then create a while loop that iterates until the next field in the response contains None. The returned results are added to the device_list at each iteration of the loop. At the end we can see that there are 511 devices, which is the same value as the count field in the response.

import requests
import os

# Get the API token from an environment variable
token = os.environ.get('NAUTOBOT_TOKEN')

# Add the Authorization header
headers = {'Authorization': f'Token {token}'}

# This is the base URL for all Nautobot API calls
base_url = 'https://nautobot.demo.networktocode.com/api'

# Create the initial request for the first batch of records
response = requests.get(f'{base_url}/dcim/devices', headers=headers)

# Store the initial device list
device_list = [device for device in response.json()['results']]

# Notice that we now have the first 50 devices
>>> len(device_list)
50

# But there are 511 total!
>>> response.json()['count']
511

# Loop until 'next' is None, adding the retrieved devices to device_list on each iteration
if response.json()['next']:
    while response.json()['next']:
        print(f"Retrieving {response.json()['next']}")
        response = requests.get(response.json()['next'], headers=headers)
        for device in response.json()['results']:
            device_list.append(device)

Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=50
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=100
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=150
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=200
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=250
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=300
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=350
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=400
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=450
Retrieving https://nautobot.demo.networktocode.com/api/dcim/devices/?limit=50&offset=500

>>> len(device_list)
511

Handling Pagination in Cisco Webex

In the code below, first we get the Room IDs for the WebEx Teams rooms I am a member of. Then we retrieve the members from the DevNet Dev Support Questions room and create a continuous function that follows the Link URL and displays the content. The While loop is broken when the Link header is no longer present, returning None when we try to retrieve it with headers.get(‘Link’).

import requests
import re
import os

api_path = "https://webexapis.com/v1"

# You can retrieve your token here: https://developer.webex.com/docs/api/getting-started
token = os.environ.get('WEBEX_TOKEN')
headers = {"Authorization": f"Bearer {token}"}

# List the rooms, and collect the ID for the DevNet Support Questions room
get_rooms = requests.get(f"{api_path}/rooms", headers=headers)
for room in get_rooms.json()['items']:
    print(room['title'], room['id'])
    if room['title'] == "Sandbot-Support DevNet":
      room_id = room['id']

# This function will follow the Link URLs until there are no more, printing out
# the member display name and next URL at each iteration. Note that I have decreased the maximum number of records to 1 so as to force pagination. This should not be done in a real implementation. 

def get_members(room_id):
    params = {"roomId": room_id, "max": 1}
    # Make the initial request and print the member name
    response = requests.get(f"{api_path}/memberships", headers=headers, params=params)
    print(response.json()['items'][0]['personDisplayName'])
    # Loop until the Link header is empty or not present
    while response.headers.get('Link'):
        # Get the URL from the Link header
        next_url = response.links['next']['url']
        print(f"NEXT: {next_url}")
        # Request the next set of data
        response = requests.get(next_url, headers=headers)
        if response.headers.get('Link'):
            print(response.json()['items'][0]['personDisplayName'])
        else:
            print('No Link header, finished!')

# Execute the function using the Sandbox-Support DevNet RoomID
get_members(room_id)


Conclusion

I hope this has been a useful tutorial on using requests to work with vendor REST APIs! While no two API implementations are the same, many of the most common patterns for working with them are covered here. As always, hit us up in the comments or on our public Slack channel with any questions. Thanks for reading!

-Matt



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

GraphQL vs. REST API Case Study

Blog Detail

If you’ve been watching this space, you’ve seen me talking about Nautobot’s GraphQL capabilities and how GraphQL helps you:

  • GraphQL queries are much more efficient than RESTful queries
  • GraphQL makes your life easier by making data more accessible
  • The above results in dramatic improvement in your quality of life

This post is a case study in those aspects. It will empirically demonstrate how GraphQL:

  • Minimizes your number of queries
  • Returns only the data you want
  • Makes it so you don’t have to manually filter data and build the desired data structure in a script
  • Creates faster automation
  • Reduces your workload

I will be running this case study using https://demo.nautobot.com/. Readers are encouraged to follow along, using the scripts below.

The Problem Statement

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:

  • We want information from all devices within the ams site
  • The data structure should organize information so that all the data is grouped on a per-device basis
  • We want this specific data for each device:
    • Device name
    • Device role
    • All the interface names
    • The list of IP address(es) for each interface, even if the interface has no configured IP address(es)

The GraphQL Solution

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))

GraphQL Results

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:

  • The returned data comes back in a structure that matches that of the query
  • GraphQL returns only the requested data
  • The returned data is ready for programmatic parsing

Running the script six times produced an average of 2.23 seconds, returning data for ten devices in the ams site.

RESTful Solution

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:

  • We had to artificially construct the data structure, which required a non-trivial amount of work
  • The RESTful script requires three distinct API calls, with some calls iterated multiple times
  • Each API call returns WAY more information than we are interested in
  • Since the call to get interface data for the ams site returns so much extraneous information, Nautobot applies the default limit of 50 results per call
    • The limit constraint reduces the load on the Nautobot server
    • With the default database in https://demo.nautobot.com, the call to get all the interface data iterates six times, returning up to 50 results per call
  • The call to get the IP address information must iterate once for each of the ten devices in the ams site
  • The RESTful script is over twice as long as the GraphQL script and is much more complex
  • The amount of time required to construct, test, and validate the RESTful script was well over an order magnitude longer than that required for the GraphQL script (your mileage may vary!)
"""
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.

Final Results

MethodAverage Run Time# of QueriesTime to Create Script
GraphQL2.2 seconds1~ 20 minutes
RESTful14.9 seconds17~ 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.

GraphQL Considerations for Server Load

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:

  • Use multiple, targeted GraphQL queries instead of a single GraphQL query with a large scope
  • Use RESTful queries and offload the processing from the Nautobot server, doing the post-processing on your local host

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 . . .

    Wrapping Up

    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:


    Conclusion

    You can also hit us up on the #nautobot channel on NTC Slack.

    Thank you, and have a great day!

    -Tim



    ntc img
    ntc img

    Contact Us to Learn More

    Share details about yourself & someone from our team will reach out to you ASAP!

    Leveraging the Power of GraphQL with Nautobot

    Blog Detail

    By now, almost every device or system has a REST API. REST APIs are well understood and streamline getting data into and out of systems programmatically. Since REST APIs are resource-specific, there is a different API for every resource. Because of this paradigm, users tend not to understand the relationships between the data resources. And in order to get all of the data needed to perform an operation, users may need to make numerous API calls, such as fetching a resource, getting an index of the related resource, fetching that related resource and the index of yet another related object, and the process continues.

    This post introduces GraphQL, a flexible API query language that streamlines working with relational data programmatically. You’ll get to see firsthand the value of GraphQL as we explore Nautobot’s GraphQL implementation. Having GraphQL as part of Nautobot makes it much simpler to gather data from your Network Source of Truth!

    This is the first in a series of posts that will discuss:

    • Using Nautobot’s GraphiQL interface to construct and practice GraphQL queries (this post)
    • Using Postman to construct programmatic GraphQL queries
    • Leveraging queries constructed in GraphiQL
    • Using Python to leverage programmatic GraphQL queries via
      • Programmatic GraphQL queries constructed with Postman
      • The pynautobot python library
    • Empirically demonstrating how GraphQL saves time and effort over traditional REST queries

    A Quick Primer: User Interfaces, Data, and Data Context

    A good user interface (UI) will present information in a way the user can understand. It does so by clearly presenting requested data points in a way easy for the user to understand, providing context for a data point, and allowing the user to quickly access related data points.

    GraphQL is for programmatic data what a good UI is for a person

    Take IP addresses as an example: a workflow may require retrieval of a single IP address for a given interface. Nautobot’s Web UI presents an interface’s IP address along with contextual information, including the remotely connected device and interface, as in the screenshot below:

    Not only does this view provide info on the objects related to the Interface and IP address, it also provides links to the related objects. This contextual data in the UI helps people understand the larger relationship for the IP address and easily access related information.

    The next section will explore how GraphQL provides the same clear presentation of requested data and contextual information, but in a programmatic format.

    What Is GraphQL?

    GraphQL is a query language for APIs. It allows a user to programmatically request and receive data from a server. One of GraphQL’s defining features is the capability to allow the user to specify the exact data needed, even if the data components are in different data silos. And, unlike REST API queries, a GraphQL query will return only the specific, requested data, even if the requested data spans multiple resources.

    GraphQL benefits summary:

    • Returns a data set with only the requested data; it does not require any filtering or post-processing
    • Easily returns a data set that shows the context and relationships around a given data point

    A single GraphQL query returns concise data would otherwise require multiple REST queries and extensive filtering

    For example, imagine you wanted to retrieve the IP address for an interface. It may follow that your process will also need other information related to the interface IP address: interface name, remote interface name, remote IP address, and remote device. Using GraphQL, you can easily specify the exact data you need, including the required related data. This post will demonstrate how to get started with GraphQL queries via Nautobot’s GraphiQL interface.

    Nautobot’s GraphiQL Browser

    Nautobot contains a GraphiQL interface. GraphiQL (notice the included i) is an interactive browser environment that allows users to easily craft GraphQL queries and explore the returned data.

    The exercises in this article use the public Nautobot sandbox available at https://demo.nautobot.com. We encourage you to follow along as we explore Nautobot’s GraphiQL interface.

    Accessing Nautobot’s GraphiQL Interface

    From the Nautobot Web UI homepage, scroll down and look to the lower right corner for a multi-choice menu. On that multi-choice menu, click on the GraphQL icon.

    You’ve arrived at the GraphiQL Interface:

    The GraphiQL interface for the public demo is https://demo.nautobot.com/graphql/.

    Now that we’ve reached the GraphiQL UI, let’s explore how to leverage it.

    GraphiQL’s Interactive UI

    Autocompletion

    One very interactive feature in GraphiQL is autocompletion. When the user constructs a query, the UI provides allowed completions for the level the user is typing on. This is possible because the system understands the GraphQL schema and relationships. In the example below, the user is typing within the query stanza, and the UI is showing allowed autocompletions that include the text the user has already typed:

    Interactive Data Schema Model in the UI

    Nautobot’s GraphiQL interactive environment enables the user to quickly create queries via access to the relevant data model in the UI. In GraphiQL, notice the <Docs button on the upper-right side. Clicking on this opens an interactive Documentation Explorer menu.

    In the screenshot below, to see what options are available for query in devices, type out (1) devices, then (2) click on the [DeviceType] link (you may have to hover the cursor over devices for a moment to see the Query:devices:[DeviceType] link appear). This opens the DeviceType Explorer. In this example, the user explores the schema to find that (3) name is an allowed field to query within the devices stanza.

    Quick access to Nautobot’s data models in real time gives the user the power to quickly create an effective query.

    GraphQL Query Examples

    The following are all examples from Network to Code’s public Nautobot demo instance, which has a robust set of data to demo and practice against. We encourage you to follow along at https://demo.nautobot.com.

    Example 1: Get All Device Names

    This first query will return all device names and only the device names: the data returned will not require any filtering. Start typing in the query on the left hand side. In the example below, Nautobot is showing a list of valid attributes that contain the letter ‘n’ for a device object. Once you’ve complete the query (1), press the play button (2) to execute and see the data (3):

    Take specific notice that only the requested data about the devices (each device’s name) was returned. GraphQL returns a data set that does not require filtering out extraneous information. A subsequent post in this series will explore that capability further.

    NOTE: Complete text examples for this and the remaining queries are at the end of this post

    Example 2: Query Devices and Include Site Names

    The next query returns the site name along with each device name. Notice the site attribute nests within the device object, in accordance with the Device data model.

    Here is the full query and some of the returned data:

    Example 3: Narrow the Scope Using Query Parameters

    In the next scenario, the user wants to retrieve a list of device names, but only for devices in the ams site. To narrow the returned data to a specific site, use a query parameter. In this case we will query for devices in the ams site using the (site: “ams”)query parameter. Since the query is explicitly narrowing the data to the ams site, returning the device’s site name is no longer necessary.

    This is a fairly simple query with the expected results. Notice that GraphQL returned only the requested data, saving us the time of filtering out unneeded data.

    The next example will be a bit more sophisticated and start to show how GraphQL can return data that would otherwise require multiple REST queries.

    Example 4: A More Sophisticated Query with Amazing Results

    GraphQL can return in one request data that would otherwise require multiple API requests. This next example shows a user requesting a list of interface names for each device in the ams site and the IP address on each interface:

    See the Text Query Examples and Results at the end of this post for a more complete example of the returned GraphQL data.

    This GraphQL query returns a data structure that otherwise would require 12 REST queries and extensive post processing

    This example humbly executes and demonstrates a powerful GraphQL capability in Nautobot. Namely:

    • GraphQL saves the user from having to execute 12 REST API calls
      • https://demo.nautobot.com/api/dcim/devices?site=ams
        • Returns a list of devices in ams
        • NOTE – There are 10 devices in ams site
      • https://demo.nautobot.com/api/dcim/interfaces?site=ams
        • Returns data for all interfaces in ams site
      • https://demo.nautobot.com/api/ipam/ip-addresses?device=ams-edge-01
        • Contains IP address information for a device
        • Run once for each of the ten devices in ams
    • With GraphQL, the user does not have to process the results for each of the REST API calls above and construct a data structure to hold the information
    • GraphQL saves the user the time needed to scour the API docs looking for the endpoint that would most efficiently return the data
      • In the example above, the user may have to spend time looking for the https://demo.nautobot.com/api/dcim/interfaces?site=ams API, which returns data for all interfaces and allows site to be specified as a query parameter
    • GraphQL returns only the data the user asks for, which limits or likely eliminates the effort to post-process the data

    A follow-on post in this series will explore in-depth those required REST calls and the subsequent Python data processing that would produce results equivalent to this single GraphQL query.

    Example 5: Easily Include Additional Relevant Data

    To finish out our exploration of the ams site, the user also needs the interface’s remote device and connected interface:

    Leveraging the Autocomplete Feature

    When constructing sophisticated queries like this one, it is very helpful to leverage the autocomplete feature in the query. This feature will show you if you are in the correct position in the data model to get the data point you are looking for (connected_interface autocompletes in the interfaces stanza) . . .

    . . . or not! Notice that connected_interface is not an option and will not autocomplete in the devices stanza:

    Example 6: A GraphQL Example of the Prior Web UI Case

    Earlier, this article showed Nautobot’s Web UI displaying the IP address (10.0.192.0/32) for Interface Ethernet1/1 on atl-edge-01. That same page also showed the connected Interface (Ethernet1/1) on the connected device (atl-edge-02).

    GraphQL’s ease of querying and concise data retrieval allow both automation and people to efficiently retrieve and process data

    The GraphQL equivalent query and results in GraphiQL are:

    Spotlight on Query Parameters

    This last example showcases two query parameters that qualify the interfaces query:

    • device:“atl-edge-01”
      • Returns interface data for only the specified device
    • The name__ie lookup expression
      • This specific expression allows for a case-insensitive exact match on the interface name (case insenstive, exact match)
      • Depending on your use case, this query could have also used name:"Ethernet1/1 instead, but would require exact capitalization matching
      • A full explanation of Nautobot’s available lookup expressions is available at https://nautobot.readthedocs.io/en/latest/rest-api/filtering/#lookup-expressions

    Final Thoughts and a Look Ahead

    Most optimizations have trade-offs, and GraphQL is no exception. By returning only the requested data that may span multiple resources, GraphQL offloads much of the data processing from the user onto the server. Depending on your situation and use case, this may or may not be the desired behavior.

    In terms of performance, GraphQL is best suited for

    • Lots of data for a single device
    • A few bits of data for hundreds of devices

    In general, it is best to use multiple GraphQL queries or multiple individual REST API queries for

    • Requesting data about thousands of devices

    The next post in this series will go on to demonstrate how to retrieve GraphQL data programmatically in Postman and in Python. Another post will use the fourth example in this post as a case study to examine the required REST API calls and processing the user would have to perform if using REST API queries instead of GraphQL.

    -Tim


    Text Query Examples and Results

    The text of the GraphQL queries and the results for each are below. The GraphQL queries can be pasted into a GraphiQL query or used in the body of an API call.

    Example 1: Get All Device Names

    Query Nautobot for all device names

    query {
      devices {
        name
      }
    }
    

    Result (abbreviated for brevity)

    {
      "data": {
        "devices": [
          {
            "name": "ams-edge-01"
          },
          {
            "name": "ams-edge-02"
          },
          {
            "name": "ams-leaf-01"
          },
    
    
          {
            "name": "sin-leaf-07"
          },
          {
            "name": "sin-leaf-08"
          }
        ]
      }
    }
    

    Example 2: Query Devices with Site Names

    query {
      devices {
        name
    	site {
          name
        }
      }
    }
    

    Here is the returned data, snipped for brevity:

    {
      "data": {
        "devices": [
          {
            "name": "ams-edge-01",
            "site": {
              "name": "ams"
            }
          },
          {
            "name": "ams-edge-02",
            "site": {
              "name": "ams"
            }
          },
        ...
        ...
          {
            "name": "sin-leaf-07",
            "site": {
              "name": "sin"
            }
          },
          {
            "name": "sin-leaf-08",
            "site": {
              "name": "sin"
            }
          }
        ]
      }
    }
    

    Example 3: Narrow the Scope Using Query Parameters

    Query Nautobot for the names of all the devices in the ams site:

    query {
     devices(site:"ams") {
       name
     }
    }
    

    Here is the sample output, snipped for brevity:

    {
     "data": {
       "devices": [
         {
           "name": "ams-edge-01"
         },
         {
           "name": "ams-edge-02"
         },
         {
           "name": "ams-leaf-01"
         },
       ...
       ...
         {
           "name": "ams-leaf-07"
         },
         {
           "name": "ams-leaf-08"
         }
       ]
     }
    }
    

    Example 4: A More Sophisticated Query with Amazing Results

    query {
     devices(site:"ams") {
       name
       interfaces {
         name
         ip_addresses {
           address
         }
       }
     }
    }
    

    Abbreviated results:

    {
     "data": {
       "devices": [
         {
           "name": "ams-edge-01",
           "interfaces": [
             {
               "name": "Ethernet1/1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.0/32"
                 }
               ]
             },
             {
               "name": "Ethernet2/1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.2/32"
                 }
               ]
             },
       ...
       ...
             {
           "name": "ams-leaf-08",
           "interfaces": [
             {
               "name": "Ethernet1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.33/32"
                 }
               ]
             },
             ...
             ...
               {
               "name": "vlan99",
               "ip_addresses": [
                 {
                   "address": "10.11.71.0/32"
                 }
               ]
             },
             {
               "name": "vlan1000",
               "ip_addresses": [
                 {
                   "address": "10.11.7.0/32"
                 }
               ]
             }
           ]
         }
       ]
     }
    }       
    

    Example 5: Easily Include Additional Relevant Data

    query {
     devices(site:"ams") {
       name
       interfaces {
         name
         ip_addresses {
           address
         }
         connected_interface {
           device {
             name
           }
           name
         }
       }
     }
    }
    

    Abbreviated results:

    {
     "data": {
       "devices": [
         {
           "name": "ams-edge-01",
           "interfaces": [
             {
               "name": "Ethernet1/1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.0/32"
                 }
               ],
               "connected_interface": {
                 "device": {
                   "name": "ams-edge-02"
                 },
                 "name": "Ethernet1/1"
               }
             },
             {
               "name": "Ethernet2/1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.2/32"
                 }
               ],
               "connected_interface": {
                 "device": {
                   "name": "ams-edge-02"
                 },
                 "name": "Ethernet2/1"
               }
             },
           ...
           ...
             {
               "name": "Management1",
               "ip_addresses": [],
               "connected_interface": null
             }
           ]
         },
       ...
       ...
         {
           "name": "ams-leaf-08",
           "interfaces": [
             {
               "name": "Ethernet1",
               "ip_addresses": [
                 {
                   "address": "10.11.192.33/32"
                 }
               ],
               "connected_interface": {
                 "device": {
                   "name": "ams-edge-01"
                 },
                 "name": "Ethernet10/1"
               }
             },
             {
               "name": "Ethernet2",
               "ip_addresses": [
                 {
                   "address": "10.11.192.35/32"
                 }
               ],
               "connected_interface": {
                 "device": {
                   "name": "ams-edge-02"
                 },
                 "name": "Ethernet10/1"
               }
             },
           ...
           ...
             {
               "name": "vlan99",
               "ip_addresses": [
                 {
                   "address": "10.11.71.0/32"
                 }
               ],
               "connected_interface": null
             },
             {
               "name": "vlan1000",
               "ip_addresses": [
                 {
                   "address": "10.11.7.0/32"
                 }
               ],
               "connected_interface": null
             }
           ]
         }
       ]
     }
    }
    

    Example 6: A GraphQL Example of the Prior Web UI Case

    query {
      interfaces(device:"atl-edge-01", name__ie:"Ethernet1/1") {
         name
       ip_addresses {
         address
       }
       connected_interface {
         name
         device {
           name
         }
       }
     }
    }
    

    Result:

    {
     "data": {
       "interfaces": [
         {
           "name": "Ethernet1/1",
           "ip_addresses": [
             {
               "address": "10.0.192.0/32"
             }
           ],
           "connected_interface": {
             "name": "Ethernet1/1",
             "device": {
               "name": "atl-edge-02"
             }
           }
         }
       ]
     }
    }
    


    ntc img
    ntc img

    Contact Us to Learn More

    Share details about yourself & someone from our team will reach out to you ASAP!