Skip to content

Lab 22.1 - Using Python requests with Arista eAPI

The goal of this lab is to practice interacting with the Arista eAPI from Python using the requests library, working your way from experimenting with commands to writing a complete script that builds a normalized LLDP neighbor list.

Task 1 - Interact with eAPI from Python

In this task, you will explore working with the Python requests module built to simplify working with HTTP-based APIs.

For this lab, you will use the four Arista switches.

Step 1-1

Verify you can ping the Arista switches by name.

ntc@ntc-training:~$ ping eos-spine1
ntc@ntc-training:~$ ping eos-spine2
ntc@ntc-training:~$ ping eos-leaf1
ntc@ntc-training:~$ ping eos-leaf2

Step 1-2

Enter the Python shell.

ntc@ntc-training:scripts$ python
Python 3.8.12 (default, Oct 13 2021, 09:22:51)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Step 1-3

Import the requests module while on the Python shell. In addition, import the object that simplified using authentication for REST APIs as well as the json module.

>>> import requests
>>> from requests.auth import HTTPBasicAuth
>>> import json
>>>

Step 1-4

Use help on requests. You will see a description of this Python package. You can press q to exit the help viewer.

>>> help(requests)

Help on package requests:

NAME
    requests

DESCRIPTION
    Requests HTTP Library
    ~~~~~~~~~~~~~~~~~~~~~

    Requests is an HTTP library, written in Python, for human beings.
    Basic GET usage:

       >>> import requests
       >>> r = requests.get('https://www.python.org')
       >>> r.status_code
       200
       >>> b'Python is a programming language' in r.content
       True

    ... or POST:

       >>> payload = dict(key1='value1', key2='value2')
       >>> r = requests.post('https://httpbin.org/post', data=payload)
       >>> print(r.text)
       {
         ...
         "form": {
           "key1": "value1",
           "key2": "value2"
         },
         ...
       }

    The other HTTP methods are supported - see `requests.api`. Full documentation
    is at <https://requests.readthedocs.io>.

    :copyright: (c) 2017 by Kenneth Reitz.
    :license: Apache 2.0, see LICENSE for more details.

# output omitted

You can also run dir(requests) to see available attributes and built-in methods.

Step 1-5

On your jump-host desktop, navigate to the EOS Command Explorer in a browser.

Set the format to json and enter the command "show version" into the Commands box.

You should see the following Request object in the Request Viewer:

{
  "jsonrpc": "2.0",
  "method": "runCmds",
  "params": {
    "format": "json",
    "timestamps": false,
    "autoComplete": false,
    "expandAliases": false,
    "cmds": [
      "show version"
    ],
    "version": 1
  },
  "id": "EapiExplorer-1"
}

This is the object we need to send to the device. We'll use this in an upcoming step.

Step 1-6

Create four new variables while on the Python shell: auth, headers, payload, and url.

auth should be equal to HTTPBasicAuth('ntc', 'ntc123')

headers should be equal to headers = { 'Content-Type': 'application/json' }

payload should be equal to the Request object you copied above.

url should be url = 'http://eos-spine1/command-api' - this needs the command-api appended to the switch name or IP to work.

The summary up until this point is the following:

Note: there is no need to format the dictionaries as shown below. It is done for readability.

>>> import requests
>>> import json
>>> from requests.auth import HTTPBasicAuth
>>>
>>> auth = HTTPBasicAuth('ntc', 'ntc123')
>>> headers = {
...     'Content-Type': 'application/json'
... }
>>>
>>> payload = {
...     "jsonrpc": "2.0",
...     "method": "runCmds",
...     "params": {
...         "format": "json",
...         "timestamps": False,
...         "cmds": [
...             "show version"
...         ],
...         "version": 1
...     },
...     "id": "EapiExplorer-1"
... }
>>>
>>> url = 'http://eos-spine1/command-api'
>>>

At this point, we are ready to make a web API call to the Arista switch. Remember the Arista switch only supports HTTP POSTs even though we are getting data back. This is why it's a REST-like API.

Step 1-7

Make the API call to the device using the post method of requests as shown below.

>>> response = requests.post(url, data=json.dumps(payload), headers=headers, auth=auth)
>>>

This made the API call and returned data back stored as response.

You can verify the type of response and see it's still a Requests object:

>>> type(response)
<class 'requests.models.Response'>
>>>

Step 1-8

Let's now explore key attributes of response.

First, validating the API call was successful. If it was, then we should see an HTTP status code of "200" as the value for the status_code.

>>> response.status_code
200
>>>

Now, let's see the actual response from the switch using the text attribute.

>>> rsp = response.text
>>>
>>> type(rsp)
<class 'str'>
>>>

Now Print it out:

Note: if you use the print statement, you actually can't tell it's a string. This is critical to understand because you may think it's a dictionary.

