Nautobot Ansible Variable Management at Scale

Blog Detail

Ansible’s variable management system is fairly extensible, however the cost of that extensibility and associated design choices can cause some inefficiencies when interacting with external systems. Specifically, it can become rather time consuming to get all of the data a playbook requires at run time. Leveraging Nautobot’s GraphQL and Nautobot Ansible collection, we will explore what an optimal solution would be.

A Quick Lesson in Ansible Inventory

Initially Ansible had created an Ansible Dynamic Inventory, which was a script that would print to the terminal a JSON serializable structure in a specific format. Once Ansible 2.4 was released, there was support for Ansible Inventory Plugins, which provide a more object-oriented and Pythonic experience, as well as a separation of the configuration (generally via YAML files) from the inventory itself.

With both of these dynamic inventory types as well as any static inventory, the inventory must be compiled before the play runs. This means that all inventory and variables are collected before a playbook is run. If your playbook requires a connection to only a single device and needs a single configuration parameter, this would still require the entire inventory and variables to compile, which is the same as if the playbook had to connect to thousands of devices for dozens of variables each.

This design certainly has its advantages, such as Ansible’s use of the hostvars (not to be confused with host_vars) magic variable. Meaning, even if you need to connect to a only single device, you can still have access to another device’s variable. This would allow you to do something like:

  - name: "SET SPINE_INTERFACE BY LOOKING INTO THE SPINES VARIABLE STRUCTURE"
    set_fact:
      spine_interface: "{{ hostvars[inventory_hostname[:5] ~ 'spine01']['interface_mappings'][inventory_hostname] }}"

However, such a requirement is not often needed, and it is perfectly valid to provide an alternative solution without such a feature, as we will explore.

The Speed Issue

It is obvious that the standard design causes a host of speed issues when not all variables are required. Within Nautobot, to collect all of the interfaces and config context of thousands of devices could literally take hours. This is because the queuing mechanism looks something like:

Speed Issue

In this example, it could potentially take hundreds or even thousands of API calls before the first task runs, and all of that data needs to be stored in memory. This is true even if the only data we require actually looks like:

Speed Issue

GraphQL to the Rescue

Recognizing the speed issues, at Network to Code we have worked with our customers for years on various work-arounds, which was one of the drivers to introducing GraphQL to Nautobot. What we have observed from dozens of engagements with our customers is:

  • Playbooks rarely need access to all data
  • There is generally a single “generate configuration” playbook that does need access to all data
  • There are usually different ways data may need to be requested
  • Managing separate inventories is complicated and leads to issues
  • The primary issue is the way in which data is queued, with Ansible expecting all data to be queued beforehand

With that in mind, we looked to change the way that variables are populated; this is different from saying we looked to change how the inventory plugin works. The basic premise is to get the bare minimum inventory from the inventory plugin, then populate the data within the play itself. The direct benefit is that inventory does not require nearly the amount of data (which must be present before any task in the play is run) before starting. And we have also distributed the amount of data to be smaller API calls made while the play is running. Additionally, if we do not need all data, we simply do not need to get that data at all.

It is in that second step that GraphQL really shines. GraphQL provides a single API that can be called to send only the data that is required. There is a lookup and an Ansible module within Nautobot’s Ansible Collection. This means that we can use a single inventory setup for all of our playbooks and have specific tasks to get the data required for specific playbooks. We can also change the majority of the API calls to happen per device rather than all up front. This has a significant performance impact, as bombarding the server with hundreds or thousands of API calls at once can cause performance issues—not only for the user of Ansible, but potentially deteriorating the performance of the server for everyone else.

Rescue

Even when you do require all of the data, it looks more like this (where time is left-to-right and scaled to your actual needs):

Rescue

Note: The depicted batch size is equal to the fork size you have chosen. There are also alternative Ansible strategies one can explore outside the scope of this blog.

Note: The API calls shown are not meant to represent the actual amount a production instance may have, but merely to illustrate the point.

Example Playbook and Inventory

So let’s take a look at what such a playbook and inventory may look like.

plugin: networktocode.nautobot.inventory
api_endpoint: "https://demo.nautobot.com"
validate_certs: False

config_context: False
plurals: False
interfaces: False
services: False
racks: False
rack_groups: False

compose:
  device_id: id

group_by:
  - site
  - tenant
  - tag
  - role
  - device_type
  - manufacturer
  - platform
  - region
  - status

A playbook to obtain and populate the data could look like:

