Device Provisioning Guide

Introduction

Use the Catalyst Center API to provision configurations to the devices that it manages. It supports network configurations specific to the site's devices and allows deploying templates to devices. These templates support variables that you can replace during deployment.

Goal

The goals of this guide are:

  1. Assign a device to a site.
  2. Provision network settings for the site.
  3. Create a configuration template project.
  4. Create a template within the project.
  5. Provision a device with the template.

Device provisioning workflow

Endpoints and methods used

  • POST /dna/system/api/v1/auth/token
  • GET /dna/intent/api/v1/site
  • GET /dna/intent/api/v1/network-device/serial-number/{serial_number}
  • POST /dna/intent/api/v1/network/{site_id}
  • POST /dna/intent/api/v1/site/{site_id}/device
  • GET /dna/intent/api/v1/template-programmer/project
  • POST /dna/intent/api/v1/template-programmer/project/{project_id}/template
  • GET /dna/intent/api/v1/task/{task_id}
  • POST /dna/intent/api/v1/template-programmer/template/version
  • POST /dna/intent/api/v1/template-programmer/template/deploy

Prerequisites

For this guide, we recommend that the developer becomes familiar with authenticating to the Catalyst Center API, and managing Sites, and Devices.

Environment

This guide was developed using:

Authentication

First, we must authenticate and retrieve a token from the API.

Note: Do not use verify=False or urllib3.disable_warnings() if you are not sure of its purpose. Read Authentication and Authorization.

import requests
from requests.auth import HTTPBasicAuth
import urllib3
urllib3.disable_warnings()

BASE_URL = 'https://<IP Address>'
AUTH_URL = '/dna/system/api/v1/auth/token'
USERNAME = '<USERNAME>'
PASSWORD = '<PASSWORD>'

response = requests.post(BASE_URL + AUTH_URL, auth=HTTPBasicAuth(USERNAME, PASSWORD), verify=False)
token = response.json()['Token']

Site and Device Information

With the token information, we proceed to retrieve the information of the Catalyst Center Guide Building site in the Catalyst Center and the information from the device with serial number 919L3GOS8QC.

SITE_URL = '/dna/intent/api/v1/site'
site_name = 'DNA Center Guide Building'
site_id = ''
response = requests.get(BASE_URL + SITE_URL,
                        headers=headers, verify=False)

for site in response.json()['response']:
    if site['name'] == site_name:
        site_id = site['id']

DEVICES_BY_SERIAL_URL = '/dna/intent/api/v1/network-device/serial-number/{serial_number}'
serial_number = '919L3GOS8QC'
response = requests.get(BASE_URL + DEVICES_BY_SERIAL_URL.format(serial_number=serial_number),
             headers=headers,
             verify=False)
device_ip = response.json()['response']['managementIpAddress']
device_name = response.json()['response']['hostname']

Network API

The Network API consists of 16 endpoints to create networks, assign settings, manage device credentials that are related to the network, and configure several other options.

In this guide, we use the Network API to create a network with default DHCP, DNS, and Syslog settings, which apply to devices in the network.

NETWORK_URL = '/dna/intent/api/v1/network/{site_id}'
network = {
    "settings": {
        "dhcpServer": [
            "172.30.200.5"
        ],
        "dnsServer": {
            "domainName": "devnet.local",
            "primaryIpAddress": "172.30.200.6",
            "secondaryIpAddress": "172.30.200.7"
        },
        "syslogServer": {
            "ipAddresses": [
                "10.255.0.1"
            ],
            "configureDnacIP": True
        }
    }
}
response = requests.post(BASE_URL + NETWORK_URL.format(site_id=site_id),
                         headers=headers, json=network,
                         verify=False)

Site API

Using Site API, it is possible to assign a device to a site:

SITE_DEVICE_URL = '/dna/intent/api/v1/site/{site_id}/device'
site_devices = {
    'device': device_ips
}
response = requests.post(BASE_URL + SITE_DEVICE_URL.format(site_id=site_id),
                            headers=headers, json=site_devices,
                            verify=False)

Configuration Template API

Use the Configuration Template API to interact with the Template Programmer functionality within DNA Center. It is a powerful API used to manage configuration projects, templates, and deploy templates to devices. Set template variables when you call the deployment endpoint.