>>> rsp
'{"jsonrpc": "2.0", "id": "EapiExplorer-1", "result": [{"memTotal": 2014520, "uptime": 609624.65, "modelName": "vEOS", "internalVersion": "4.22.4M-15583082.4224M", "mfgName": "", "serialNumber": "", "systemMacAddress": "52:54:00:5d:0e:bb", "bootupTimestamp": 1636026477.0, "memFree": 1333220, "version": "4.22.4M", "architecture": "i686", "isIntlVersion": false, "internalBuildId": "08527907-ec51-458e-99dd-e3ad9c80cbbd", "hardwareRevision": ""}]}'
>>>

Step 1-9

Load the response JSON string and convert it to a dictionary:

>>> data = json.loads(response.text)
>>>

Perform a type check:

>>> type(data)
<class 'dict'>
>>>

Step 1-10

Print the dictionary using json.dumps:

>>> print(json.dumps(data, indent=4))
{
    "jsonrpc": "2.0",
    "id": "EapiExplorer-1",
    "result": [
        {
            "memTotal": 2014520,
            "uptime": 609624.65,
            "modelName": "vEOS",
            "internalVersion": "4.22.4M-15583082.4224M",
            "mfgName": "",
            "serialNumber": "",
            "systemMacAddress": "52:54:00:5d:0e:bb",
            "bootupTimestamp": 1636026477.0,
            "memFree": 1333220,
            "version": "4.22.4M",
            "architecture": "i686",
            "isIntlVersion": false,
            "internalBuildId": "08527907-ec51-458e-99dd-e3ad9c80cbbd",
            "hardwareRevision": ""
        }
    ]
}
>>>

Step 1-11

Print the system MAC address.

>>> print(data['result'][0]['systemMacAddress'])
52:54:00:5d:0e:bb
>>>

Step 1-12

Extract everything from the actual output in a variable first and then print the system MAC address again.

>>> result = data['result'][0]
>>>
>>> print(result.get('systemMacAddress'))
52:54:00:5d:0e:bb
>>>

Saving everything under result as its own variable streamlines accessing data if you need to extract multiple key-value pairs.

Step 1-13

Use the command show vlan brief to get all vlans back from the device.

Print the JSON object using json.dumps out when complete.

>>> payload = {
...     "jsonrpc": "2.0",
...     "method": "runCmds",
...     "params": {
...         "format": "json",
...         "timestamps": False,
...         "cmds": [
...             "show vlan brief"
...         ],
...         "version": 1
...     },
...     "id": "EapiExplorer-1"
... }
>>>
>>> response = requests.post(url, data=json.dumps(payload), headers=headers, auth=auth)
>>>
>>> data = json.loads(response.text)
>>>
>>> print(json.dumps(data, indent=4))

Step 1-14

Save the VLAN object as a new variable called vlans.

>>> vlans = data['result'][0]
>>>

Step 1-15

Print out the vlan name for VLAN 1.

>>> print(vlans['vlans']['1']['name'])
default

You should see that this is quite the nested dictionary and the work from Module 1 is extremely helpful for working with REST APIs returning JSON data.

Task 2 - Gather Neighbors Script

In this task, you will write a script that queries two Arista EOS switches for their LLDP neighbors.

The final data structure should be a dictionary. Each key will be the hostname of the device. The value will be a list of dictionaries - each of these dictionaries should have the following keys: neighbor_interface, neighbor, and local_interface.

Before you query both devices and create the final script, you will start with testing on the Python shell.

Step 2-1

Run the command show lldp neighbors for eos-spine1. Store the "JSON" results in a variable called data and print it using json.dumps.

You'll notice this process becomes repetitive, so you'd want to store a few of these statements in a re-usable object like a function if you wanted to use this for production.

>>>
>>> payload = {
...      "jsonrpc": "2.0",
...      "method": "runCmds",
...      "params": {
...          "format": "json",
...          "timestamps": False,
...          "cmds": [
...              "show lldp neighbors"
...          ],
...          "version": 1
...      },
...      "id": "EapiExplorer-1"
... }
>>>
>>> response = requests.post(url, data=json.dumps(payload), headers=headers, auth=auth)
>>>
>>> data = json.loads(response.text)
>>>
>>> print(json.dumps(data, indent=4))
{
    "jsonrpc": "2.0",
    "id": "EapiExplorer-1",
    "result": [
        {
            "tablesLastChangeTime": 1636026622.4149334,
            "tablesAgeOuts": 0,
            "tablesInserts": 2,
            "lldpNeighbors": [
                {
                    "ttl": 120,
                    "neighborDevice": "eos-leaf1.ntc.com",
                    "neighborPort": "Ethernet2",
                    "port": "Ethernet2"
                },
                {
                    "ttl": 120,
                    "neighborDevice": "eos-leaf2.ntc.com",
                    "neighborPort": "Ethernet2",
                    "port": "Ethernet3"
                }
            ],
            "tablesDeletes": 0,
            "tablesDrops": 0
        }
    ]
}
>>>

