Querying Cisco DNAC using REST APIs and Python

After copious amounts of blood, sweat and tears, I managed to compile a script which allowed me to enter a MAC Address and query Cisco DNA Center using REST APIs to return a list of useful information such as:

---------------
Switch Details:
---------------
Connected to: [SWITCH-NAME] | [SWITCH-IP]
Switch Model: [MODEL]
Switch Location: [LOCATION]

---------------
Device Details:
---------------
Port: [PORT]
VLAN: [VLAN]
IP Address: [IP-ADDRESS]

Sure, this information can be found via DNAC Assurance on the GUI anyway, but what Engineer doesnt jump at the chance to do something just for the sake of it and pull their hair out at something trying to get it to work?

Caveats

  • MAC Addresses must be in the format XX:XX:XX:XX:XX:XX
  • Credentials will be sent unencrypted over HTTP, I didn’t get around to securing this yet
  • Errors will not seem very helpful or descriptive, but should mean that the data is simply not known by DNAC
  • The API used can also query for similar information using an IP Address, but I did not configure that as part of my script. MAC Address input only at present

The Script

For any Python/REST API experts who just want the script, here it is in its entirety. If you want a walkthrough of the logic behind it however, stick around and scroll down.

import requests
from requests.auth import HTTPBasicAuth
import json

print("Enter the MAC Address of the device you wish to track [XX:XX:XX:XX:XX:XX]:")
Device_MAC = input()

requests.packages.urllib3.disable_warnings()

url = "https://DNAC-IP-ADDRESS/dna/system/api/v1/auth/token"

headers_dict = {

    "content-type": "application/json",
    "entity_type": "mac_address",
    "entity_value": str(Device_MAC),

    }

resp = requests.post(url, auth=HTTPBasicAuth(username='USERNAME', password='PASSWORD'), headers=headers_dict, verify=False)

token = resp.json()['Token']

headers_dict['x-auth-token'] = token

reply = requests.get("https://DNAC-IP-ADDRESS/dna/intent/api/v1/client-enrichment-details/", headers=headers_dict, verify=False)

data = reply.json()

data2 = json.dumps(data, sort_keys=True, indent=4)

Switch_Name = (data[0]["connectedDevice"][0]["deviceDetails"]["hostname"])
Switch_IP = (data[0]["connectedDevice"][0]["deviceDetails"]["managementIpAddress"])
Switch_Type = (data[0]["connectedDevice"][0]["deviceDetails"]["type"])
Switch_Location = (data[0]["userDetails"]["location"])
Device_IP = (data[0]["userDetails"]["hostIpV4"])
Device_Port = (data[0]["userDetails"]["port"])
Device_VLAN = (data[0]["userDetails"]["vlanId"])

print(
    "\n" + "---------------" +
    "\n" + "Switch Details:" +
    "\n" + "---------------" +
    "\n" + "Connected to: " + Switch_Name + " | " + Switch_IP +
    "\n" + "Switch Model: " + Switch_Type +
    "\n" + "Switch Location: " + Switch_Location +
    "\n" +
    "\n" + "---------------" +
    "\n" + "Device Details:" +
    "\n" + "---------------" +
    "\n" + "Port: " + Device_Port +
    "\n" + "VLAN: " + str(Device_VLAN) +
    "\n" + "IP Address: " + Device_IP
)

Script Walkthrough

Still here? Great.

Let’s walk through the script step by step.

Importing Required Libraries

We begin by importing all the required libraries which Python will use to talk to, communicate with and output data from DNAC in a form we are happy with.

import requests
from requests.auth import HTTPBasicAuth
import json

‘Requests’ is a library which will allow us to use REST APIs within our script. JSON is another useful library that will (as you may have guessed) allow Python to work with JSON structured data.

User MAC Address Query

The first and only interaction we will have with the script will be to enter the MAC Address of the device we want to ask DNAC about information for:

print("Enter the MAC Address of the device you wish to track [XX:XX:XX:XX:XX:XX]:")
Device_MAC = input()

The script will prompt us for input and expect the format listed. It will then name this MAC Address as the variable ‘Device_MAC’.

Disable Warnings

If we don’t have a valid Certificate on our DNAC Server (for whatever reason), then this function will allow us to ignore them and continue the script as normal without clogging up the terminal with error messages.

requests.packages.urllib3.disable_warnings()

Set Token URL

The DNAC API is great, but you cant just use it freely without authenticating first! To make things a little neater, I defined the URL to be used for requesting an authenticating token and gave it an extremely creative name:

url = "https://DNAC-IP-ADDRESS/dna/system/api/v1/auth/token"

Note that you’ll have to enter your own IP Address of the DNAC server you are accessing.

Define REST Headers

