Backing up your Cisco devices with Ansible

Danny McCaslin
7 min readMar 5, 2021

There are a lot of products out there that can be used to manage your Cisco configuration files. If you are working for a smaller company without a budget for those gadgets, or if you already use Ansible for server configuration and you want to keep your environment simple, it can be a cheap and relatively simple way to manage your Cisco devices.

I’m going to boil this down to three steps:

  1. Configure your Ansible server to communicate with your Cisco devices.
  2. Configure Cisco devices to communicate with Ansible.
  3. Configure your Ansible server to alert you of any configuration changes in your devices.

First, I’m going to assume that you already have Ansible running. There are already some really great tutorials for getting started with Ansible, including this one from Ansible itself. Ansible does a little bit of handwaving around “Make ssh work between Ansible and your client” so I’ll refer you to this article or this article for more details on that. If you haven’t set up Ansible before and you’re only using it for Cisco configuration, you really just need to create a management user for Ansible and run ssh-keygen -t rsa to generate your ssh key (We will use this key later to allow certificate-based login to your network devices.)

After you have the basics in place, you need to set up Ansible to communicate with your devices. You need to edit your /etc/ansible/hosts file to include your Cisco devices. If you’ve installed Ansible on Ubuntu you can use sudo nano /etc/ansible/hosts and use the nano text editor. If you’re on RHEL you will either have to use the vi text editor or install nano with sudo yum install nano . With your text editor of choice, add a group and your devices.

[Cisco_Devices]
cisco-switch-1 ansible_host=192.168.100.10
cisco-switch-2 ansible_host=192.168.100.11
cisco-asa-1 ansible_host=192.168.100.1

Your group block will change depending on your device IP addresses.

Ansible use YAML (*.yml) files for configuration management. One of the features that Ansible allows is the creation of group-specific variables. If you have a group called Cisco_Devices, you can create a yml file in the /etc/ansible/group_vars directory to provide global variables for all devices in the Cisco_Devices group.

$ sudo touch /etc/ansible/group_vars/Cisco_Devices.yml
$ sudo nano /etc/ansible/group_vars/Cisco_Devices.yml

Inside /etc/ansible/group_vars/Cisco_Devices.yml (note that all YAML files begin with three dashes)

---ansible_ssh_user: ansibleuser
ansible_network_os: ios

The ansible_ssh_user and ansible_network_os variables will now be available to all of the hosts in the Cisco_Devices group. Now it’s time to write your playbook. Spacing is of the utmost importance for YAML files. When I first started using Ansible it took many tries to get my first playbook right, and I wound up using that playbook as a template for future playbooks before I got the hang of it. Every space between a dash and a word counts, and every space in YAMLs weird indentions counts as well.

---- hosts: Cisco_Devicesgather_facts: false
connection: network_cli
tasks:- name: get timestamp
command: date +%Y%m%d-%H-%M-%S
register: timestamp
- name: backup ios config
ios_config:
backup: yes
backup_options:
filename: "running-config-{{ timestamp.stdout }}"
dir_path: "/data/{{ inventory_hostname }}"

Let’s walk through this file. hosts identifies the hosts or host groups you are running the playbook against. Ansible has the ability to gather facts from a host and we are opting not to do that. The connection_type tells Ansible that we are connecting to a network_cli connection and not a traditional server.

The first task to run gets the date and time and registers a timestamp. The register statement takes the output of a task and saves it as a variable. The second task backs up the running config of the switch, into a file named “running-config-” appended with the timestamp, into a directory named after the inventory hostname in your /etc/ansible/hosts file under the /data directory. The /data directory is a place where I store all of our device configs on the Ansible server, so you will want to name that whatever you wish. Save the file as cisco-backup.yml

Now, on to your Cisco device. But first, you need to make sure your ansible user has generated its RSA key. Cisco devices have a line length limit so it needs to be outputted in such a way that you can copy each line without exceeding the limit. You can do this with the fold command.

