Managing Cisco IOS Upgrades with Ansible

by Patrick Ogenstad

March 15, 2016

I was recently asked to automate the way a client handles Cisco IOS upgrades. As I’ve been using Ansible a lot lately I decided to start there. Basically the steps required to do the upgrade can be broken down into parts which map quite nicely to tasks in an Ansible playbook. Even if you aren’t using IOS you might find it interesting to see how different Ansible modules can be combined in order to complete a set of tasks.

Cisco IOS upgrades - defining the tasks

Previously the way the routers were upgraded was to ssh into each device and tftp or scp copy the image files to flash and change the boot order.

Specifically the routers were Cisco uBR 10k routers which have different routing engines. In this environment they were called PRE4 and PRE5, each type requires it’s own .bin image. Also the linecards required a .pkg file specific to the routing engine. Many of the routers have redundant routing engines so the images need to be copied onto standby disks as well as the primary disks.

The steps required to complete the upgrade looked like this:

Figure out if the device had a redundant routing engine

Figure out if the device had a PRE4 or a PRE5

Copy the correct boot image(s) and linecard image(s) to the device

Change the system boot order

Save the configuration

Reboot

Setting up Ansible tasks

If you are completely new to Ansible you might want to learn some basics, but it should still be quite easy to follow along.

After looking around a bit i figured that one of the easiest way to check if the target device was redundant was to do an SNMP query to the cRFStatusPeerUnitState (1.3.6.1.4.1.9.9.176.1.1.4) oid from CISCO-RF-MIB. I setup this first task with the snmp_query module.

- name : Collect snmp_query : host : " {{ inventory_hostname }}" version : 2c community : " {{ snmp_community }}" oid : - 1.3.6.1.4.1.9.9.176.1.1.4.0 register : output

The result in the output variable was then parsed. Here I was looking for a value of 9 indicating that the peer unit was in standby hot state, those devices where marked as redundant using the set_fact module. If the value was 2 (disabled) I marked the device as not redundant. Other responses would indicate that something was wrong, so those devices should fail without continuing.

- name : Device is redundant set_fact : redundant : True when : output['1.3.6.1.4.1.9.9.176.1.1.4.0'] == 9 - name : The device is not redundant set_fact : redundant : False when : output['1.3.6.1.4.1.9.9.176.1.1.4.0'] == 2 - name : Exit fail : msg : " Broken" when : output['1.3.6.1.4.1.9.9.176.1.1.4.0'] not in [2, 9]

The next part was to figure out if the device was a PRE4 or a PRE5. At first I planned to get this through SNMP too, however it seemed like I would have to poll the entire inventory of each device which would give me a lot of information I wasn’t interested in. In the end I figured I’d go with a simpler solution, good old screenscraping. I saw that when you issue a “show version” the PRE4 devices has a line containing UBR10K4 where the PRE5 says UBR10K5. So I used the ios_command module from Ansible’s new networking initiative and then just saved the output from that. The output was parsed and the pre variable was assigned.

- name : Collect version ios_command : host : " {{ inventory_hostname }}" username : " {{ device_ssh_username }}" password : " {{ device_ssh_password }}" commands : " show version" register : version - name : Pre5 set_fact : pre : 5 when : ' "UBR10K5" in version["stdout"][0]' - name : Pre4 set_fact : pre : 4 when : ' "UBR10K4" in version["stdout"][0]'

Now I had all the information I needed and it was time to transfer the software to the devices. For this I used the ntc_file_copy module from ntc-ansible.

- name : Copy Pre4 boot image ntc_file_copy : host : " {{ inventory_hostname }}" username : " {{ device_ssh_username }}" password : " {{ device_ssh_password }}" platform : cisco_ios_ssh local_file : " {{ cmts_boot_image_pre4 }}" remote_file : " {{ cmts_boot_image_pre4 }}" file_system : " disk0:" when : pre == 4 - name : Copy Pre4 boot image redundant ntc_file_copy : host : " {{ inventory_hostname }}" username : " {{ device_ssh_username }}" password : " {{ device_ssh_password }}" platform : cisco_ios_ssh local_file : " {{ cmts_boot_image_pre4 }}" remote_file : " {{ cmts_boot_image_pre4 }}" file_system : " stby-disk0:" when : pre == 4 and redundant

The actual playbook had a few more tasks to copy the images for the linecards and different files for the PRE5 systems, this has been omitted for brevity.

Once all the files were in place the boot order needed to be changed. For this i used the ios_config module from the new core networking features of Ansible, which will be shipped with Ansible 2.1.

If the configuration needs to be changed we also want to save the configuration, this is done by notifying a handler. In the below snipped the task to set the boot image for PRE4 has been omitted.

- name : Configure startup ios_config : lines : - boot-start-marker - boot system disk0:{{ cmts_boot_image_pre5 }} - boot-end-marker before : [ ' default boot system' ] replace : block host : ' {{ inventory_hostname }}' username : " {{ device_ssh_username }}" password : " {{ device_ssh_password }}" when : pre == 5 notify : save config handlers : - name : save config ios_command : host : " {{ inventory_hostname }}" username : " {{ device_ssh_username }}" password : " {{ device_ssh_password }}" commands : " write mem"

Rebooting

The above examples will only prepare the devices for a new version and after this they need to be rebooted. You could do this by adding a reload command to the save config handler. But you will probably want to save the actual reboot for a maintenance window. Another playbook could easily be created using the snmp_device_version module to get the current version and if it’s not the desired version issue a reload.

Conclusion

Even though this playbook might not be usable to you as is, I hope it gave you some ideas about how you can work with Ansible to automate your network.