API Programmability - Part 2: Python

6 minute read

Introduction

In API Programmability - Part 1 we showed you the basics of how to use IP Fabric’s API.  If you have not read the blog, please take a few minutes to look at it to understand how to create a token and familiarize yourself with it.  In this post we are going to use the ipfabric Python package located on pypi.org and at GitHub. The code we will be discussing here is in the python-part-2 directory located at https://github.com/community-fabric/blog-demos.

Prerequisites

Before we begin here are some of the basics you will need to follow along:

  • IP Fabric (3.7.1 or higher which supports API Token)
  • Python 3 (Any version should do)
  • pip (for python package installation)
    • `pip3 install ipfabric`
  • IDE or Notepad++ (Optional)

Let's get coding

class IPFClient(httpxClient):
    def __init__(
            self,
            base_url: Optional[str] = None,
            token: Optional[str] = None,
            snapshot_id: str = "$last",
            *vargs,
            **kwargs
    ):
        """
        Initializes the IP Fabric Client
        :param base_url: str: IP Fabric instance provided in 'base_url' parameter, or the 'IPF_URL' environment variable
        :param token: str: API token or 'IPF_TOKEN' environment variable
        :param snapshot_id: str: IP Fabric snapshot ID to use by default for database actions - defaults to '$last'
        :param vargs:
        :param kwargs:
        """
        try:
            base_url = urljoin(base_url or os.environ["IPF_URL"], "api/v1/")
        except KeyError:
            raise RuntimeError(f"IP Fabric base_url not provided or IPF_URL not set")

        try:
            token = token or os.environ["IPF_TOKEN"]
        except KeyError:
            raise RuntimeError(f"IP Fabric token not provided or IPF_TOKEN not set")

        super().__init__(base_url=base_url, *vargs, **kwargs)
        self.headers.update({'Content-Type': 'application/json', 'X-API-Token': token})

        # Request IP Fabric for the OS Version, by doing that we are also ensuring the token is valid
        self.os_version = self.fetch_os_version()
        self.snapshots = self.get_snapshots()
        self.snapshot_id = snapshot_id
        self.inventory = Inventory(self)

When initializing the client it requires the base_url and token or the environment variables IPF_URL and IPF_TOKEN to be set.  It will then send a request to your IP Fabric instance to get the version and all snapshots in the instance. 

"""
snapshot.py
"""
from ipfabric import IPFClient
from pprint import pprint


if __name__ == '__main__':
    ipf = IPFClient('https://demo3.wordpress-625423-2416527.cloudwaysapps.com/', snapshot_id="$lastLocked")

    print(f"IP Fabric version: {ipf.os_version}")
    """
    IP Fabric version: 4.0.2
    """

    print(f"Class snapshot id is set to \t{ipf.snapshot_id}")
    print(f"Latest snapshot id is set to\t{ipf.snapshots['$last']. snapshot_id}")
    """
    Class snapshot id is set to    d3bd033e-1ba6-4b27-86f5-18824a1a495e
    Latest snapshot id is set to   c8684ea9-dfd8-400d-a4b8-ba1c4bc7c185
    """

    print("Print a snapshot object: ")
    pprint(ipf.snapshots[ipf.snapshot_id].__dict__)
    """
    Print a snapshot object: 
    {'count': 640,
     'end': datetime.datetime(2021, 10, 21, 12, 37, 3, 513000),
     'snapshot_id': 'd3bd033e-1ba6-4b27-86f5-18824a1a495e',
     'loaded': True,
     'locked': True,
     'name': 'Baseline 10-21',
     'start': datetime.datetime(2021, 10, 21, 11, 59, 54, 941000)}
    """

If no snapshot is set it will default to use $last, however you can also specify $prev, $lastLocked, or an ID of a snapshot.  Please remember when dealing with snapshots is that it must be loaded to view the data in inventory and technology tables.  Snapshot meta data is loaded in a python object with start and end times converted into datetime objects for easier manipulation and querying.

Inventory Data

"""
get_inventory.py
"""
from ipfabric import IPFClient
from pprint import pprint


if __name__ == '__main__':
ipf = IPFClient('https://demo3.wordpress-625423-2416527.cloudwaysapps.com/')

print([x for x in dir(ipf.inventory) if not x.startswith('_')
])
"""
['devices', 'families', 'interfaces', 'models', 'part_numbers', 'platforms', 'sites', 'vendors']
"""

Inside the IPFClient class I created an Inventory object which can be used to fetch all data in those tables.  This contains data like sites, devices, and interfaces.  We can easily pull that data by calling the all method on them.

sites = ipf.inventory.sites.all()
print(f"Number of sites: {len(sites)}")
print("First site:")
pprint(sites[0])
"""
Number of sites: 25
First site:
{'devicesCount': 22,
 'id': '1111118694',
 'siteKey': '1111118694',
 'siteName': 'HWLAB',
 'siteUid': 'HWL',
 ...}
"""

This removes the need for going to the webpage, finding the API endpoint to use, and the data required to post.  The all method also allows filtering.  It is recommended to create your filter in the GUI to ensure you are retrieving the correct data.  Simply create a filter, in the example I am filtering on site L71 however you can also use advanced filter, then click the "?" Table Description button at the top right of the table.

Here we find the filter information.  You may copy and convert this JSON object to a Python dictionary or enclose it as a string and the client will handle the JSON conversion for you.