$ fold -b -w 72 /home/ansibleuser/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDsHU1lcssL2Eh8P9oPivoQGgdCFSeHvXY6
HVWgeBtP4abzJg+LsEa0y3LQd0zgEmxRra+s5p5QywNsLZoLdgDYygtfbCUt3rcVVNNWJm89
x1lN6N0w4hr+ugdCes/YhLUZ+y3ZSjw3r+g/f8jSYT/ijS1bEj+A+HRZg/SWlcggGJRGLOyM
XdO9li1/qHSH3+w823SUo3sKiH7XtW1UGhUiikuIDyG7LWrGiGzOaCiwaYBT7Vm9J507cxgw
4SnRWnopLqVbAUua8/zMmJ89BryBs08ywmxGsN3WEr05OWLYwxdW1VkbavXaqXHzXlA7rnI6
n7ErTeY8g0Xuk3PmuRMN

This limits the length of each line to 72 characters. ignore the ssh-rsa and start with the key itself to copy it line by line into your Cisco device.

On the Cisco device you need to create your user.

>ssh administrator@192.168.100.10
Password:
cisco-switch-1>en
Password:
cisco-switch-1#conf t
Enter configuration commands, one per line. End with CNTL/Z.
cisco-switch-1(config)#username ansibleuser privilege 15 secret SuperSecretPassword1
cisco-switch-1(config)#ip ssh pubkey-chain
cisco-switch-1(conf-ssh-pubkey)#username ansibleuser
cisco-switch-1(conf-ssh-pubkey-user)#key-string
cisco-switch-1(conf-ssh-pubkey-data)#AAAAB3NzaC1yc2EAAAADAQABAAABAQDsHU1lcssL2Eh8P9oPivoQGgdCFSeHvXY6
cisco-switch-1(conf-ssh-pubkey-data)#HVWgeBtP4abzJg+LsEa0y3LQd0zgEmxRra+s5p5QywNsLZoLdgDYygtfbCUt3rcVVNNWJm89
cisco-switch-1(conf-ssh-pubkey-data)#x1lN6N0w4hr+ugdCes/YhLUZ+y3ZSjw3r+g/f8jSYT/ijS1bEj+A+HRZg/SWlcggGJRGLOyM
cisco-switch-1(conf-ssh-pubkey-data)#4SnRWnopLqVbAUua8/zMmJ89BryBs08ywmxGsN3WEr05OWLYwxdW1VkbavXaqXHzXlA7rnI6
cisco-switch-1(conf-ssh-pubkey-data)#n7ErTeY8g0Xuk3PmuRMN
cisco-switch-1(conf-ssh-pubkey-data)#exit
cisco-switch-1(conf-ssh-pubkey-user)#exit
cisco-switch-1(conf-ssh-pubkey)#exit
cisco-switch-1(config)#

Enter enable mode and configure terminal. create a user ansibleuser with a secret password with a privilege level of 15. This will allow the user to start out in enable mode. ip ssh pubkey-chain allows you to configure the ssh pubkey and username ansibleuser allows you to edit that user’s key. key-string lets you enter the lines from the key one by one.

Once you’ve finished, move over to your ansible server and log in as your ansibleuser user. Issue the command ssh ansibleuser@192.168.100.10. If you did everything right you should have only to type yes to accept the key and then you will be connected to the device in enable mode. Repeat for all of your devices, using the same username and copying the same key over each time.

Once you’re finished, you should be able to run your ansible playbook against the group.

ansible-playbook cisco-backup.yml

Check your directory to make sure your backups are there.

So, now we want to be alerted of any configuration changes. I’m not solid on shell scripting so I used this article as a chance to learn. Two days and some headaches later I think I got it. Rather than trying to explain line by line, let me just throw it all on here.

