Automating SDA Edge Node Workflow with Ansible

"The What?" - In this blog I want to cover a project with Ansible that I created to automate parts of a workflow relating to an SDA edge node (EN) deployment. Now to breakdown the workflow I will be touching on looks like this in Ansible:

My goal further down in this post is to walk you through how each phase was automated and breakdown each phase.


"The Why?" - Automating the workflow removes several manual tasks, & saves arguably 30 or more minutes depending on how fast and well someone knows the workflow & how to navigate within DNAC. All thanks for consuming DNAC APIs & Ansible we can knock this workflow out in 4-5 minutes. Lastly, this took me some time so I hope that this will help others identify payload requirements for specific API calls & potentially help you figure out how to use Ansible to consume APIs in general. For me this stuff is fun & requires a lot of trial & error.


"The How?" - To start it is important to have a general understanding of how Ansible playbooks & Tower works, & how to consume APIs in general but specifically DNAC APIs (this may help get you started: Cisco DevNet: APIs, SDKs, Sandbox, and Community for Cisco Developers). Also, understand that the base config for underlay discovery is already present on the edge node deployed in the field. Base config consists of device routing, access control, & management control, etc. Essentially the bare minimum that will allow us to consume DNAC APIs to discover, & provision the edge node.


Additional pre-reqs/notes:

  • You will need to tweak each individual playbook that I am going to share to meet your specific environment needs. Truthfully you can rely on curl to obtain some of the information or run the queries right from within DNAC

  • Items that will vary: siteName, siteName id string, user/pass and authz string, all urls used in API calls, other variables you will see referenced

  • Used & tested against later DNAC train 2.2.x.x; strongly suggest testing before attempting to use in production;

  • Most of the playbooks first play you will see obtaining an DNAC authz token, extraction with ansible, & creating a var so we can use with later calls

  • Some variables are hardcoded, while others are gathered via user input from tower survey upon workflow launch

Once the base config is present on an edge node that is connected somewhere in the environment it is safe to assume that we can begin the Adding EN to SDA Fabric workflow via Ansible. A brief overview of the phases to prep an EN for full deployment & user readiness:

  • Phase 1 - Deploy EN with base underlay config to field (not a part of the automated workflow, but critical component)

  • Phase 2 - Adding EN to DNAC inventory (discovery)

  • Phase 3 - Add Device to Site

  • Phase 4 - Update ISE EN Details (technically this is optional & will vary depending on how you setup policies in ISE) -- In this demo use case Device location & group play a role in policies

  • Phase 5 - Provision EN

  • Phase 6 - Add device to Fabric topology as EN type

  • Phase 7 - Unfortunately, the last piece to prep the EN for user access is port provisioning AKA host onboarding in DNAC; right now as of this post the API (addPortAssignmentForUserDeviceInSDAFabric) is only capable of allowing remote calls to statically provision the ports for onboarding so automating this phase of the workflow will not work if you are looking to provision ports with dynamic assignment for host onboarding via ISE policies;

Sharing for reference: API names used (you can get these from online docs I have shared OR via DNAC developer toolkit):

  • Add Device

  • Assign Device To Site

  • ISE API Calls to Update NAD Details/Attributes - Check Network Device Update API

  • Provision Wired Device

  • Add Edge Device In SDA Fabric

  • Add Port Assignment For User Device In SDA Fabric

Now that you have a better understanding of each phase let's dive into the details of each playbook that is a part of our workflow. My goal here is to show playbooks to provide examples for you to reference/use.


Phase 1 details - omitted since not truly a part of automated workflow


Phase 2 playbook details -Adding Edge Node to DNAC Inventory via discovery; pay attention to the payload in the fifth task as there are some required strings needed to perform discovery + add to inventory;