Project Management

Projects group templates for easier management. We use the API to assign the device to the Onboarding Configuration Project, requiring the projectId.

TEMPLATE_PROJECT_URL = '/dna/intent/api/v1/template-programmer/project'
response = requests.get(BASE_URL + TEMPLATE_PROJECT_URL,
             headers=headers,
             verify=False)
project_name = "Onboarding Configuration"
response = get_configuration_template_project(headers)
project_id = ''
for project in response.json['response']:
    if project['name'] == project_name:
        project_id = project['id']

Template Management

Templates enable the developer to deploy configurations to the devices managed by Catalyst Center, keeping different versions if needed and making them more dynamic by using variables in the definition. In this guide, we create a template with two variables.

We will create a template and after that, we will retrieve the templateId to commit it, which makes it possible to use it to deploy to devices.

template_info = {
    "name": "DNA Center Guide",
    "description": "Guide Configuration Template",
    "tags": [],
    "deviceTypes": [
        {
            "productFamily": "Routers",
            "productSeries": "Cisco 1000 Series Integrated Services Routers"
        }
    ],
    "softwareType": "IOS-XE",
    "softwareVariant": "XE",
    "templateContent": "ip access-list extended $permitACLName\npermit ip 10.0.0.0 0.255.255.25.0 any\npermit ip 172.16.0.0 0.15.255.255 any\npermit ip 192.168.0.0 0.0.255.255 any\n!\n\nip access-list extended $denyACLName\ndeny ip 10.0.0.0 0.255.255.25.0 any\ndeny ip 172.16.0.0 0.15.255.255 any\ndeny ip 192.168.0.0 0.0.255.255 any\n!\n",
    "rollbackTemplateContent": "",
    "templateParams": [
        {
            "parameterName": "permitACLName",
            "dataType": "STRING",
            "defaultValue": None,
            "description": None,
            "required": True,
            "notParam": False,
            "paramArray": False,
            "displayName": None,
            "instructionText": None,
            "group": None,
            "order": 1,
            "selection": {
                "selectionType": None,
                "selectionValues": {},
                "defaultSelectedValues": []
            },
            "range": [],
            "key": None,
            "provider": None,
            "binding": ""
        },
        {
            "parameterName": "denyACLName",
            "dataType": "STRING",
            "defaultValue": None,
            "description": None,
            "required": True,
            "notParam": False,
            "paramArray": False,
            "displayName": None,
            "instructionText": None,
            "group": None,
            "order": 2,
            "selection": {
                "selectionType": None,
                "selectionValues": {},
                "defaultSelectedValues": []
            },
            "range": [],
            "key": None,
            "provider": None,
            "binding": ""
        }
    ],
    "rollbackTemplateParams": [],
    "composite": False,
    "containingTemplates": []
}

TEMPLATE_URL = '/dna/intent/api/v1/template-programmer/project/{project_id}/template'
response = requests.post(BASE_URL + TEMPLATE_URL.format(project_id=project_id),
                         headers=headers, json=template,
                         verify=False)

task_id = response.json()['response']['taskId']

time.sleep(3)
TASK_BY_ID_URL = '/dna/intent/api/v1/task/{task_id}'
response = requests.get(BASE_URL + TASK_BY_ID_URL.format(task_id=task_id),
                        headers=headers, verify=False)

template_id = response.json()['response']['data']

After creating the template, commit it to have a version suitable for deployment to a device.

template_version = {
    "comments": "DNAC Guide Initial Version",
    "templateId": template_id
}
TEMPLATE_VERSION_URL = '/dna/intent/api/v1/template-programmer/template/version'
response = requests.post(BASE_URL + TEMPLATE_VERSION_URL,
                         headers=headers, json=template_version,
                         verify=False)

Template deployment

Once you create and commit the template, it becomes available for deployment to a device, which you can also do using the Template API.