#!/bin/sh
#config-checker.sh
#First loop looks at each folder in /data. Each of those folders is named for a network device and contains config filesfor j in `ls /data -1t`do#change directory to the folder for that particular switchcd /data/${j}N=0#Grab the two newest files. Add them to an array called FILEfor i in `ls -1t | head -2`doFILE[$N]=$iN=$N+1done#dif the two files in FILEvar=`diff ${FILE[0]} ${FILE[1]}`#if a diff exists, send an emailif [[ ${var} ]]then#The python script here takes two variables; the name of the device and the content of the diffpython3 /home/dmccaslin/sendEmail.py $j "${var}"fidone

The script is looping through each folder in the directory where you are storing switch configs (in my case, this is /data), then grabbing the two newest files in each folder and performing a diff. diff will output any changes from one file to the other if any changes exist.

I was trying to use my CentOS native mail applications to send email but I just could not get them configured properly. I would get authentication errors and in testing I was getting warnings that the test email (which I was authenticating as myself and sending to myself from myself) could not be sent because I wasn’t allowed to send email from my address when authenticated as me (??). It was then that I remembered that I have a python3 script ready to go just for sending email.

#!/usr/bin/python3#sendEmail.pyfrom email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from config import smtpuser,smtppass,sender
from sys import argv
SWITCHNAME=argv[1]
CONTENT=argv[2]
recipients = ["recipient1@example.com","recipient2@example.com"]
def sendEmail(user,message):
smtpObj = smtplib.SMTP('smtp.office365.com', 587)
smtpObj.ehlo()
smtpObj.starttls()
smtpObj.login(smtpuser,smtppass)
msg = MIMEMultipart()
msg['From'] = sender
msg['To'] = ", ".join(recipients)
msg['Subject'] = 'A diff has appeared from {switchname}'.format(switchname=user)
body ="""<html><head></head><body><h2> A diff has appeared from {switchname}:</h2><p> {message}</p></body></html>""".format(switchname=user,message=message)
msg.attach(MIMEText(body, 'html'))
text = msg.as_string()
smtpObj.sendmail(smtpuser, recipients, text)
smtpObj.quit()
sendEmail(SWITCHNAME,CONTENT)

In my example I use one specified SMTP account to send email through shared mailboxes. I’m storing all of that info in a config.py file which looks something like this:

#SMTP DETAILSsmtpuser = 'smtp@example.com'
smtppass = 'superSecretPassword'
sender = 'ansible@example.com'

Note that you don’t need the #!/usr/bin/python3 shebang at the beginning of this file as it’s not executing anything. It is merely storing variables. Also, you will need to make sure that both your config-checker.sh and sendEmail.py files are executable. Do that with the following commands:

chmod +x sendEmail.py
chmod +x config-checker.sh

The last thing to do is to schedule all of these scripts to run. Right now you can run these scripts and playbooks whenever you want, but we want to schedule these tasks. We will do that with crontab. Crontab is the linux task scheduler. You can list current cron jobs with crontab -l and you can edit crontab with crontab -e. If this is your first time using crontab it will ask which text editor you want to use. On my system the default is vi.

Here’s a little cron cheat sheet I found online:

* * * * * "command to be executed" args
- - - - -
| | | | |
| | | | ----- Day of week (0 - 7) (Sunday=0 or 7)
| | | ------- Month (1 - 12)
| | --------- Day of month (1 - 31)
| ----------- Hour (0 - 23)
------------- Minute (0 - 59)

You can set yours to happen whenever you wish. In my example, I am running /usr/bin/ansible-playbook with the argument /home/ansibleuser/cisco-backup.yml at 2:03 every Monday morning.

3 2 * * 1 /usr/bin/ansible-playbook /home/ansibleuser/cisco-backup.yml

For the config checker, I want that to run not long after I come in on Monday morning.

45 8* * 1 /home/ansibleuser/config-checker.sh

And there you have it! The cron service will run at the appropriate time, and you can sleep well knowing not only that your Cisco device configs are being backed up but that they are being checked for changes as you go.

--

--

Danny McCaslin

Danny McCaslin is a versatile systems administrator who is trying to automate himself out of a job so he can spend his time outdoors.