As part of a project to migrate from one MPLS carrier to a new one we were faced with the challenge of deploying a consistent, correct configuration to each of 450 remote sites. Each of these remote sites is similar in terms of the number of subnets, IP addressing schemes, and router models. Unfortunately there are are also a number of differences. While the IP schemes follow the same format for each location, each one has it’s own subnet assigned, the interfaces can change between FastEthernet or Gigabit depending on the router type, and the number of hosts in each location that are restricted via ACL varies widely. As part of this new project there were a total of four different configurations that a remote location could receive. While it may have been possible to configure each remote site manually, choosing the correct template to follow as we went along, it opened the doors for a huge amount of error. A typo in an IP address on an interface that went unnoticed may not be caught until later on, a typo in an ACL could open us up to security issues, and applying the wrong template to a location could cause wasted time on troubleshooting. Enter scripting.
Some background on scripting
I went to an engineering school and majored in Computer Engineering. As part of the standard curriculum I got to take a few programming classes. I hated them. With a passion. “I’ll never use this”… “Im getting into networking, why waste my time on this”. I kept that mentality through school, graduated with my BE without any issues. Got my first job as a network technician, got my CCNA shortly after, never having touched programming. My boss at the time was extremely into Perl. At the time this is what it came across as in my head:
He had written Perl scripts to accomplish pretty much anything you could think of. While I could appreciate what they did, I just didn’t have the interest to jump into it, my main focus was networking and there was plenty of networking to learn.Eventually some project came up where I dove into some Perl and came up with a script that did something (I don’t actually remember what it did, it didn’t cure cancer, but it did something to solve a problem), and while it was a good feeling, still didn’t stick with me.
Eventually I changed jobs and saw more and more problems come up that needed solutions. Call Detail Records that needed reporting without the money for a professional product. I wrote (struggled through) a Perl script to search and spit out call records. Would not win any programming competitions but it got the job and I felt good about it. At the end of the day though it was still a struggle to write and I never fully felt like I *got* Perl. Fast forward another year or so without any real scripting and more problems/challenges were starting to present themselves. Requirements to push out changes to 450 remote sites, each of which has different IP addressing or requirements. Doing it manually would take an enormous amount of man hours (even at 5 minutes per location, that’s almost an entire full 40 hour work week, and no one actually had a full work week to dedicate to this. Not to mention it’s mind numbingly boring and open to error). There was no software we owned(or that I found) that would accomplish what I was looking to do so I searched to see what other programming options were out there, and I discovered Python. It was actually a two part Google Python class on Youtube. It immediately clicked with me. It made sense, seemed easy and I was able to jump right in. I was by no means an expert from the start, and still am not, but the amount of resources available on the Internet for Python, and the way the language worked just made sense to me. Since those two videos I’ve gotten deep into Python and it’s allowed me to build the script this blog post is about.
The Problem
This project had four possible templates that could be applied to a site depending on which types of connectivity were available at each site(DMVPN with broadband, DMVPN with cellular, T1, and a combination of those). Each site was identified by a unique number, and had a corresponding IP scheme. For example site 123 might have an IP address scheme 10.1.23.0/24, which was then broken down into smaller subnets. Another difference was the number of hosts each site had that were controlled by access lists. One site may have 2 servers that needed to be controlled by an ACL, while another may have 3 or 4, each with different IP addresses. Since each site was unique, it wasn’t as easy as using our normal change deployment tool to push out a single change everywhere
The Script
Without diving into every line of the script I’ll give you some of the pseudocode and key functions/modules. If you want to know more about how any one piece works feel free to contact me.
Input
Prior to this project starting we already had an Excel file that contained every site, with subnets and IP addressing broken out across a number of different columns, almost 30 in total. In the past this was used mostly for reference and documentation, but would serve as the key input for my script to work.The excel file had a header row of variable names in brackets like [Loopback0] or [DataVlanGW]. Every other row below that is associated with a single site, identified by a unique site ID in one of the columns. I added a new column to this spreadsheet called “TemplateID”. This column had a possible value of 1 – 4, each number corresponding to a certain configuration template.Other columns are things like site id, loopback address ,gateways, subnet masks, etc. Filling this spreadsheet out initially was a manual process but it’s important to remember that it’s a one time effort to fill it out. Once you have this it becomes very powerful for future applications. I exported this from Excel to a CSV and then copied it to our linux server where I’d be building the Python script. Here’s a snippet of what the Excel headers look like:
Templates
I had four possible Templates I’d be using. I built the entire router configuration in Notepad++ and replaced any piece of the config that would differ between stores with a variable name in brackets. In the example below I’m using the variable [Loopback0], which is also a column in the spreadsheet:
interface Loopback0
description Network Management
Interface ip address [Loopback0] 255.255.255.255
no shut
exit
So, four of these templates, each slightly different and then saved as Template#.conf in a directory.
You’ll notice there are more than four templates here, one ‘a’ template, and one ‘b’ template. This was because we had two different router models(2800 and 2900), each of which had slightly different interfaces and IPS configuration. In my script I check the model of the router and choose the appropriate template.
Processing the input
I use the built in Python function csv.reader to read through every row of the CSV file, storing each column(separated by commas in a CSV) as a variable in an array. The technical term for this in Python is a dictionary. So for example, the first column gets stored as “TemplateId”, the second column stored as “Model”, and so on until I store every column for that row into the dictionary. I then use ‘if – then’ logic to check the value of the first column. If templateid = 1 then I load my Template1.conf file, if templateid = 2, then load Template2.conf, etc. Once the Template file is loaded into a variable I run this one command which basically runs a ‘find/replace’ on any variable surrounded by brackets in the template file and replace it with the values currently in the array for that row.
output = replace_words(tempstr, values)
I found the above function ‘replace_words’ after a little Googling. ‘tempstr’ in the above function is the base template file I loaded in, and ‘values’ is the name of the dictionary I stored every column for this site ID in the CSV. I then write this to a new file with the unique file name of the site id. I know the site ID because it was a column in the CSV that I processed, so it’s trivial to save a file called site-###.txt, pulling ### from the array. Because this entire thing is in a ‘for’ loop, it will repeat this logic for every row in the CSV file until it reaches the end.The best part of this is it processes all 450 rows of this file in about 20 seconds. 450 consistent, full, unique Cisco router configs in 20 seconds. I’m not automatically copying them or applying them to every device, but I have the config files on hand at this point. I’ll talk about how I automated pushing them out to each site in the next blog post.
Here’s a snippet of the code accomplishing this first part. This only shows one set of the ‘if-then’ logic for Template #1, but there are 3 other if-thens in the real script to accommodate the other templates.:
script, inputcsvfile = argv
with open(inputcsvfile, “rb”) as infile:
reader = csv.reader(infile)
next(reader, None) #Skip the header line of the CSV
for row in reader:
values = { ‘[Hostname]’:row[2], ‘[Loopback0]’:row[5], ‘[DataVlanNet]’:row[6], ‘[DataVlanGW]’:row[7], ‘[Data2VlanNet]’:row[8], ‘[Data2VlanGW]’:row[9], ‘[WifiVlanNet]’:row[10], ‘[WifiVlanGW]’:row[11], ‘[IPSVlanNet]’:row[16], ‘[IPSVlanGW]’:row[17], ‘[IPSVlanIP]’:row[18], ‘[SwitchIP]’:row[19], ‘[SerialIP]’:row[20], ‘[BGPNeigh]’:row[21], ‘[SerialDesc]’:row[22] } #truncated
outputfile = “outputs/site-“+row[2]+”-NEWCONFIG.txt”
storecopycommandsfile = “StoreCopyCommands/site-“+row[2]+”-r1.txt”
#Next line will check to see what the template ID is set to, and load the appropriate template into variable ‘t’
if row[0] == “1”: #Open the correct template
if row[1] == “2811”:
t = open(“Templates/Template1a.conf”, “r”)
elif row[1] == “2911”:
t = open(“Templates/Template1b.conf”, “r”)
#Store the template into a temp string
tempstr = t.read()
t.close()
#Rip through the template and do a find/replace for the current store number
output = replace_words(tempstr, values)
#Write out the new config file
fout = open(outputfile,”w”)
fout.write(output)
fout.close()
But wait! There’s more!
I mentioned one of the challenges of generating these configs was that each one had an access list with varying numbers of hosts that needed to be included. Since my CSV file didn’t have columns for every host in an ACL I had to turn to an alternative method. For this I relied on three things:
- The current running configurations for each site
- Regular Expressions
- More Python
Current configs
Each site already contained existing ACL entries for each host. Since these were already in production, they were known to be working.Here’s an example of something I might be working with:
permit tcp host 10.10.10.10 10.1.1.0 0.0.0.255 eq 9100
permit tcp host 10.10.10.11 10.1.1.0 0.0.0.255 eq 9100
In this case the host that is unique per site is the source host(10.10.10.10 or 10.10.10.11 in this example), and the destination network and port stay consistent from site to site. This example shows two source hosts, other sites may have less or more. It doesn’t matter as you’ll see below.
Regular Expressions (Regex)
If you aren’t familiar with regular expressions I’d strongly recommend you research them and start to practice. Regular expressions allow you to search for any number of patterns in text and not only match on them, but also store them for further processing. For learning regular expressions, there is an excellent site I found called www.pythex.org. This lets you enter in the text you want to search in one box, and the regular expression you are testing in another, and will highlight what matches and what doesn’t. It’s an excellent tool for when you are starting out, or when you just need to troubleshoot why something isn’t matching correctly.
In the example ACL above I need to identify the unique source hosts from each line and then do something with them. The regular expression I used to match this is:
permit tcp host (\d+.\d+.\d+.\d+) 10.1.1.0 0.0.0.255 eq 9100
If you’ve never looked at a regular expression before it can be intimidating, but if you break it down piece by piece it isn’t too bad. Here is what it is searching for:
- Search for the text ‘permit tcp host ‘
- Now look for a digit(\d) that repeats one or more times (+) followed by a ‘.’, repeated 3 more times. This should look like an IP address to you.
- I want to put parentheses around this entire IP address to save it to a variable for further use
- The IP address should be followed by the text ‘10.1.1.0 0.0.0.255 eq 9100’
The regex I used isn’t perfect, but it isn’t necessarily wrong either. There are a number of different ways you could write it, some much more accurate than others. For example, I am just checking that there is one or more repeating digits before the ‘.’ but I don’t check that they are valid for use in an IP address, so it would match things like 999.999.999.999. For my specific use case it doesn’t matter because I’m relying on the current running config(which Cisco already validated when it was entered into the router) as a valid IP address. Here is what this looks like on the Pythex site:
Python
Now that I searched for the occurrence of the source host, I need to tie it all together with Python. Here’s the pseudo code for this piece:
- Read every line of the current running config, searching for the regular expression from above
- If you find a match, store the matching piece(The IP address in () ) into a list in Python, until you get to the end of the file
- Open the new router config we generated in the previous “Processing the input” section
- Loop through this file and search for the string “!Inserted ACL”. This was text I included in each template to server as a marker for a place I want to insert these site specific access list entries.Since it has an ‘!’ at the beginning, it doesn’t interfere with the Cisco config, but still allows me to search for it.
- If you find the string “!Inserted ACL” in the file, then replace it with the site specific access list entries we just found using the regular expressions
- Repeat this for each match we had for the regular expression
- Save the file
Here’s a snippet of the actual code:
existingconfig = open(myfilename)
serverips = []
for line in existingconfig:
servermatch = re.search(r’permit tcp host (\d+.\d+.\d+.\d+) 10.1.1.0 0.0.0.255 eq 9100′,line) #Match the regex
if servermatch:
serverips.append(servermatch.group(1)) #If we get a match add it to the list called serverips
serverips = list(set(serverips)) #This just gets the unique values from the list and then saves back to the list
checkforinsert = False #initialize this variable
for line in fileinput.input(outputfile, inplace=1): #loop through our initial saved config file
if line.startswith(‘!InsertedObjectGroups’): #Search each line for the string “!InsertedObjectGroups”
checkforinsert = True #If we find the string, then change the variable checkforinsert to true
else:
if checkforinsert: #if checkforinsert is true, then let’s print out the new object group, using the matches from our regex
print “\nobject-group network Server_Ips”
print “description Server IPs”
for num in serverips: #loop through the Python list for each server IP found
print ” host “,num
print line, #continue to print the lines of text
Disclaimer: Python is very strict about indentation. I’m still working on finding a good way to include pieces of code in the blog, and all of the indents in the above snippet probably don’t line up correctly and would error out, but it should give you an idea of what I’m doing. This is what we end up with after it writes the config:
!InsertedObjectGroups
object-group network Server_Ips
description Server IPs
host 10.10.10.10
host 10.10.10.11
Since the entire thing is in a for loop it doesn’t matter if the site had 1 host or 1,000 hosts. It will just read through the entire file, saving the matches it finds, and then spit them back out into the new config. Again, this entire piece combined with the previous section only took about 20 seconds to run for 450 sites. If you had to read through every ACL at the time of a cutover to search for unique hosts per site you would almost be guaranteed to miss one here and there, or type it incorrectly. This greatly reduces that margin of error, and saves you huge amounts of headache.
Testing
Make sure you test everything you are writing. Run your script, check the output, apply it to a lab router and see where it errors out. Run it against multiple test cases to make sure you’ve accounted for any anomalies that may come up. Don’t try to write the entire thing in one shot. The worst thing you could do is try to conquer a large problem with your first script and have it blow up, creating more damage than if you had just configured your devise manually. Small steps are good and as you become more confident it will become much easier. The idea is to make your life easier, not to get yourself fired because you took on a problem too large for your current skill set.
Wrapping it up
While this post didn’t cover every detail of coding in Python, I hope it gave you enough of a taste to see what is possible once you start to get into it. I think it’s important to note that I didn’t jump into this as my first script. A number of the pieces in this script came from other previous scripts that were much simpler. Once you learn how to do each small piece, it becomes easier to combine them all together and really build something that can give you results. One of the biggest hang ups I had when starting out was I wanted everything to be ‘perfect code’, the most efficient, streamlined code ever written. It doesn’t work like that though, it’s a process. What I wrote can definitely be cleaned up, optimized, and just generally improved, but at the end of the day it solved the problem that I needed to solve and I consider it a success. I took a task that previously would have taken 40 hours + and reduced it to 20 seconds. Going forward I can review the code I wrote, research some more Python and optimize it, but for right now it got the job done and I’m happy.
Simply wish to say your article is as surprising.
The clearness for your publish is just great and that i can suppose you’re knowledgeable on this subject.
Well with your permission allow me to grab your feed to stay updated with drawing close post.
Thanks a million and please carry on the enjoyable work.
Pingback: Automation: Making Better networks | not (always) the network
First, thanks for the post, and well done! I’m looking to do something very similar, and have working VB scripts to accomplish essentially the same thing. However, for many reasons, I’m inclined to migrate to Python for these types of tasks and your blog post is the closest I have found for what I’m trying to achieve. Any chance you would be willing to share or email me the script you engineered? Would love to use this as a resource.
Very nice blog, thank you!
David