TEMPLATE_DEPLOY_URL = '/dna/intent/api/v1/template-programmer/template/deploy'
deployment_info = {
    "forcePushTemplate": False,
    "isComposite": False,
    "targetInfo": [
        {
            "hostName": device_name,
            "params": {
                "permitACLName": "GUIDE-ALLOW-ACL",
                "denyACLName": "GUIDE-DENY-ACL"
            },
            "type": "MANAGED_DEVICE_IP"
        }
    ],
    "templateId": template_id
}
response = requests.post(BASE_URL + TEMPLATE_DEPLOY_URL,
                         headers=headers, json=deployment_info,
                         verify=False)
print(response)

Code

The repository for this guide is here. The final code with functions appears as below.

# Authentication
BASE_URL = 'https://<IP Address>'
AUTH_URL = '/dna/system/api/v1/auth/token'
USERNAME = '<USERNAME>'
PASSWORD = '<PASSWORD>'

# URLs
DEVICES_BY_SERIAL_URL = '/dna/intent/api/v1/network-device/serial-number/{serial_number}'
NETWORK_URL = '/dna/intent/api/v1/network/{site_id}'
SITE_DEVICE_URL = '/dna/intent/api/v1/site/{site_id}/device'
SITE_URL = '/dna/intent/api/v1/site'
TASK_BY_ID_URL = '/dna/intent/api/v1/task/{task_id}'
TEMPLATE_DEPLOY_URL = '/dna/intent/api/v1/template-programmer/template/deploy'
TEMPLATE_PROJECT_URL = '/dna/intent/api/v1/template-programmer/project'
TEMPLATE_URL = '/dna/intent/api/v1/template-programmer/project/{project_id}/template'
TEMPLATE_VERSION_URL = '/dna/intent/api/v1/template-programmer/template/version'

# Get Authentication token
def get_dnac_jwt_token():
    response = requests.post(BASE_URL + AUTH_URL,
                             auth=HTTPBasicAuth(USERNAME, PASSWORD),
                             verify=False)
    token = response.json()['Token']
    return token

# Get list of sites
def get_sites(headers):
    response = requests.get(BASE_URL + SITE_URL,
                            headers=headers, verify=False)
    return response.json()['response']

# Get device by serial
def get_device_by_serial(headers, serial_number):
    response = requests.get(BASE_URL + DEVICES_BY_SERIAL_URL.format(serial_number=serial_number),
                            headers=headers,
                            verify=False)
    return response.json()['response']

# Add devices to site
def add_devices_site(headers, site_id, devices):
    headers['__runsync'] = 'true'
    headers['__runsynctimeout'] = '30'
    response = requests.post(BASE_URL + SITE_DEVICE_URL.format(site_id=site_id),
                             headers=headers, json=devices,
                             verify=False)
    return response.json()

# Create template configuration project
def create_network(headers, site_id, network):
    response = requests.post(BASE_URL + NETWORK_URL.format(site_id=site_id),
                             headers=headers, json=network,
                             verify=False)
    return response.json()

# Get template configuration project
def get_configuration_template_project(headers):
    response = requests.get(BASE_URL + TEMPLATE_PROJECT_URL,
                             headers=headers,
                             verify=False)
    return response.json()

# Create template
def create_configuration_template(headers, project_id, template):
    response = requests.post(BASE_URL + TEMPLATE_URL.format(project_id=project_id),
                             headers=headers, json=template,
                             verify=False)
    return response.json()['response']

# Create configuration template version
def create_configuration_template_version(headers, template_version):
    response = requests.post(BASE_URL + TEMPLATE_VERSION_URL,
                             headers=headers, json=template_version,
                             verify=False)
    return response.json()['response']

# Deploy template
def deploy_configuration_template(headers, deployment_info):
    response = requests.post(BASE_URL + TEMPLATE_DEPLOY_URL,
                             headers=headers, json=deployment_info,
                             verify=False)
    return response.json()

# Get Task result
def get_task(headers, task_id):
    response = requests.get(BASE_URL + TASK_BY_ID_URL.format(task_id=task_id),
                            headers=headers, verify=False)
    return response.json()['response']

