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.
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:
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"
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.
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.