In the spirit of "Automate Everything" I was tasked with scripting some oft needed tasks on Cisco Switches. It's been a while since I've had to do anything even remotely related to switches, so I thought I'd start by googling for some ways to automate tasks on switches. What I found:
Both seemed to be able to get the job done quite well. Unfortunately it turns out that the source for sw_script is actually nowhere to be found and Trigger wouldn't even install properly, giving me a whole plethora of compiler errors. Since I was rather time constrained, I decided to fall back to good old Expect.
Expect s a framework to automate interactive applications. Basically what it does is let the user insert text into the input of the program, and then watches the output of the program for specific occurrences of text, hence the name "Expect". For example, consider a program that requires the user to enter a username and password. It lets the user know this by giving us prompts:
$ ftp host.local Username: Password:
We can use Expect to scan the output of the program and respond with the username and password when appropriate:
spawn ftp host.local expect "Username:" send "fboender\r" expect "password:" send "sUp3rs3creT\r"
It's a wonderful tool, but error handling can be somewhat tricky, as you'll see further in this article.
Scripting a Cisco switch
There is an excellent Expect library for Python called Pexpect. Installation on Debian-derived systems is as easy as "
aptitude install python-pexpect".
Here's an example session on a Cisco switch we'll automate with Expect in a bit:
$ ssh firstname.lastname@example.org Password: Switch>enable Password: Enter configuration commands, one per line. End with CNTL/Z. Switch(config)#interface Gi2/0/2 Switch(config-if)#switchport access vlan 300 Switch(config-if)#no shutdown Switch(config-if)#end Switch#wr mem Building configuration... [OK] Switch#quit
This is a simple manual session that changes the Vlan of switch port "
Gi2/0/2" to Vlan 300. So how do we go about automating this with PExpect?
The first step is to log in. This is fairly easy:
import pexpect switch_ip = "10.0.0.1" switch_un = "user" switch_pw = "s3cr3t" switch_port = "Gi2/0/2" switch_vlan = 300 child = pexpect.spawn('ssh %s@%s' % (switch_un, switch_ip)) child.logfile = sys.stdout child.timeout = 4 child.expect('Password:') child.sendline(switch_pw) child.expect('>')
First we import the pexpect module. Then we spawn a new process "
ssh email@example.com". We set the process' logfile to sys.stdout. This is merely for debugging purposes. It tells PExpect to show all the output it's receiving on our terminal. The default timeout is set to 4 seconds.
Then comes the first juicy bit. We let Expect know that we expect to see a 'Password:' prompt. If something goes wrong, for instance the switch at 10.0.0.1 is down, expect will wait for 4 seconds, looking for the text 'Password:' in SSH's output. Of course, it won't get that prompt since the switch is down. It will then raise a
pexpect.TIMEOUT exception after 4 seconds. If it does detect the 'Password:' prompt, it will then send the switch password and wait until it detects the prompt.
If we want to catch errors and show the user somewhat helpful error messages, we can use try/except clauses:
try: child.expect('Password:') except pexpect.TIMEOUT: raise OurException("Login prompt not received")
After the password prompt, we send the password. If all goes well, we'll receive the prompt. Otherwise the switch will ask for the password again. We don't "expect" this, so PExpect will timeout once again while waiting for the ">" prompt.
try: child.sendline(switch_pw) child.expect('>') except pexpect.TIMEOUT: raise OurException("Login failed")
Let's jump ahead a bit and look at the next interesting problem. What if we supply the wrong port? The switch will respond like so:
Switch(config)#interface Gi2/0/2 ^ % Invalid input detected at '^' marker.
If, on the other hand, our port is correct, we'll simply get a prompt:
So here we have two possible scenario's. Something goes wrong, or it goes right. How do we detect this? We can tell Expect that we expect two different scenario's:
o = child.expect(['\(config-if\)#', '% Invalid']) if o != 0: raise OurException("Unknown switch port '%s'" % (port))
The first scenario '\(config-if\)#' is our successful one. The second is when an error occurred. We then simply check that we got the successful one, and otherwise raise an error.
The rest of the script is just straight-forward expects and sendline's.
The full script
Here's the full script:
import pexpect switch_ip = "10.0.0.1" switch_un = "user" switch_pw = "s3cr3t" switch_enable_pw = "m0r3s3cr3t" port = "Gi2/0/2" vlan = 300 try: try: child = pexpect.spawn('ssh %s@%s' % (switch_un, switch_ip)) if verbose: child.logfile = sys.stdout child.timeout = 4 child.expect('Password:') except pexpect.TIMEOUT: raise OurException("Couldn't log on to the switch") child.sendline(switch_pw) child.expect('>') child.sendline('terminal length 0') child.expect('>') child.sendline('enable') child.expect('Password:') child.sendline(switch_enable_pw) child.expect('#') child.sendline('conf t') child.expect('\(config\)#') child.sendline('interface %s' % (port)) o = child.expect(['\(config-if\)#', '% Invalid']) if o != 0: raise Exception("Unknown switch port '%s'" % (port)) child.sendline('switchport access vlan %s' % (vlan)) child.expect('\(config-if\)#') child.sendline('no shutdown') child.expect('#') child.sendline('end') child.expect('#') child.sendline('wr mem') child.expect('[OK]') child.expect('#') child.sendline('quit') except (pexpect.EOF, pexpect.TIMEOUT), e: error("Error while trying to move the vlan on the switch.") raise
It's too bad that I couldn't use any of the existing frameworks. I could have tried getting Trigger to compile, but I was time constrained so I didn't bother. There are other ways of configuring Switches too. SNMP is one way, but it is complex and prone to errors. I believe it's also possible to retrieve the entire configuration from a switch, modify it and put it back. This is partly what Rancid does. However that would require even more time.
Expect was a good fit in this case. Although it too is rather error prone, it's fairly easy to catch errors as long as you're expecting (no pun intended) them. I strongly suggest you give Trigger a try before falling back to Expect. It seems like a very decent tool.