Table of Contents
Introduction
One use case that I have been working on for several customers is the ZTP (Zero-Touch-Provisioning) via PNP (Plug and Play) on Catalyst devices. Such a simple use case can be complex depending on the configuration that you want to push. I had customers who wanted to push the entire config (with BGP, MPLS, IPsec tunnels, etc.) in one shot or just a simple config (with a management IP, User).
Some of my customers implemented this via API calls and some via Ansible modules. A couple months ago Cisco developed a new, more intent-based Ansible module to simplify the provisioning process, which I will cover in this article in more detail.
If you want to learn more about the ZTP (PnP) process itself, I can highly recommend starting to read the blog series from Adam Radford : https://blogs.cisco.com/developer/cisco-dna-center-plug-and-play-pnp-part-1
Lab setup
In my lab, I am using DNA Center with two CSR1000V routers and a GitLab server with a Docker Runner.
For your reference, I attached the Dockerfile and the pipeline file.
Dockerfile
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y gcc python3.11 git && \
apt-get install -y python3-pip ssh && \
pip3 install --upgrade pip && \
pip3 install ansible && \
pip3 install dnacentersdk && \
pip3 install jmespath && \
pip3 install pyats[full] && \
pip3 install ansible-lint && \
ansible-galaxy collection install cisco.dnac
.gitlab-ci.yml
variables:
IMAGE_NAME_DNAC: $CI_REGISTRY_IMAGE/dnac-pnp
IMAGE_TAG_DNAC: "1.0"
stages:
- build
- deploy
build_image:
stage: build
tags:
- shell-runner
script:
- docker build -t $IMAGE_NAME_DNAC:$IMAGE_TAG_DNAC .
push_image:
stage: build
needs:
- build_image
tags:
- shell-runner
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker push $IMAGE_NAME_DNAC:$IMAGE_TAG_DNAC
dnac_ansible_job:
stage: deploy
needs:
- push_image
tags:
- docker-runner
image: $IMAGE_NAME_DNAC:$IMAGE_TAG_DNAC
script:
- ansible-playbook -i ansible/hosts ansible/PNP_NOT_INTENT-BASED.yml
- ansible-playbook -i ansible/hosts ansible/PNP_INTENT-BASED.yml
Non intent-based Ansible modules
Cisco implemented the Ansible modules for DNA Center based on the available APIs and created a 1:1 copy of that.
All available modules are listed here:
https://github.com/cisco-en-programmability/dnacenter-ansible/tree/main/plugins/modules
For some customers, this is really overwhelming and if you don’t understand fully which API is used for which action, it won’t be easy to implement the Ansible modules.
In the following example, I created the entire workflow to onboard a device with all necessary actions involved:
- Get the project ID of “Onboarding Configuration”
- Create a template under the Onboarding configuration project
- Approve the version of the template
- Create a site
- Get the site ID
- Get the Image ID (optional)
- Add the device to the PNP inventory
- Claim the device
PNP_NOT_INTENT-BASED.yml
- hosts: dnac_servers
gather_facts: no
connection: local
tasks:
- name: Get timestamp from the system
shell: "date +%Y-%m-%d%H-%M-%S"
register: tstamp
#
# Get project details
#
- name: Get list of Projects and getting the ID of the onboarding project
cisco.dnac.configuration_template_project_info:
name: "Onboarding Configuration"
register: project_result
- name: Set Project variable
ansible.builtin.set_fact:
project_id: "{{ project_result.dnac_response[0].id }}"
- debug: var=project_id
#
# Create template / Template Info
#
- name: Create an configuration_template_project
cisco.dnac.configuration_template_create:
name: "CSR1000V_{{ tstamp.stdout }}"
templateContent: "hostname TEST"
language: "VELOCITY"
projectName: "Onboarding Configuration"
deviceTypes:
- productFamily: "Switches and Hubs"
projectId: "{{ project_id }}"
softwareType: "IOS-XE"
softwareVariant: "XE"
register: configuration_template_project_result
- name: Set Task ID
ansible.builtin.set_fact:
task_id: "{{ configuration_template_project_result.dnac_response.response.taskId }}"
- debug: var=task_id
- name: Get Task by id
cisco.dnac.task_info:
taskId: "{{ task_id }}"
register: result_task_id
- name: Set Template ID
ansible.builtin.set_fact:
template_id: "{{ result_task_id.dnac_response.response.data }}"
- debug: var=template_id
- name: Create Versioning
cisco.dnac.configuration_template_version_create:
comments: "COMMITED"
templateId: "{{ template_id }}"
register: template_version_result
#
# Create Site / Site Info
#
- name: Create Site
cisco.dnac.site_create:
site:
building:
latitude: 37.338
longitude: -121.832
name: "beye.blog"
parentName: "Global"
type: "building"
- name: Get Site Info
cisco.dnac.site_info:
name: "Global/beye.blog"
register: site_result
- name: Set Site variable
ansible.builtin.set_fact:
site_id: "{{ site_result.dnac_response.response[0].id }}"
- debug: var=site_id
#
# Software Image Info
#
- name: Get list of Images and getting the ID of the image
cisco.dnac.swim_image_details_info:
isTaggedGolden: True
imageName: "csr1000v-universalk9.17.03.05.SPA.bin"
register: image_result
- name: Set Images variable
ansible.builtin.set_fact:
image_id: "{{ image_result.dnac_response.response[0].imageUuid }}"
- debug: var=image_id
#
# ZTP
#
- name: Adds a device to the PnP database
cisco.dnac.pnp_device:
state: present
version: 2
deviceInfo:
serialNumber: 96MZPDLDSNX
name: CSR1000V-01
state: Unclaimed
pid: CSR1000V
register: pnp_device_result
- name: Set Device variable
ansible.builtin.set_fact:
device_id: "{{ pnp_device_result.dnac_response.id }}"
- debug: var=device_id
- name: Claim device to a site
cisco.dnac.pnp_device_claim_to_site:
deviceId: "{{ device_id }}"
siteId: "{{ site_id }}"
type: "Default"
configInfo:
configId: "{{ template_id }}"
configParameters:
- key: ""
value: ""
imageInfo:
imageId: "{{ image_id }}"
register: claim_result
- debug: var=claim_result
Let’s check the pipeline status:
Let’s validate the status in DNA Center:
Over 120 lines of code and 9 different DNA Center Ansible modules to use Plug and Play and onboard a device?!
“That’s too much and does not simplify the process using Ansible”
This is the feedback I got from one of my customers.
The Cisco engineering team developed a module that is more intent-based.
Thanks to the authors Madhan Sankaranarayanan & Rishita Chowdhary.
New intent-based Ansible modules
The following two Ansible modules are doing the job (cisco.dnac.template_intent & cisco.dnac.pnp_intent) in a simpler and an intent-based approach, as the modules are doing a lot of heavy API jobs in the backend 😀.
PNP_INTENT-BASED.yml
- hosts: dnac_servers
gather_facts: no
connection: local
tasks:
- name: Get timestamp from the system
shell: "date +%Y-%m-%d%H-%M-%S"
register: tstamp
#
# Create the template
#
- name: Create the template
cisco.dnac.template_intent:
state: "merged"
config:
- projectName: "Onboarding Configuration"
templateContent: "hostname TEST"
language: "velocity"
deviceTypes:
- productFamily: "Switches and Hubs"
softwareType: "IOS-XE"
softwareVariant: "XE"
templateName: "CSR1000V_{{ tstamp.stdout }}"
versionDescription: "{{ tstamp.stdout }}"
#
# ZTP
#
- name: Create pnp device
cisco.dnac.pnp_intent:
config:
- type: "building"
site:
building:
latitude: 37.338
longitude: -121.832
name: "beye.blog"
parentName: "Global"
project_name: "Onboarding Configuration"
template_name: "CSR1000V_{{ tstamp.stdout }}"
image_name: "csr1000v-universalk9.17.03.05.SPA.bin"
site_name: "Global/beye.blog"
deviceInfo:
serialNumber: "96MZPDLDSNV"
hostname: "CSR1000V-02"
state: "Unclaimed"
pid: "CSR1000V"
register: pnp_result
- debug: var=pnp_result
Let’s validate the status in DNA Center:
Here we go! This did it in just a few lines! 👍