---
- name: "TEST NAUTOBOT INVENTORY"
  connection: "local"
  hosts: "all"
  gather_facts: "no"

  tasks:
      - name: "SET FACT FOR QUERY"
        set_fact:
          query_string: |
            query ($device_id: ID!) {
              device(id: $device_id) {
                config_context
                hostname: name
                position
                serial
                primary_ip4 {
                  id
                  primary_ip4_for {
                    id
                    name
                  }
                }
                tenant {
                  name
                }
                tags {
                  name
                  slug
                }
                device_role {
                  name
                }
                platform {
                  name
                  slug
                  manufacturer {
                    name
                  }
                  napalm_driver
                }
                site {
                  name
                  slug
                  vlans {
                    id
                    name
                    vid
                  }
                  vlan_groups {
                    id
                  }
                }
                interfaces {
                  description
                  mac_address
                  enabled
                  name
                  ip_addresses {
                    address
                    tags {
                      id
                    }
                  }
                  connected_circuit_termination {
                    circuit {
                      cid
                      commit_rate
                      provider {
                        name
                      }
                    }
                  }
                  tagged_vlans {
                    id
                  }
                  untagged_vlan {
                    id
                  }
                  cable {
                    termination_a_type
                    status {
                      name
                    }
                    color
                  }
                  tagged_vlans {
                    site {
                      name
                    }
                    id
                  }
                  tags {
                    id
                  }
                }
              }
            }

      - name: "GET DEVICE INFO FROM GRAPHQL"
        networktocode.nautobot.query_graphql:
          url: "{{ nautobot_url }}"
          token: "{{ nautobot_token }}"
          validate_certs: False
          query: "{{ query_string }}"
          update_hostvars: "yes"
          graph_variables:
            device_id: "{{ device_id }}"

The above shows update_hostvars set, which will publish the variables for any playbook task after this point. Within a playbook that starts like the above, you would have access to the data. If the playbook did not have any requirements for the above data, you would simply not include such tasks.

Life without GraphQL

Without GraphQL the same can still be accomplished. In the past at Network to Code, we have used Ansible custom modules. Within the custom module you can populate the ansible_facts key, which will actually update the data associated with a device. So if a custom Ansible module had the below code:

    results = {"ansible_facts": {"ntp": ["1.1.1.1", "2.2.2.2"]}}
    module.exit_json(**results)

you could have access to the data in the playbook as usual, such as:

  - debug: var=ntp

Inventory Recommendations

If you will notice in the example inventory, the inventory is minimal. The basic premise is that you should disable any data not required to create groups and, generally speaking, retain only the minimum amount of information required to connect to the device, such as IP address and network OS.


Conclusion

Changing the queuing mechanism has dramatic effects on the overall speed, and Nautobot’s ecosystem was built to take advantage of these capabilities. But that is not the only way to work this, as you could build a custom module as well. When thinking about performance and scalability of the data, you should consider a lightweight inventory and more detailed data on a task level.