---
- name: DNAC
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    global_pw: XX
    enable_pw: XX

  tasks:
  - name: Get DNAC Authz Token
    uri:
      url: https://xx.xx.xx.xx/api/system/v1/identitymgmt/token
      headers:
        Accept: application/json
        Authorization: Basic bWNpZxxxk6dGVzXXXX==
        content-type: application/json
      body_format: json
      body: None
      validate_certs: no
      status_code: 200
      method: POST
    register: dnac_token  

  - name: Create Token Var
    set_fact:
      token: "{{ dnac_token | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.Token'

  - name: Extract Token
    set_fact:
     auth_token: "{{ token[0] }}"

  - name: View Auth Token
    debug: 
     msg: "{{ auth_token }}"
  
  - name: Add new NAD to DNAC Inventory
    uri:
      url: https://xx.xx.xx.xx/dna/intent/api/v1/network-device/
      headers:
        Accept: application/json
        X-Auth-Token: "{{ auth_token }}"
        content-type: application/json
      status_code: 202
      method: POST
      body_format: json
      body: '{"ipAddress" :["{{ ip_addr }}"],"type" : "NETWORK_DEVICE","computeDevice" : "False", "snmpVersion" : "V3","snmpUserName" : "xxxx","snmpMode" : "RW","snmpAuthProtocol" : "SHA","snmpAuthPassphrase" : "xxxx","snmpPrivProtocol" : "AES128","snmpPrivPassphrase" : "xxxx","snmpRetry" : "3","snmpTimeout" : "5","cliTransport" : "SSH","userName" : "dnac","password" : "{{ global_pw }}","enablePassword": "{{ enable_pw }}"}}'
      validate_certs: no
    register: dnac_response

  - name: Pause for 1 minute to let DNAC discover & add NAD
    pause:
      minutes: 1
      prompt: "Waiting 1 minute for DNAC to discover & add NAD to Inventory"
  
  - name: Print DNAC response
    debug:
      msg: "{{ dnac_response }}"

Phase 3 Playbook details - Add device to site

---
- name: DNAC
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
  - name: Get DNAC Authz Token
    uri:
      url: https://xx.xx.xx.xx/api/system/v1/identitymgmt/token
      headers:
        Accept: application/json
        Authorization: Basic bWNpZxxxXXX
        content-type: application/json
      body_format: json
      body: None
      validate_certs: no
      status_code: 200
      method: POST
    register: dnac_token  

  - name: Create Token Var
    set_fact:
      token: "{{ dnac_token | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.Token'

  - name: Extract Token
    set_fact:
     auth_token: "{{ token[0] }}"

  - name: View Auth Token
    debug: 
     msg: "{{ auth_token }}"
  
  - name: Add new NAD to Site
    uri:
      url: https://xx.xx.xx.xx/dna/system/api/v1/site/8256ba5c-6b6c-4109-9c40-e0a55xxx422/device/
      headers:
        X-Auth-Token : "{{ auth_token }}"
        Content-type : application/json
        __persistbapioutput: true
        __runsync: false
        __runsynctimeout: 55
      status_code: 202
      method: POST
      body_format: json
      body: '{ "device": [ { "ip": "{{ ip_addr }}" } ] }'
      validate_certs: no
    register: dnac_response

  - name: Waiting for DNAC to Add Device to Site
    pause:
      minutes: 1
      prompt: "Waiting 1 minute for DNAC to add NAD to Site"
  
  - name: Print DNAC response
    debug:
      msg: "{{ dnac_response }}"

Phase 4 Playbook - Remember this is optional; Truly depends on if you use certain ISE NAD attributes in policies & want CoA's sent from specific ISE Node; Checkout ISE Online SDK for ISE API consumption help (https://x.x.x.x:9060/ers/sdk#_);

---
- name: ISE
  hosts: localhost
  connection: local
  gather_facts: false
  vars:
    ise_user: xxxx
    ise_pass: xxxx

  tasks:
  - name: Get Existing NAD ID string
    uri:
      url: https://x.x.x.x:9060/ers/config/networkdevice?filter=ipaddress.EQ.{{ ip_addr }}
      user: "{{ ise_user }}"
      password: "{{ ise_pass }}"
      headers:
        Accept: application/json
        content-type: application/json
        ers-media-type: network.networkdevice.1.1
      status_code: 200
      method: GET
      validate_certs: no
    register: nad_id

  - name: Print returned ISE json data
    debug:
     msg: "{{ nad_id.json }}"
   
  - name: Get ISE ID String
    set_fact:
     id: "{{ nad_id | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.SearchResult.resources[*].id'

  - name: Extract NAD ID from Nested List
    set_fact:
     id: "{{ id[0][0] }}"

  - name: Print ISE NAD ID
    debug: 
     msg: "{{ id }}"

  - name: Update NAD Details in ISE DB
    uri:
      url: https://x.x.x.x:9060/ers/config/networkdevice/{{ id }}
      user: "{{ ise_user }}"
      password: "{{ ise_pass }}"
      headers:
        Accept: application/json
        content-type: application/json
        ers-media-type: network.networkdevice.1.1
      status_code: 200
      method: PUT
      body_format: json
      body: '{"NetworkDevice": {"id": "{{ id }}","name": "Axxx{{ site_id }}","profileName": "Cisco","coaPort": "1700","authenticationSettings" : {},"snmpsettings" : {"pollingInterval" : 3600,"linkTrapQuery" : "false","macTrapQuery" : "false",},"trustsecsettings" : {"deviceAuthenticationSettings" :{},"sgaNotificationAndUpdates" : {"downlaodEnvironmentDataEveryXSeconds" : 86400,"downlaodPeerAuthorizationPolicyEveryXSeconds" : 86400,"reAuthenticationEveryXSeconds" : 86400,"downloadSGACLListsEveryXSeconds" : 86400,"otherSGADevicesToTrustThisDevice" : "true","sendConfigurationToDevice" : "true","sendConfigurationToDeviceUsing" : "ENABLE_USING_COA","coaSourceHost" : "{{ ise_coa_node }}"
},"deviceConfigurationDeployment" : {"includeWhenDeployingSGTUpdates" : "true",}},"NetworkDeviceIPList": [{"ipaddress": "{{ ip_addr }}","mask": 32,}],"NetworkDeviceGroupList": ["Location#All Locations#{{ location }}","Device Type#All Device Types#SDA#{{ owner }}","IPSEC#Is IPSEC Device#No"]}}'
      validate_certs: no
    register: ise_update

  - name: Print ISE Update Notification
    debug: 
     msg: "{{ ise_update }}"