def main():
    # obtain the Cisco DNA Center Auth Token
    token = get_dnac_jwt_token()
    headers = {'X-Auth-Token': token, 'Content-Type': 'application/json'}

    # Get Site ID
    site_name = 'DNA Center Guide Building'
    response = get_sites(headers)
    for site in response:
        if site['name'] == site_name:
            site_id = site['id']

    print('Printing site name "{site_name}" site id {site_id}'.format(site_name=site_name,
                                                                      site_id=site_id))

    # Get Device IP and Name using Serial Number
    serial_number = '919L3GOS8QC'
    response = get_device_by_serial(headers, serial_number)
    device_ip = response['managementIpAddress']
    device_name = response['hostname']
    device_ips = [device_ip]

    print('\nPrinting device serial {serial_number} device IP {ip}'.format(serial_number=serial_number,
                                                                           ip=device_ip))
    # Create Site Network
    network = {
        "settings": {
            "dhcpServer": [
                "172.30.200.5"
            ],
            "dnsServer": {
                "domainName": "devnet.local",
                "primaryIpAddress": "172.30.200.6",
                "secondaryIpAddress": "172.30.200.7"
            },
            "syslogServer": {
                "ipAddresses": [
                    "10.255.0.1"
                ],
                "configureDnacIP": True
            }
        }
    }
    response = create_network(headers, site_id, network)

    site_devices = {
        'device': device_ips
    }

    # Assign device to site
    response = add_devices_site(headers, site_id, site_devices)
    print(response['result']['progress'])

    # Get Project information
    project_name = "Onboarding Configuration"
    response = get_configuration_template_project(headers)
    project_id = ''
    for project in response:
        if project['name'] == project_name:
            project_id = project['id']

    # Create Configuration Template
    template_info = {
        "name": "DNA Center Guide",
        "description": "Guide Configuration Template",
        "tags": [],
        "deviceTypes": [
            {
                "productFamily": "Routers",
                "productSeries": "Cisco 1000 Series Integrated Services Routers"
            }
        ],
        "softwareType": "IOS-IOS",
        "softwareVariant": "IOS",
        "templateContent": "ip access-list extended $permitACLName\npermit ip 10.0.0.0 0.255.255.25.0 any\npermit ip 172.16.0.0 0.15.255.255 any\npermit ip 192.168.0.0 0.0.255.255 any\n!\n\nip access-list extended $denyACLName\ndeny ip 10.0.0.0 0.255.255.25.0 any\ndeny ip 172.16.0.0 0.15.255.255 any\ndeny ip 192.168.0.0 0.0.255.255 any\n!\n",
        "rollbackTemplateContent": "",
        "templateParams": [
            {
                "parameterName": "permitACLName",
                "dataType": "STRING",
                "defaultValue": None,
                "description": None,
                "required": True,
                "notParam": False,
                "paramArray": False,
                "displayName": None,
                "instructionText": None,
                "group": None,
                "order": 1,
                "selection": {
                    "selectionType": None,
                    "selectionValues": {},
                    "defaultSelectedValues": []
                },
                "range": [],
                "key": None,
                "provider": None,
                "binding": ""
            },
            {
                "parameterName": "denyACLName",
                "dataType": "STRING",
                "defaultValue": None,
                "description": None,
                "required": True,
                "notParam": False,
                "paramArray": False,
                "displayName": None,
                "instructionText": None,
                "group": None,
                "order": 2,
                "selection": {
                    "selectionType": None,
                    "selectionValues": {},
                    "defaultSelectedValues": []
                },
                "range": [],
                "key": None,
                "provider": None,
                "binding": ""
            }
        ],
        "rollbackTemplateParams": [],
        "composite": False,
        "containingTemplates": []
    }
    response = create_configuration_template(headers, project_id, template_info)
    task_id = response['taskId']

    time.sleep(3)
    response = get_task(headers, task_id)

    template_id = response['data']

    # Create Template version
    template_version = {
        "comments": "DNAC Guide Initial Version",
        "templateId": template_id
    }
    create_configuration_template_version(headers, template_version)

    # Deploy Template to device
    deployment_info = {
        "forcePushTemplate": False,
        "isComposite": False,
        "targetInfo": [
            {
                "hostName": device_name,
                "params": {
                    "permitACLName": "GUIDE-ALLOW-ACL",
                    "denyACLName": "GUIDE-DENY-ACL"
                },
                "type": "MANAGED_DEVICE_IP"
            }
        ],
        "templateId": template_id
    }
    response = deploy_configuration_template(headers, deployment_info)
    print(response)


if __name__ == "__main__":
    main()