Cisco DNA Center - Command Runner (Python)

DNAC Series

This is part of a DNAC series:

Disclaimer: the code in this post is not production-grade code obviously. The examples in the post are merely conceptual and for informational purposes.

Introduction

In this blog post, we have been showing some POSTMAN samples for running a Network Discovery and Command Runner. In the next sections, we will explore how we can implement the CommandRunner functionality using Python Requests. I recommend you to first go through the POSTMAN post before attempting this one.

Note about equipment

In this post, I’m using my own DNAC in my lab. However, if you want to follow along, you could also use a Cisco sandbox environment delivered by Cisco Devnet. To get a list of all sandboxes, check out this link. For this tutorial, you could use this one. Note that this is a reservable instance as the always-on is restricted in functionality.

CommandRunner

See also the previous post towards the bottom to see the same flow using Postman screenshots.

Get list of devices

First, we will retrieve the list of devices onto which we would like to run our command. So we will call the /api/v1/network-device endpoint but we’ll pass in a family parameter as we only want to execute the command on devices in the ‘switches’ family. We will store all device id’s in a list as we will need to pass that list in the JSON body of the Command Runner API. The command we will run against the devices in our list, will be show ip interface brief.

A quick note, in the lab DNAC I’m using I need to use /api/v1/network-device instead of /dna/intent/api/v1/network-device. Not really sure why, I think it’s a bug. In your code, you should be able to use /dna/intent/api/v1/network-device without any issue.

def main():
    family = "Switches and Hubs"

    device_url = url + "/api/v1/network-device?family=" +  family
    response =  requests.get(device_url, headers=headers, verify=False ).json()
    devices = response["response"]

    device_list = []

    # Store all device ids in a list
    for device in devices:
        device_list.append(device['id'])

    # Pass the device list into the payload
    payload = {
        "commands": [
            "show ip interface brief"
        ],
        "deviceUuids": device_list
    }
Run command

Next, let’s actually run the command. We’ll need to do a POST request to the /api/v1/network-device-poller/cli/read-request endpoint and pass along the JSON body we created earlier. This JSON body contained the command to execute as well as the list of device (id’s) to execute the command against.

A quick note, in the lab DNAC I’m using I need to use /api/v1/network-device-poller/cli/read-request instead of /dna/intent/v1/network-device-poller/cli/read-request. Not really sure why, I think it’s a bug. In your code, you should be able to use /dna/intent/v1/network-device-poller/cli/read-request without any issue.

We will get back a (task) url as part of the response:

    command_url = url + "/api/v1/network-device-poller/cli/read-request"
    response = requests.post(command_url, headers=headers, data=json.dumps(payload), verify=False ).json()
Check task

Also this API is asynchonous, so we will get back a response containing the taskId and the url which we can use for further processing.

    task_url = response['response']['url']
    task = waitTask(url, task_url )
    fileId = json.loads(task['response']['progress'])

Therefore, I implemented a waitTask function that essentually checks every second the tasks API and checks the response. If there is an endTime in the response then we can finish the polling.

def waitTask(url, task_url):
   for i in range(10):
      time.sleep(1)
      response_task =  requests.get(url + task_url, headers=headers, verify=False ).json()
      if response_task['response']['isError']:
         print("Error")
      if "endTime" in response_task['response']:
         return response_task
Check result

The tasks API will provide a response with a fileId. This fileId points to a file containing the output of the command.

processFile(url, fileId['fileId'])

I have created a processFile method that calls the /dna/intent/api/v1/file/{fileId} endpoint.

def processFile(url, fileid):
    file_url = url + f"/api/v1/file/{fileid}"
    print(f"FileURL: {file_url}")
    response = requests.get(file_url, headers=headers, verify=False ).json()
    print(response[0]['commandResponses']['SUCCESS']['show ip interface brief'])

This API will show the results of our command. See below for an example:

wauterw@WAUTERW-M-65P7 CommandRunner % python3 commandrunner.py
The file id is 9f6b1061-c8d4-47cb-804a-082415f3495e
FileURL: https://10.48.82.183/dna/intent/api/v1/file/9f6b1061-c8d4-47cb-804a-082415f3495e
show ip interface brief
Interface              IP-Address      OK? Method Status                Protocol
Vlan1                  unassigned      YES manual up                    up      
Vlan23                 192.168.23.60   YES NVRAM  up                    up      
Vlan3011               192.168.11.25   YES manual up                    up      
Vlan3012               192.168.11.29   YES manual up                    up      
Vlan3013               192.168.11.33   YES manual up                    up      
GigabitEthernet0/0     10.48.172.60    YES NVRAM  up                    up      
GigabitEthernet1/0/1   192.168.13.66   YES manual up                    up      
GigabitEthernet1/0/2   unassigned      YES unset  administratively down down    
GigabitEthernet1/0/3   unassigned      YES unset  down                  down    
GigabitEthernet1/0/4   unassigned      YES unset  down                  down    
GigabitEthernet1/0/5   unassigned      YES unset  down                  down    
GigabitEthernet1/0/6   unassigned      YES unset  down                  down    
GigabitEthernet1/0/7   unassigned      YES unset  down                  down    
GigabitEthernet1/0/8   unassigned      YES unset  down                  down    
GigabitEthernet1/0/9   unassigned      YES unset  down                  down    
GigabitEthernet1/0/10  unassigned      YES unset  down                  down    
GigabitEthernet1/0/11  unassigned      YES unset  down                  down    
GigabitEthernet1/0/12  unassigned      YES unset  down                  down    
GigabitEthernet1/0/13  unassigned      YES unset  down                  down    
GigabitEthernet1/0/14  unassigned      YES unset  down                  down    
GigabitEthernet1/0/15  unassigned      YES unset  down                  down    
GigabitEthernet1/0/16  unassigned      YES unset  down                  down    
GigabitEthernet1/0/17  unassigned      YES unset  down                  down    
GigabitEthernet1/0/18  unassigned      YES unset  down                  down    
GigabitEthernet1/0/19  unassigned      YES unset  down                  down    
GigabitEthernet1/0/20  unassigned      YES unset  down                  down    
GigabitEthernet1/0/21  unassigned      YES unset  down                  down    
GigabitEthernet1/0/22  unassigned      YES unset  up                    up      
GigabitEthernet1/0/23  unassigned      YES unset  up                    up      
GigabitEthernet1/0/24  unassigned      YES unset  up                    up      
GigabitEthernet1/1/1   unassigned      YES unset  up                    up      
GigabitEthernet1/1/2   unassigned      YES unset  down                  down    
GigabitEthernet1/1/3   unassigned      YES unset  down                  down    
GigabitEthernet1/1/4   unassigned      YES unset  up                    up      
Te1/1/1                unassigned      YES unset  down                  down    
Te1/1/2                unassigned      YES unset  down                  down    
Te1/1/3                unassigned      YES unset  down                  down    
Te1/1/4                unassigned      YES unset  down                  down    
LISP0                  unassigned      YES unset  up                    up      
LISP0.4097             192.168.30.60   YES unset  up                    up      
LISP0.4099             172.16.100.1    YES unset  up                    up      
LISP0.4100             192.168.11.29   YES unset  up                    up      
LISP0.4101             unassigned      YES unset  deleted               down    
Loopback0              192.168.30.60   YES NVRAM  up                    up      
Loopback1042           172.16.100.1    YES manual up                    up      
Loopback1044           172.16.96.1     YES manual up                    up      
Loopback1045           172.16.99.1     YES manual up                    up      
Loopback2045           192.168.18.1    YES manual up                    up      

I shared the code in bits and pieces, if you want to see the final Python script, check out the Github repo here.