When we send our GET Request to DNAC, there are some parameters we need to define in the Headers of the Request.

headers_dict = {

    "content-type": "application/json",
    "entity_type": "mac_address",
    "entity_value": str(Device_MAC),

    }

I’ve chosen to include them as part of a single Python dictionary, setting the content type to be in JSON format, and telling DNAC that I am providing a MAC Address with the string value of whatever we chose to enter earlier on.

Request Authentication Token from DNAC

Now we can use our DNAC credentials to request a token so that our Script can work. We are including our specified Headers from previous steps and are telling our script to ignore any errors regarding self-signed Certificates that would otherwise cause it to fail.

resp = requests.post(url, auth=HTTPBasicAuth(username='USERNAME', password='PASSWORD'), headers=headers_dict, verify=False)

The reply received has been assigned the variable ‘resp’.

Why use a token and just not use HTTP authentication everywhere? To be honest I don’t have a solid answer, especially if you’re using this for a lab environment. But if you want to use this anywhere else in production, I can imagine they wouldn’t want credentials to be exposed.

Ironically, the credentials used here could potentially be seen by anyone sniffing traffic, so if anyone wants to improve this script and secure it, please be my guest. Feedback and suggestions are always welcome.

Extract Token from DNAC Response

The below code will extract the relevant Token information from the DNAC response and assign it the variable ‘token’:

token = resp.json()['Token']

Add Token to Headers

We can then dynamically add our newly acquired Authentication Token to our previous dictionary for use in our request Headers.

headers_dict['x-auth-token'] = token

Alright, we’ve set up everything we need to get started, let’s start pulling some information!

Request Information from DNAC

Using the requests library we initially imported, I am able to make a REST API GET Request to DNAC’s specific URL which is able to provide this information. Information regarding other URLs can be found on Cisco DevNet documentation here.

reply = requests.get("https://DNAC-IP-ADDRESS/dna/intent/api/v1/client-enrichment-details/", headers=headers_dict, verify=False)

Once more I am referencing the Headers I defined earlier (with the newly acquired Token) and disabling any errors that would stop my script from running if there were any issues with the self-signed certificate.

Format Received Data

Behind the scenes, we would have received a chunk of data from Cisco DNA Center; before we ask Python to decode it and extract what we want, we need to format it so that Python can understand it.

data = reply.json()

data2 = json.dumps(data, sort_keys=True, indent=4)

The above code tells Python that the reply is in JSON format, and then formats the data in the expected JSON way. I’m certainly not an expert in Python/REST APIs let alone JSON, so this is as far as I was able to understand it.

Kindly taken from the same Cisco DevNet page as before, here is an *example* of the format the received data will be in:

[
    {
        "userDetails": {
            "id": null,
            "connectionStatus": null,
            "hostType": null,
            "userId": null,
            "hostName": {},
            "hostOs": {},
            "hostVersion": {},
            "subType": {},
            "lastUpdated": null,
            "healthScore": [
                {
                    "healthType": null,
                    "reason": null,
                    "score": null
                }
            ],
            "hostMac": null,
            "hostIpV4": null,
            "hostIpV6": [ null ],
            "authType": {},
            "vlanId": null,
            "ssid": {},
            "location": {},
            "clientConnection": null,
            "connectedDevice": [ null ],
            "issueCount": null,
            "rssi": {},
            "snr": {},
            "dataRate": {},
            "port": {}
        },
        "connectedDevice": [
            {
                "deviceDetails": {
                    "family": null,
                    "type": null,
                    "location": {},
                    "errorCode": null,
                    "macAddress": null,
                    "role": null,
                    "apManagerInterfaceIp": null,
                    "associatedWlcIp": null,
                    "bootDateTime": {},
                    "collectionStatus": null,
                    "interfaceCount": {},
                    "lineCardCount": {},
                    "lineCardId": {},
                    "managementIpAddress": null,
                    "memorySize": null,
                    "platformId": null,
                    "reachabilityFailureReason": null,
                    "reachabilityStatus": null,
                    "snmpContact": null,
                    "snmpLocation": null,
                    "tunnelUdpPort": null,
                    "waasDeviceMode": {},
                    "series": null,
                    "inventoryStatusDetail": null,
                    "collectionInterval": null,
                    "serialNumber": null,
                    "softwareVersion": null,
                    "roleSource": null,
                    "hostname": null,
                    "upTime": null,
                    "lastUpdateTime": null,
                    "errorDescription": {},
                    "locationName": {},
                    "tagCount": null,
                    "lastUpdated": null,
                    "instanceUuid": null,
                    "id": null,
                    "neighborTopology": [
                        {
                            "nodes": [
                                {
                                    "role": null,
                                    "name": null,
                                    "id": null,
                                    "description": null,
                                    "deviceType": {},
                                    "platformId": {},
                                    "family": {},
                                    "ip": {},
                                    "softwareVersion": {},
                                    "userId": {},
                                    "nodeType": {},
                                    "radioFrequency": {},
                                    "clients": null,
                                    "count": {},
                                    "healthScore": {},
                                    "level": null,
                                    "fabricGroup": {}
                                }
                            ],
                            "links": [
                                {
                                    "source": null,
                                    "linkStatus": null,
                                    "label": [
                                        null
                                    ],
                                    "target": null,
                                    "id": {},
                                    "portUtilization": {}
                                }
                            ]
                        }
                    ],
                    "cisco360view": null
                }
            }
        ],
        "issueDetails": {
            "issue": [
                {
                    "issueId": null,
                    "issueSource": null,
                    "issueCategory": null,
                    "issueName": null,
                    "issueDescription": null,
                    "issueEntity": null,
                    "issueEntityValue": null,
                    "issueSeverity": null,
                    "issuePriority": null,
                    "issueSummary": null,
                    "issueTimestamp": null,
                    "suggestedActions": [
                        {
                            "message": null,
                            "steps": [ null ]
                        }
                    ],
                    "impactedHosts": [
                        {
                            "hostType": null,
                            "hostName": null,
                            "hostOs": null,
                            "ssid": null,
                            "connectedInterface": null,
                            "macAddress": null,
                            "failedAttempts": null,
                            "location": {
                                "siteId": null,
                                "siteType": null,
                                "area": null,
                                "building": null,
                                "floor": {},
                                "apsImpacted": [
                                    null
                                ]
                            },
                            "timestamp": null
                        }
                    ]
                }
            ]
        }
    }
]

