Cisco DNA Center ZTP (Zero-touch-provisioning) with Ansible intent-based 🥳

Picture of Chris Beye

Chris Beye

Systems Architect @ Cisco Systems

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! 👍