-Ken Celenza



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!

    Nautobot GraphQL Requests via Postman and Python

    Blog Detail

    GraphQL is a powerful query tool because it allows efficient queries for specific information that can span multiple resources that would otherwise require literally dozens of individual REST queries and extensive data filtering, post-processing, and isolation.

    This article builds on the prior articles in this series, which describe how to craft GraphQL queries in Nautobot’s GraphiQL interface and how to use GraphQL aliasing to customize the returned key names and do multiple queries in a single GraphQL request.

    The previous post in this series demonstrated how to leverage the Pynautobot Python package for programmatic GraphQL queries, showing a contrasting way (from the Postman-centric way described in the post below) to construct programmatic GraphQL queries in Python against Nautobot. The Pynautobot Python path provides a very clean, customized way to programmatically run simple or sophisticated GraphQL requests against Nautobot. The methodology described in this post examines a more general way to go about that, but at the expense of additional steps. Depending on your use case, one way or the other may be preferable.

    This post will demonstrate three different techniques for configuring token authentication within Postman and two different data formats in the request body.

    This post uses Postman version 8.0.6.

    The examples in this post all use the public Nautobot demo site and the free Postman app. Readers are encouraged to follow along.

    Authentication

    Security tokens are typically required for programmatic access to Nautobot’s data. The following sections cover how to obtain a token and how to leverage it within Postman to craft GraphQL API calls.

    Nautobot security tokens are created in the Web UI. To view your token(s), navigate to the API Tokens page under your user profile. If there is no token present, create one or get the necessary permissions to do so.

    1-api-token

    Setting Up Postman Token Authentication

    There are multiple ways to authenticate your Nautobot API queries in Postman:

    • Directly inserting the token information in the request header
    • The Authentication tab for the request
    • The Authorization tab for the collection

    Authentication Option 1: Editing the Header

    Inserting the token authentication information directly in the header allows individual queries to authenticate, but the user must manually populate the header with the authentication info on each new query.

    To start a query that uses this method within Postman, create a new Postman collection:

    • Click on the ‘+’ sign on the upper left of the app.
    • Name the collection by clicking on the pencil/edit button by the default New Collection name.

    NOTE: A Postman Collection is a group of saved requests, oftentimes with a common theme, workflow, or function

    2-collection

    Name the collection Authentication Testing.

    Create a new API request by clicking on the ‘+’ sign to the right of the new collection tab.

    3-auth-testing

    Before we create the actual request, we’ll edit the header by adding the authentication/token info:

    1. Go to the Headers tab
    2. View the auto-generated headers (click on the eyeball)
    3. In the next available row, type Authorization in the Key column (it should allow you to auto-complete)
    4. In the Value column, type in the word Token<space><key>
      • In this example I entered Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    The Nautobot public sandbox key is aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

    4-edit-header

    Your Authorization portion is now complete.

    Query via GraphQL in Body

    The next steps will guide you through crafting the rest of the query. This query will retrieve the name of each device along with the device’s site name.

    This example will use the GraphQL format in the body.

    1. Change the request type to POST
    2. Enter the Nautobot server + GraphQL endpoint URL <server>/api/graphql/
    3. Go to the Body section
    4. Select GraphQL
    5. Input the specific query shown below
    query {
     devices {
       name
       site {
        name
      }
     }
    }
    
    5-graphql-body

    Click the Send button and wait for the return data. You should see output similar to the picture below if you are using the public-facing Nautobot demo site.

    6-returned-data

    Save the request in the collection.

    7-save-request

    Authentication Option 2: The Authorization Tab

    This section will show an example of authentication in the request’s Authorization tab. This example will reside in the same Authentication Testing collection.

    To create the new request:

    1. Right-click on the collection name
    2. Select Add Request
    3. Name the request bkk devices; uses Authorization Tab
    8-add-request

    Configure the Authorization tab first, using the settings shown below; the Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa entry is the same as in the prior section.

    9-edit-auth-tab

    Go to the Headers tab. If the hidden headers are not already being shown, click on the hidden headers eyeball to view the hidden headers. The Authorization key-value pair should be visible now. This was auto-generated from info in the Authorization tab.

    10-show-headers

    Query via Raw Data

    Finish out the query, but this time use raw in the Body section. Be sure to specify the format as JSON.

    This query will return the device names in the bkk site, along with each device’s rack name and device role name. Be sure to escape the double-quotes in the site query parameter as shown below.

    {
       "query": "query {devices(site:\"bkk\") {name device_role { name } rack { name } } }"
    }
    

    NOTE: Raw format is really cumbersome to work with, in part because of the requirement to escape quotes

    11-raw-body-query

    Notice the potentially tedious nature of crafting the query above using the raw data option. It may be best to use the GraphQL format in the body for more complex queries.

    If you check the Headers tab again, you’ll notice that changing the body format to JSON sets the Content-Type value to application/json.

    12-updated-headers

    Authentication Option 3: Via the Collection Environment

    Within a Postman collection, it is more practical to configure authentication in the collection’s environment instead of configuring authentication for each request. Configuring authentication for the entire collection lets the user configure authentication just once.

    This next section will describe how to set authentication at the collection level.

    To the right of the collection name there are 3 dots (1); click on them and select Edit (2).

    13-collection-auth

    From here

    1. Select the Authorization tab
    2. Select API Key for Type
    3. Set Key to Authorization
    4. Set Value to Token<space><key>
    5. Save
    14-collection-auth-config

    Any added requests in this collection can now use the Inherit auth from parent setting.

    To configure a request to use the collection’s authentication:

    1. Select the bkk devices request that uses the Authorization tab (for example)
    2. Select the Authorization tab within the request
    3. Switch the Type to Inherit auth from parent
    4. Click Send
    15-auth-from-parent

    Save this updated request if you wish.

    Converting Postman Queries to Python

    Postman has helped us construct our GraphQL requests. This next section describes how to leverage those requests programmatically in Python.

    This section will continue on from where we left off above, with the bkk devices request.

    Postman has a very useful capability to export your crafted requests into a code format of your choice. On the right-hand side of the app, select the Code snippet button, marked as </>.

    16-code-export-button

    Within the search box that appears you can type Python or select from any of the languages that appear in the dropdown menu. This example will use the Python requests library.

    17-select-python-requests (1)

    Selecting the Python – Requests option, you will now see Python code that will programmatically make the request using Python’s requests library and return the response.

    18-python-code

    If cookie information appears in the code and you do not wish this to be present, you can manually delete it from the code or prevent it from being generated by clicking on Cookies (located directly beneath the Send button) to open the Cookie Manager; delete any cookies configured and then regenerate the Python code.

    19-cookies

    This Python code snippet can be added to any script or simply inserted into a Python interpreter:

    ~ % python3
    Python 3.9.2 (v3.9.2:1a79785e3e, Feb 19 2021, 09:06:10)
    [Clang 6.0 (clang-600.0.57)] on darwin
    Type "help", "copyright", "credits" or "license" for more information.
    >>> import requests
    >>>
    >>> url = "https://demo.nautobot.com/api/graphql/"
    >>>
    >>> payload="{\n    \"query\": \"query {devices(site:\\"bkk\\") {name device_role { name } rack { name } } }\"\n}"
    >>> headers = {
    ...   'Authorization': 'Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
    ...   'Content-Type': 'application/json'
    ... }
    >>>
    >>> response = requests.request("POST", url, headers=headers, data=payload)
    >>>
    >>> print(response.text)
    {"data":{"devices":[{"name":"bkk-edge-01","device_role":{"name":"edge"},"rack":{"name":"bkk-101"}},{"name":"bkk-edge-02","device_role":{"name":"edge"},"rack":{"name":"bkk-102"}},{"name":"bkk-leaf-01","device_role":{"name":"leaf"},"rack":{"name":"bkk-101"}},{"name":"bkk-leaf-02","device_role":{"name":"leaf"},"rack":{"name":"bkk-102"}},{"name":"bkk-leaf-03","device_role":{"name":"leaf"},"rack":{"name":"bkk-103"}},{"name":"bkk-leaf-04","device_role":{"name":"leaf"},"rack":{"name":"bkk-104"}},{"name":"bkk-leaf-05","device_role":{"name":"leaf"},"rack":{"name":"bkk-105"}},{"name":"bkk-leaf-06","device_role":{"name":"leaf"},"rack":{"name":"bkk-106"}},{"name":"bkk-leaf-07","device_role":{"name":"leaf"},"rack":{"name":"bkk-107"}},{"name":"bkk-leaf-08","device_role":{"name":"leaf"},"rack":{"name":"bkk-108"}}]}}
    >>>
    

    NOTE: Being that there are multiple ways to use the requests library, you can also use the more concise requests.post method: response = requests.post(url, headers=headers, data=payload).

    Here is a Python script that uses the requests.post method. The script executes the GraphQL request, prints the text response, and then pretty prints the returned json data:

    import json
    import requests
    
    from pprint import pprint
    
    print("Querying Nautobot via graphQL API call.")
    print()
    
    url = "https://demo.nautobot.com/api/graphql/"
    print("url is: {}".format(url))
    print()
    
    payload="{\n    \"query\": \"query {devices(site:\\"bkk\\") {name device_role { name } rack { name } } }\"\n}"
    print("payload is: {}".format(payload))
    print()
    
    headers = {
      'Authorization': 'Token aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
      'Content-Type': 'application/json'
    }
    print("headers is: {}".format(headers))
    print()
    
    response = requests.post(url, headers=headers, data=payload)
    
    response_data = response.json()
    
    print("Here is the response data in json:")
    pprint(response_data)
    

    Note the tedious nature of crafting payload variable in the script above – it’s fairly complex with all the escaping backslashes. Luckily, the Postman app will create the object in the code for us, whether we use GraphQL or raw for the Body format.


    Conclusion

    To fully leverage GraphQL’s efficiency, it must be used programmatically. Prior posts in this series demonstrated using Nautobot’s GraphiQL interface to craft GraphQL queries and how to use GraphQL aliasing. This post builds on that by showing how to convert those GraphQL queries into remote requests in Postman and going on to export those requests to Python code for programmatic use. Also be sure to check out the prior post on using programmatic GraphQL requests with Nautobot via the pynautobot package and how that stacks up against the methodology in this post. Generally, the pynautobot path has fewer moving parts and offers customized APIs for Nautobot that allow sophisticated GraphQL queries. The Postman+requests path offers a more general framework but with more steps.



    ntc img
    ntc img

    Contact Us to Learn More

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