We can see that eos-spine1 has 2 neighbor entries pointing to the 2 leaf switches.

We can also see the keys returned from the device do not match the keys we want for this lab. We need to map neighborDevice to neighbor, neighborPort to neighbor_interface, and port to local_interface.

Step 2-2

Extract the neighbors object from data and save it as lldp_neighbors.

>>> lldp_neighbors = data['result'][0]['lldpNeighbors']
>>>

There are two ways we can go about mapping the current dictionary keys to the desired keys. We can use conditional if statements for each key or create a dictionary that maps them for us that provides a bit more scale. Let's use the first option.

Step 2-3

Now create a new list that will store the new dictionary with the new values.

This list will be called neighbors_list.

>>> neighbors_list = []

Step 2-4

Loop through each neighbor in lldp_neighbors. For each iteration, you will create a dictionary that will be appended to neighbors_list.

While building this dictionary, you will also convert keys as described above.

for neighbor in lldp_neighbors:
    new_neighbor = {
        "neighbor_interface": neighbor["neighborPort"],
        "local_interface": neighbor["port"],
        "neighbor": neighbor["neighborDevice"]
    }
    neighbors_list.append(new_neighbor)

Step 2-5

Pretty print neighbors_list.

>>> print(json.dumps(neighbors_list, indent=4))
[
    {
        "neighbor_interface": "Ethernet2",
        "local_interface": "Ethernet2",
        "neighbor": "eos-leaf1.ntc.com"
    },
    {
        "neighbor_interface": "Ethernet2",
        "local_interface": "Ethernet3",
        "neighbor": "eos-leaf2.ntc.com"
    }
]

Step 2-6 - Challenge Exercise

This is an optional challenge, try to solve it on your own if you have some spare time! A suggested solution is available below.

Use the previous steps to build a script that outputs neighbors for eos-spine1 and eos-spine2 as such:

ntc@ntc-training:python$ python eos_neighbors.py
{
    "eos-spine1": [
        {
            "neighbor_interface": "Ethernet2",
            "local_interface": "Ethernet2",
            "neighbor": "eos-leaf1.ntc.com"
        },
        {
            "neighbor_interface": "Ethernet2",
            "local_interface": "Ethernet3",
            "neighbor": "eos-leaf2.ntc.com"
        }
    ],
    "eos-spine2": [
        {
            "neighbor_interface": "Ethernet3",
            "local_interface": "Ethernet2",
            "neighbor": "eos-leaf1.ntc.com"
        },
        {
            "neighbor_interface": "Ethernet3",
            "local_interface": "Ethernet3",
            "neighbor": "eos-leaf2.ntc.com"
        }
    ]
}

Stop scrolling if you don't want to see the solution.

.
























.

Step 2-7 - Solution

Here is an example of a working script:

There is no need to parameterize the command being sent or use functions, but this should give you a good idea on how to start coding, and adding modularity, as you re-factor and optimize. Even the script below could be modularized more. Remember, this is just for learning purposes.

import requests
import json
from requests.auth import HTTPBasicAuth


def eapi_request(device, commands):
    auth = HTTPBasicAuth("ntc", "ntc123")
    headers = {"Content-Type": "application/json"}

    url = f"http://{device}/command-api"
    payload = {
        "jsonrpc": "2.0",
        "method": "runCmds",
        "params": {
            "format": "json",
            "timestamps": False,
            "cmds": commands,
            "version": 1,
        },
        "id": "EapiExplorer-1",
    }

    response = requests.post(
        url, data=json.dumps(payload), headers=headers, auth=auth
    )
    return response


def get_eos_neighbors(response):

    data = json.loads(response.text)

    device_neighbors = data["result"][0]["lldpNeighbors"]

    neighbors_list = []
    for neighbor in device_neighbors:
        new_neighbor = {
            "neighbor_interface": neighbor["neighborPort"],
            "local_interface": neighbor["port"],
            "neighbor": neighbor["neighborDevice"],
        }
        neighbors_list.append(new_neighbor)

    return neighbors_list


def main():

    neighbors = {}

    devices = ["eos-spine1", "eos-spine2"]
    commands = ["show lldp neighbors"]
    for dev in devices:
        response = eapi_request(dev, commands)
        neighbors[dev] = get_eos_neighbors(response)

    print(json.dumps(neighbors, indent=4))


if __name__ == "__main__":
    main()

And there you have it. A complete script to go out and collect neighbor information from a Arista DC network.