Using Ansible to configure AP’s on a Cisco 9800 Controller using a Google Sheet
Using Ansible to configure AP’s on a Cisco 9800 Controller using a Google Sheet
I work for a university with nearly 10000 AP’s and growing. There is a steady amount of new construction in addition to replacing older technology on a ~ 5 year cycle. I recently discovered Ansible and have been using it to upgrade ios on switches and build some utilities for provisioning ports. Spending a lot of time configuring AP’s I thought I would try to build some efficiencies using Ansible. I started building some scripts to apply tags, but eventually I got ambitious and thought maybe I could apply all the configuration options from a Google Sheet without even accessing the controller gui. I am going to detail and document the process.
BTW, thanks to all those who post examples, youtube videos, and the like. It has been very helpful and motivated me to share this process.
This Ansible script will read information from a Google sheet and make the following changes:
- Apply Names
- Apply Locations
- Change to static addresses
- Apply Tags
- Write Tags
- Enter Primary and Secondary Controllers
- Write the Config
Start with a Google Sheet
Prior to using Ansible I always started with a predictive design that details the names and locations of the Access Points. From there I would scan the mac addresses into a google sheet column. enter the AP names into a column, enter the IP addresses (I have them pre-configured for DNS/DHCP with the mac address of each AP). Then I add other details that the controller will need including:
Mask
Gateway
The mac address parsed for controller (1bad.f00d.cafe format)
The default name which is “AP” followed by the mac address separated with dashes
Primary & Secondary controller and associated IP’s
Tags (Policy, RF, Site)
Most of these entries are either derived from row 1, or derived from the mac address, not entered manually. I also added a column called “skip” which allows you to skip configuring AP’s. The use case is configuring lets say 100 Access Points for a new deployment and you are doing a box of 10 at a time. Putting a “x” on all the AP you want to skip allows the Ansible script to run much more efficiently by not trying to connect to AP’s that are not connected to the controller.
Here is an example of the google sheet that I use:
https://docs.google.com/spreadsheets/d/1C1yModG1MwwDSvnZHdA8aCAoX1FkgmCdSQiAy1Fzymk/edit?usp=sharing
Connect AP's to a switch
Connect the AP's to a network switch (either a staging switch or production location...your choice) and make sure they attach to a controller. In my case I have two a primary and secondary controller and the script will put them on the correct controller as defined in the spreadsheet regardless of which one they use for the initial connection. Once connected it should look like this on the controller:
Ansible
I am assuming that you know some basics about ansible already. There are two main files that you need to be aware of: the inventory file which is a text file with a list of the devices that you will connect to in your automation playbook. The second file is the playbook which is the script that executes and provide the automation.
My inventory file contains all the Cisco 9800 Controllers in my environment.
ctrl9800.txt
--------------
[cat9800]
172.26.225.200 ansible_connection=network_cli ansible_network=ios ansible_user=adminaccount ansible_port=22 ansible_host=172.26.225.200
172.26.225.201 ansible_connection=network_cli ansible_network=ios ansible_user=adminaccount ansible_port=22 ansible_host=172.26.225.201
[all:vars]
ansible_connection=network_cli
ansible_network_os=ios
ansible_become=yes
ansible_command_timeout=1800
boot_file_loc=flash
playbook-ctrlFromGsheet.yaml
--------------------------------------------
---
- name: Configure using Google Sheet
hosts: cat9800
vars_prompt:
- name: sheetid
prompt: " What is the Google Sheet ID ? "
private: no
when: sheetid == ""
vars:
apmacs: []
maclist: []
tasks:
- ios_command:
commands:
- show version | i uptime
register: show
- debug: var=show.stdout_lines
# the above task is not necessary just used to make sure I was connecting to the # controller and thought I would display the uptime
- name: Download JSON content play
uri:
url: http://gsx2json.com/api?id={{sheetid}}&columns=false
return_content: yes
register: jsoncontent
# Connects to the Google sheet based on Sheet ID. Remember to publish the
# sheet!
- name: Define List
debug: msg= "{{ jsoncontent.json | json_query(jmesquery) }}"
vars:
jmesquery: "[*].{number: number, name: name, fqdn: fqdn, mac: mac, ip: ip, host: host, subnet: subnet, mask: mask, gateway: gateway, skip: skip, parsedfordnslookup: parsedfordnslookup, parsedforcontroller: parsedforcontroller, defaultname: defaultname, primary: primary, primaryip: primaryip, secondary: secondary, secondaryip: secondaryip, ptag: ptag, rtag: rtag, stag: stag}"
# Associates the information in the sheet with the column headers
# Column headers eliminates spaces and return in lowercase
- name: Apply names based on Mac
ignore_errors: True
ios_command:
commands:
- ap name {{item.defaultname}} name {{item.name}}
loop: "{{jsoncontent.json.rows}}"
when:
- item.skip == 0
# Changes default name to AP name
- name: Pause to let AP reconnect to controller
pause:
seconds: 25
# Pause to allow time to reconnect to controller after name change
- name: Apply Location based on Name
ignore_errors: True
ios_command:
commands:
- ap name {{item.name}} location {{item.name}}
loop: "{{jsoncontent.json.rows}}"
when:
- item.skip == 0
# Changes from default location to actual based on name
# You may prefer a to add a column in the spreadsheet to have a name
# different from the AP name
- name: Apply Static IP addresses
ignore_errors: True
ansible.netcommon.cli_command:
command: "ap name {{item.name}} static-ip ip-address {{item.ip}} netmask {{item.mask}} gateway {{item.gateway}}"
prompt:
- '\[confirm\]'
answer:
- "\r"
loop: "{{jsoncontent.json.rows}}"
when:
- item.skip == 0
# Change from dhcp to static addresses
- name: Pause to let AP reconnect to controller
pause:
seconds: 20
- name: Apply Policy RF Site Tags
ignore_errors: True
ios_config:
commands:
- policy-tag {{item.ptag}}
- rf-tag {{item.rtag}}
- site-tag {{item.stag}}
parents: "ap {{ item.parsedforcontroller }}"
loop: "{{jsoncontent.json.rows }}"
when:
- item.skip == 0
# Apply Tags
- name: Pause to let AP Reconnect
pause:
minutes: 1
- name: write tags
ios_command:
commands:
- "ap name {{item.name}} write tag-config"
loop: "{{jsoncontent.json.rows }}"
when:
- item.skip == 0
# Write Tags
- name: set primary and backup controllers
ignore_errors: True
ios_command:
commands:
- ap name {{item.name}} controller secondary {{item.secondary}} {{item.secondaryip}}
- ap name {{item.name}} controller primary {{item.primary}} {{item.primaryip}}
loop: "{{jsoncontent.json.rows}}"
when:
- item.skip == 0
# Set Primary and Secondary Controllers. Do secondary first.
- name: Write Config
ios_command:
commands:
- wr
- name:
debug:
msg:
- "********************************************************"
- " Note: AP's will take a few minutes to move controllers"
- "********************************************************"
- " If all google sheet AP's have been configured go to "
- " File --> Publish --> Unpublish "
- "********************************************************"

Hi, I tried to use this playbook but it's not working for me. Here is the error I get:
ReplyDeleteERROR! Invalid vars_prompt data structure, found unsupported key 'when'
The error appears to be in '/Users/tluna/Downloads/playbook-ctrlFromGsheet.yaml': line 5, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: sheetid
^ here