Device Onboarding Guide

Introduction

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

Use templates for Day Zero Provisioning to send configurations to devices being onboarded to the network and Catalyst Center.

Goal

The goals of this guide are:

  1. Create Day0 template.
  2. Create a network profile.
  3. Assign sites to a network profile.
  4. Add a device to Catalyst Center.
  5. Claim the device.

Device onboarding workflow

Endpoints and methods used

  • POST /dna/system/api/v1/auth/token
  • GET /dna/intent/api/v1/site
  • POST /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 /api/v1/siteprofile
  • POST /api/v1/siteprofile/{site_profile_id}/site/{site_id}
  • POST /dna/intent/api/v1/onboarding/pnp-device/import
  • POST /dna/intent/api/v1/onboarding/pnp-device/site-claim

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.

Notes: 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 = '/api/system/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 Information

With the token information, we proceed to retrieve the information of the Catalyst Center Guide Building site in the Catalyst Center, which we will later use for the site profile information.

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']

Configuration Template API

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

Day Zero templates must belong to the Onboarding Configuration Project in the Template Editor.

Project 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)

Site Profile API

Use the Site Profile API to relate sites and templates, creating a profile with Day Zero and Day N templates, and associating it with the types of devices and sites it can be used for.

site_profile_info = {
    "name": "DNA Center Guide Profile",
    "namespace": "switching",
    "profileAttributes": [
        {
            "key": "day0.templates",
            "attribs": [
                {
                    "key": "device.family",
                    "value": "Switches and Hubs",
                    "attribs": [
                        {
                            "key": "device.series",
                            "value": "Cisco Catalyst 9500 Series Switches",
                            "attribs": [
                                {
                                    "key": "device.type",
                                    "attribs": [
                                        {
                                            "key": "template.id",
                                            "value": template_id,
                                        },
                                        {
                                            "key": "device.tag",
                                            "value": "",
                                            "attribs": [

                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                }
            ]
        }
    ]
}
SITE_PROFILE_URL = '/api/v1/siteprofile'
response = requests.post(BASE_URL + SITE_PROFILE_URL,
                         headers=headers, json=site_profile_info,
                         verify=False

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

time.sleep(3)
response = requests.get(BASE_URL + TASK_BY_ID_URL.format(task_id=task_id),
                        headers=headers, verify=False)

site_profile_id = response['id']

Onboarding (PnP) API

The Device Onboarding API supports the PnP process, allowing developers to create a workflow that detects when a device joins the network, communicates with Catalyst Center, and sends the onboarding configuration to the device.

This API consists of 28 endpoints you can use to manage workflows, include devices in the PnP Process, claim devices, and more.

In this guide, we add a device with serial number 919L3GOS8QC to the PnP process.

ONBOARDING_PNP_IMPORT_URL = '/dna/intent/api/v1/onboarding/pnp-device/import'
device_serial = '919L3GOS8QC'
device_name = 'CAT9k-DNAC-Guide'
device_pid = 'C9500-40X'
pnp_import_info = [
    {
        "deviceInfo": {
            "hostname": device_name,
            "serialNumber": device_serial,
            "pid": device_pid,
            "sudiRequired": False,
            "userSudiSerialNos": [],
            "aaaCredentials": {
                "username": "",
                "password": ""
            }
        }
    }
]

response = requests.post(BASE_URL + ONBOARDING_PNP_IMPORT_URL,
                         headers=headers, json=pnp_import_info,
                         verify=False)
device_id = response['successList'][0]['id']

After importing creating the site profile and importing the device to the PnP process, the final step is to claim the device with the configuration and the site.

claim_info = {
    "siteId": site_id,
    "deviceId": device_id,
    "type": "Default",
    "configInfo": {
        "configId": template_id,
        "configParameters": [
                {"key": "permitACLName", "value": "GUIDE-ALLOW-ACL"},
                {"key": "denyACLName", "value": "GUIDE - DENY - ACL"}
            ]
    }
}
response = requests.post(BASE_URL + ONBOARDING_CLAIM_DEVICE_URL,
                         headers=headers, json=claim_info,
                         verify=False

Code

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

# Modules import
import requests
from requests.auth import HTTPBasicAuth
import time
import sys

# Disable SSL warnings. Not needed in production environments with valid certificates
import urllib3
urllib3.disable_warnings()

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

# URLs
SITE_PROFILE_ADD_SITE_URL = '/api/v1/siteprofile/{site_profile_id}/site/{site_id}'
SITE_PROFILE_URL = '/api/v1/siteprofile'
SITE_URL = '/dna/intent/api/v1/site'
TASK_BY_ID_URL = '/dna/intent/api/v1/task/{task_id}'
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'
ONBOARDING_PNP_IMPORT_URL = '/dna/intent/api/v1/onboarding/pnp-device/import'
ONBOARDING_CLAIM_DEVICE_URL = '/dna/intent/api/v1/onboarding/pnp-device/site-claim'

# 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 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']

# Create site profile
def create_site_profile(headers, site_profile_info):
    response = requests.post(BASE_URL + SITE_PROFILE_URL,
                             headers=headers, json=site_profile_info,
                             verify=False)
    return response.json()['response']

# Assign Site to Site Profile
def assign_site_to_site_profile(headers, site_profile_id, site_id):
    response = requests.post(BASE_URL + SITE_PROFILE_ADD_SITE_URL.format(
                                                              site_profile_id=site_profile_id,
                                                              site_id=site_id),
                             headers=headers, verify=False)
    return response.json()

# Import device to PnP process
def import_device_to_pnp(headers, pnp_import_info):
    response = requests.post(BASE_URL + ONBOARDING_PNP_IMPORT_URL,
                             headers=headers, json=pnp_import_info,
                             verify=False)
    return response.json()

# Import device to PnP process
def claim_device_to_site(headers, claim_info):
    response = requests.post(BASE_URL + ONBOARDING_CLAIM_DEVICE_URL,
                             headers=headers, json=claim_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 Catalyst 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
    device_serial = '919L3GOS8QC'
    device_name = 'CAT9k-DNAC-Guide'
    device_pid = 'C9500-40X'

    # 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 - Day0",
        "description": "Guide Configuration Template",
        "tags": [],
        "deviceTypes": [
            {
                "productFamily": "Switches and Hubs",
                "productSeries": "Cisco Catalyst 9500 Series Switches"
            }
        ],
        "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": []
    }

    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 Day 0 Initial Version",
        "templateId": template_id
    }
    create_configuration_template_version(headers, template_version)

    # Create Configuration Template
    site_profile_info = {
        "name": "DNA Center Guide Profile",
        "namespace": "switching",
        "profileAttributes": [
            {
                "key": "day0.templates",
                "attribs": [
                    {
                        "key": "device.family",
                        "value": "Switches and Hubs",
                        "attribs": [
                            {
                                "key": "device.series",
                                "value": "Cisco Catalyst 9500 Series Switches",
                                "attribs": [
                                    {
                                        "key": "device.type",
                                        "attribs": [
                                            {
                                                "key": "template.id",
                                                "value": template_id,
                                            },
                                            {
                                                "key": "device.tag",
                                                "value": "",
                                                "attribs": [

                                                ]
                                            }
                                        ]
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }

    response = create_site_profile(headers, site_profile_info)

    task_id = response['taskId']

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

    site_profile_id = response['id']

    response = assign_site_to_site_profile(headers, site_profile_id, site_id)

    # Add device to PnP process

    pnp_import_info = [
        {
            "deviceInfo": {
                "hostname": device_name,
                "serialNumber": device_serial,
                "pid": device_pid,
                "sudiRequired": False,
                "userSudiSerialNos": [],
                "aaaCredentials": {
                    "username": "",
                    "password": ""
                }
            }
        }
    ]

    response = import_device_to_pnp(headers, pnp_import_info)

    device_id = response['successList'][0]['id']

    claim_info = {
        "siteId": site_id,
        "deviceId": device_id,
        "type": "Default",
        "configInfo": {
            "configId": template_id,
            "configParameters": [
                {"key": "permitACLName", "value": "GUIDE-ALLOW-ACL"},
                {"key": "denyACLName", "value": "GUIDE - DENY - ACL"}
            ]
        }
    }

    claim_device_to_site(headers, claim_info)



if __name__ == "__main__":
    main()