All that remains is to extract what we want and print it out so we are presented with it nice and neatly. Presentation is just as important.

Define Relevant Variables

One by one, we can define each variable we are interested in, select it from the JSON output and give it a neat and tidy name for use later. In this case we are interested in:

  • Switch Name
  • Switch IP Address
  • Switch Type
  • Switch Location
  • Device IP Address
  • Device Port
  • Device VLAN

To select and extract these variables from the output received from DNAC, I used the below code:

Switch_Name = (data[0]["connectedDevice"][0]["deviceDetails"]["hostname"])
Switch_IP = (data[0]["connectedDevice"][0]["deviceDetails"]["managementIpAddress"])
Switch_Type = (data[0]["connectedDevice"][0]["deviceDetails"]["type"])
Switch_Location = (data[0]["userDetails"]["location"])
Device_IP = (data[0]["userDetails"]["hostIpV4"])
Device_Port = (data[0]["userDetails"]["port"])
Device_VLAN = (data[0]["userDetails"]["vlanId"])

Now remains the final stage, the technical aspect is done – all that is left is presenting the result to the CLI for human readability!

Presenting the Final Results

The below code prints out the variables we listed at the very top of this post and that we configured in the previous step into a nice and neat, human-friendly format.

print(
    "\n" + "---------------" +
    "\n" + "Switch Details:" +
    "\n" + "---------------" +
    "\n" + "Connected to: " + Switch_Name + " | " + Switch_IP +
    "\n" + "Switch Model: " + Switch_Type +
    "\n" + "Switch Location: " + Switch_Location +
    "\n" +
    "\n" + "---------------" +
    "\n" + "Device Details:" +
    "\n" + "---------------" +
    "\n" + "Port: " + Device_Port +
    "\n" + "VLAN: " + str(Device_VLAN) +
    "\n" + "IP Address: " + Device_IP
)

Here we made use of the new line character code to ensure the information isn’t all printed onto the same line, and to present the details so that it is easy on the eyes.

That’s it! We made it this far and you didn’t even break a sweat!

We should now receive the data in the format below like we saw in the beginning of this post:

---------------
Switch Details:
---------------
Connected to: [SWITCH-NAME] | [SWITCH-IP]
Switch Model: [MODEL]
Switch Location: [LOCATION]

---------------
Device Details:
---------------
Port: [PORT]
VLAN: [VLAN]
IP Address: [IP-ADDRESS]

Summary

Running this script will prompt us for a MAC Address, providing one will query DNAC for information regarding where it is on the Network, what IP it has and some other bits of basic configuration helpful to any Network Engineer.

This script acted as a mini crash-course for me personally, I learnt ad-hoc about REST APIs, Tokens and honed my Python skills a bit more. Even though the information is already available on DNAC, querying stuff using APIs is cool and there’s no doubt it is a valuable skill to have.

I hope posting this script on the Internet will help someone or point them in the right direction to accomplish something else.



Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Ads