Phase 5 Playbook details - Provision the EN; Kicker here is that you will want to ensure that you have your network profiles are properly setup especially if you have unique templates in a provisioning workflow;

---
- name: DNAC
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
  - name: Get DNAC Authz Token
    uri:
      url: https://x.x.x.x/api/system/v1/identitymgmt/token
      headers:
        Accept: application/json
        Authorization: Basic xxxx
        content-type: application/json
      body_format: json
      body: None
      validate_certs: no
      status_code: 200
      method: POST
    register: dnac_token  

  - name: Create Token Var
    set_fact:
      token: "{{ dnac_token | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.Token'

  - name: Extract Token
    set_fact:
     auth_token: "{{ token[0] }}"

  - name: View Auth Token
    debug: 
     msg: "{{ auth_token }}"
  
  - name: Provision NAD
    uri:
      url: https://x.x.x.x/dna/intent/api/v1/business/sda/provision-device
      headers:
        Accept: application/json
        X-Auth-Token : "{{ auth_token }}"
        Content-type : application/json
      status_code: 200
      method: POST
      body_format: json
      body: '{"deviceManagementIpAddress":"{{ ip_addr }}","siteNameHierarchy":"Global/XXX/XXX_CENTER"}'
      validate_certs: no
    register: dnac_response

  - name: Pause for 2 minutes to let DNAC provision the EN
    pause:
      minutes: 2
      prompt: "Waiting 2 minutes for DNAC to provision the device"

  - name: Create DNAC Response Variable
    set_fact:
     response: "{{ dnac_response | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.description'

  - name: Print DNAC response
    debug:
      msg: "{{ response }}"

Phase 6 - Add NAD as type EN to Fabric Topology

---
- name: DNAC
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
  - name: Get DNAC Authz Token
    uri:
      url: https://xx.xx.xx.xx/api/system/v1/identitymgmt/token
      headers:
        Accept: application/json
        Authorization: Basic bWNpZmVsxxx6dxxXXXXQ==
        content-type: application/json
      body_format: json
      body: None
      validate_certs: no
      status_code: 200
      method: POST
    register: dnac_token  

  - name: Create Token Var
    set_fact:
      token: "{{ dnac_token | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.Token'

  - name: Extract Token
    set_fact:
     auth_token: "{{ token[0] }}"

  - name: View Auth Token
    debug: 
     msg: "{{ auth_token }}"
  
  - name: Add Edge Node to SDA Fabric
    uri:
      url: https://xx.xx.xx.xx/dna/intent/api/v1/business/sda/edge-device
      headers:
        Accept: application/json
        X-Auth-Token : "{{ auth_token }}"
        Content-type : application/json
      status_code: 200
      method: POST
      body_format: json
      body: '[{"deviceManagementIpAddress":"{{ ip_addr }}","siteNameHierarchy":"Global/XXX/xxx_Building"}]'
      validate_certs: no
    register: dnac_response
  
  - name: Print returned DNAC json data
    debug:
     msg: "{{ dnac_response.json }}"

  - name: Create DNAC Response Variable
    set_fact:
     response: "{{ dnac_response | json_query(jmesquery) }}"
    vars:
      jmesquery: '*.description'

  - name: Print DNAC response
    debug:
      msg: "{{ response }}"