devices = ipf.inventory.devices.all(filters={
    "siteName": [
        "like",
        "L71"
    ]
})
"""
Also acceptable if filter is string
devices = ipf.inventory.devices.all(filters='{"siteName": ["like","L71"]}')
"""
print(f"Number of devices in site L71: {len(sites)}")
print("First Device:")
pprint(devices[0])
"""
Number of devices in site L71: 25
First Device:
{'devType': 'fw',
 'family': 'fortigate',
 'hostname': 'L71FW13-HA2/root',
 'id': '1137600732',
 ...}
 """

Note on Column Names

Finally, if you know the specific columns you would like to return instead of all columns that is also possible.  If you do not specify any columns the default behavior in the client will submit a malformed request to IP Fabric which will return an error message like:

'"columns[0]" must be one of [id, devicesCount, networksCount, rDCount, rDomains, routersCount, siteKey, siteName, siteUid, stpDCount, stpDomains, switchesCount, usersCount, vlanCount]'

Using Python and the re package I have stripped out the column names from the error message and then the client is able to submit a correctly formed request with all the column names.  Submitting a list of columns will increase performance as only one call to the API is performed instead of two.  It will also increase the speed and decrease memory usage as it will limit the amount of data being returned.

vendors = ipf.inventory.vendors.all(columns=["vendor"])

print(f"Number of vendors: {len(vendors)}")
[print(vendor["vendor"]) for vendor in vendors]
"""
Number of vendors: 14
arista
aws
checkpoint
cisco
...
"""

Querying Technology Data

In the above examples we looked at Inventory data but most other data lives in the technology tables.  It would also be possible to create a class like inventory which defines each table and their endpoints.  However, in the below examples I wanted to show different methods with interacting with the base client.

"""
get_technology.py
"""
from ipfabric import IPFClient
from pprint import pprint


if __name__ == '__main__':
    ipf = IPFClient('https://demo3.wordpress-625423-2416527.cloudwaysapps.com/')

    wc = ipf.fetch_all('tables/wireless/controllers')
    """
    Also acceptable:
    wc = ipf.fetch_all('https://demo3.wordpress-625423-2416527.cloudwaysapps.com/api/v1/tables/wireless/controllers')
    wc = ipf.fetch_all('/api/v1/tables/wireless/controllers')
    """
    print(f"Wireless Controllers Count: {len(wc)}")
    pprint(wc[0])
    """
    Wireless Controllers Count: 3
    {'apCount': 0,
     'clientCount': 0,
     'controller': 'HWLAB-WLC-C4400',
    ...}
    """

The fetch_all method is similar to the all method and takes the same arguments but it requires the API endpoint to submit to.  The above example fetches all the Wireless Controllers.  This will handle all the paging requests for you, if you prefer to implement paging within your own script then the fetch method is also available:

arp = ipf.fetch('tables/addressing/arp', limit=1, start=0, snapshot_id='$prev')

pprint(arp)
"""
[{'hostname': 'HWLAB-CKP-E-1',
  'id': '11490504891149050698',
  'intName': 'LAN1',
  'ip': '10.64.128.1',
  ...}]
"""

Finally, a query method is also available for use.  This takes either a string of JSON data or Python dictionary, this is especially useful for copying and pasting the API documentation directly from the browser.  It will default to use the pager method to retrieve all results and ignore the pagination fields in the payload, this can be changed by setting all=False.  Here are a few examples:

data = {
    "columns": [
        "id",
        "sn",
        "hostname",
        "siteKey",
        "siteName",
        "totalVlanCount",
        "activeVlanCount"
    ],
    "filters": {},
    "pagination": {
        "limit": 32,
        "start": 0
    },
    "snapshot": "d3bd033e-1ba6-4b27-86f5-18824a1a495e",
    "reports": "/technology/vlans/device-summary"
}
vlans = ipf.query('tables/vlan/device-summary', data)
print(f"Count of VLANS {len(vlans)}")
pprint(vlans[0])
"""
Count of VLANS 276
{'activeVlanCount': 11,
 'hostname': 'L34AC11',
 'id': '1119052963',
 'siteKey': '885963458',
 'siteName': 'L34',
 'sn': 'a22ff89',
 'totalVlanCount': 15}
"""
print()

with open('ospf.json', 'r') as f:
    ospf = ipf.query('tables/networks/routes', f.read(), all=False)
print(f"Count of routes {len(ospf)}")
pprint(ospf[0])
"""
Count of routes 33
{'hostname': 'L1R16',
 'id': '1118750250',
 'network': '10.71.0.0/16',
 'nexthop': [{'ad': 150,
              'age': 773086,
              'intName': 'ge-0/0/2.0',
              ...}],
 'nhCount': 1,
 ...}
"""

Summary

Retrieving all data including technology tables is now simplified without requiring the need to know the column names of the tables.  In a production environment it is encouraged to use column names to increase performance and decrease the amount of data being returned. 

I hope you enjoyed this blog post and be on the lookout for Part 3: Webhooks.

Resources

Get IP Fabric

Request a demo and discover how to increase
your networks visibility & get better time efficiency.
Free Demo | Zero Obligation
Request a Demo
We're Hiring!
Join the Team and be part of the Future of Network Automation
Available Positions
IP Fabric, Inc.
115 BROADWAY, 5th Floor
NEW YORK NY, 10006
United States
This is a block of text. Double-click this text to edit it.
Phone : +1 (914) 752-2991
Email : [email protected]
IP Fabric s.r.o.
Kateřinská 466/40
Praha 2 - Nové Město, 120 00
Czech Republic
This is a block of text. Double-click this text to edit it.
Phone : +420 720 022 997
Email : [email protected]
IP Fabric, Inc. © 2022 All Rights Reserved