Phase 7 - sharing what I have started that I think will work once the API allows dynamic assignment; This is a custom ansible module that relies on python; Important note: main thing here is the ability to gather/use interfaceName that WILL vary between EN platform types. In the code below I gathered interfaceName types and created separate txt files to reference based on user variables provided. Lastly, see another example about custom ansible modules here: Using a Custom Ansible Module for ISE API Interaction

Custom module:

---
- name: DNAC Module  
  hosts: localhost  
  gather_facts: False  
  tasks:    
    - name: Test file loop      
      custom_dnac_en_host_onboard_module:        
        switch_type: "{{ switch_type }}"        
        ip_addr: "{{ ip_addr }}"

Python code:

#!/usr/bin/python
import requests
import json
from time import sleep
from ansible.module_utils.basic import AnsibleModule

token = ("")

def main():
    # Define your module arguments that a user will pass to the module
    # In my case for this module I only need the client MAC Address
    module_args = dict(
        switch_type=dict (type="str", required=True),
        ip_addr=dict (type='str', required=True),
    )

    # Create an instance of AnsibleModule class
    # Used to work with Ansible
    module = AnsibleModule(argument_spec=module_args)
    result = dict(changed=False, msg="")

    # Extract the arguments provided from user
    switch_type = module.params["switch_type"]
    ip_addr = module.params["ip_addr"]

    # DNAC API Interaction
    if switch_type == "WS-C3850-24XS-E":
        file_name = "WS-C3850-24XS-E_Int_Names.txt"
    elif switch_type == "WS-C3650-24TS-S":
        file_name = "WS-C3650-24TS-S_Int_Names.txt"
    elif switch_type == "C9300-24T":
        file_name = "C9300-24T_Int_Names.txt"
    elif switch_type == "C9300-48S":
        file_name = "C9300-48S_Int_Names.txt"
    elif switch_type == "C9300-48T":
        file_name = "C9300-48T_Int_Names.txt"
    elif switch_type == "C9300-48UXM":
        file_name = "C9300-48UXM_Int_Names.txt"
    elif switch_type == "C9500-16X":
        file_name = "C9500-16X_Int_Names.txt"
    else:
        file_name = "C9500-40X_Int_Names.txt"

    #Looping Switch Type file to get interface names
    file = open("/usr/share/ansible/sda_en_types/%s"%file_name)
    intf = file.readline()
    while intf:
        #Get Authz Token for API Interaction
        intf = file.readline()
        data = get_token()
        #Consuming DNAC API to add port assignment for user device in SDA Fabric
        api_url = 'https://x.x.x.x/dna/intent/api/v1/business/sda/hostonboarding/user-device'
        auth = {
           'X-Auth-Token' : data,
           'Content-type' : 'application/json',
           'Accept' : "application/json",
        }
        payload ={
            "deviceManagementIpAddress": ip_addr,
            "siteNameHierarchy": "Global/xxx/Cxxx",
            "interfaceName": intf,
            "dataIpAddressPoolName": "null",
            "voiceIpAddressPoolName": "null",
            "authenticateTemplateName": "Closed Authentication",
            "interfaceDescription": "User Device",
        }
        r = requests.post(url=api_url, headers=auth, json=payload, verify=False)
        sleep(5)
    file.close()

    result["msg"] = r

    # After successful module execution, pass the key/value results and exit module
    module.exit_json(**result)

def get_token():
    API_URL = 'https://x.x.x.x/api/system/v1/identitymgmt/token'
    AUTH = 'xxx','xxx'
    HEADER = {'content-type': 'application/json'}

    r = requests.post(url=API_URL, auth=AUTH, headers=HEADER, verify=False)

    data = r.text
    key = json.loads(data)
    token = key['Token']
    return token

if __name__ == "__main__":
    main()

Once the individual playbooks are setup as job templates you can tie them together with Ansible Tower workflow. Then the last component will be ensuring you have properly setup the user survey to gather respective variables. Once everything is together and working the last piece, for now, will be to manually provision ports for host onboarding - at least until the API allows for dynamic assignment :)


Versions:

  • DNAC 2.2.3.4

  • Ansible 3.8.1

  • ISE 2.7 & 3.0

Thanks for reading & good luck with your automation journey, Cheers!

0 comments

Recent Posts

See All

"The What?" - In this blog I want to cover an example of using Ansible to automate the process of provisioning ports based on user defined variables & ansible facts across different ios-xe platform ty

"The What?" - In this blog I want to cover a brief overview of one way to install AnyConnect (AC) on a linux client running a supported OS. Once I cover the overview I intend on covering a few Ansibl