Tuesday, December 9, 2014

Upgrading IOS-XR Using Python: Cisco AUT

If you have ever upgraded an IOS-XR you definitely are aware that it is a long process which includes so much control steps. Cisco has written a python tool, known as Cisco Automatic  Upgrade Tool, that upgrades an IOS-XR device with pre and post checks. Cisco CPOC video explains nearly everything about this tool which you should watch before start reading this post. Probably the video recorded for an older version of AUT, you will realize that there are little differences between the post and the video. 

Installing The Tool & Prerequisites


The tool works on Linux and Mac cause it uses pexpect module of python so it is not possible to use it on Windows Operating System. Find yourself a Linux/MAC system that is allowed to reach your routers. 


Python Version Check

The script itself does not give any warning about python version if it is old, so be sure that your python version is up to date. I successfully used the tool with python version 2.7.6. I also tried  with version 2.6  and it did not work.

You can check your python version on a Centos system using the below command: 

python -v

Pexpect Module Install

Pexpect python module must be installed on the operating system. Here is the way to install pexpect module on a Centos system:

wget http://pexpect.sourceforge.net/pexpect-2.3.tar.gz
tar xzf pexpect-2.3.tar.gz
cd pexpect-2.3
python ./setup.py install

Cisco AUT Install

You should first download the tool itself. Go the the sourceforge website 
http://sourceforge.net/p/acceleratedupgrade/code/ci/master/tree/ and get the gitclone code. Then: 

python install -d /destination_folder

Cisco AUT


If you have installed all the necessary tools and Cisco AUT , you might start using it by checking which options the script gives, by executing the command below: 

python accelerated_upgrade -h


Below is sample command you might use to upgrade a IOS-XR device.

python accelerated_upgrade --url ssh://admin:yourpassword@11.11.11.11:22  -f packagelist.txt -r tftp://10.0.0.1/ -c  extracommands.txt

As the name reveals itself, packagelist.txt includes the packages we wanted to install on the device. A sample txt file must be like below (based on your needs): 

asr9k-k9sec-px.pie-5.1.3
asr9k-fpd-px.pie-5.1.3
asr9k-mcast-px.pie-5.1.3
asr9k-mpls-px.pie-5.1.3
asr9k-mini-px.pie-5.1.3
asr9k-mgbl-px.pie-5.1.3
asr9k-doc-px.pie-5.1.3


If you wanted to upgrade more than 1 device, you can also create a device.txt file (name it as you want) and the change the command like below: 

python accelerated_upgrade --urls devices.txt -f packagelist.txt -r tftp://10.0.0.1/ -c extracommands.txt

As you already noticed, in the command we gave tftp address where router is going to download the packages from. You may also give ftp link also (including username and password)

Extracommands file, as the name reveals itself again, includes the commands that you might want to execute before upgrade, so that you can compare after the upgrade. Here is a sample file output:

show redundancy summary
show platform
admin show ds
admin show hw-module fpd location all

Pre-checks 

Pre-upgrade steps of the tool are:

  • Node Status Check 
  • Ping Check
  • Disck Space Check
  • Active Package Check 
  • Inactive Package Check 
  • Commited Package Check
  • Redundancy Nde Check 
  • Configuration Backup 
  • Backup CLI Snapshots 
  • Turboboot Check 
You can find detailed information about the pre-check steps in the Cisco CPOC video.

Sample Upgrade 


I upgraded an ASR9010 IOS-XRdevice from 4.2.3 to 5.1.3  using AUT with parameters below:

python accelerated_upgrade --url ssh://admin:yourpassword@11.11.11.11:22  -f packagelist.txt -r tftp://10.0.0.1/ -c extracommands.txt

Unfortunately telnet did not work for me, may be it was related with my router config, but ssh worked. Here are the outputs: 


As you might see in the output, post-check warned me about failed startup config (which is due to changed syntax of MoFRR), and difference between total ospf neighbor count of pre and post upgrade situations. 

FPD Upgrade

In my attempt, script did not do an automatic fpd upgrade, so you need to check and do a fpd upgrade manually. 

Log Files 

Script creates to folders, named "aulogs" and "au_out" in the executed folder, to store logs of the operation. You can see the structure in the screenshot below. 



In aulogs folder, the scripts session to the router is stored. In au_out folder the extracommands (if used) and predetermined commands output, which are taken before upgrade procedure, are stored. 

Thursday, December 4, 2014

Automating Telnet to Cisco/Juniper/Huawei Routers Using Python

When choosing a method for router automation, netconf, ssh, snmp etc are more reliable and stable than telnet but still you may it. I needed a telnet script/library which can execute multiple commands on a router and assign every output to a different variable so that i could. parse related areas.

Telnetlib is a native library in python and it can be used in the same way on all operating systems that support python. You should check the details (like how it is used basically) of the library from https://docs.python.org/2/library/telnetlib.html


In my opinion, a good telnet script for router automatization must support:

  • executing multiple commands in order
  • executing long commands (longer than terminal width)
  • assigning every result to a different variable or print it.
  • JUNOS, IOS-XR, IOS, VRP

SCRIPT

You can download the script files from here if you don't want to read the rest of the post.

We will use class logic in this script. Classes are very useful if you repeat the same tasks again and again. With a class it is possible to create many objects. For example  every telnet session to a different router could be an object of the same class, and every object could have different values. If you don't know much about classes in python, i would suggest you to take a lookout at page Jeff Knupp's page: http://www.jeffknupp.com/blog/2014/06/18/improve-your-python-python-classes-and-object-oriented-programming/

Variables

Login and Password Phrases

When telnetting to a router, different vendors has different login and password phrases. It is possible to wait for these phrases and then send the username or password. The variables  that we will use in the script  to identify these phrases  are:

login_phrase = ["sername:", "ogin:"]
password_phrase = ["assword:"]

You might realize that the first letters are missing, this is done to avoid case sensitivity.

Terminal Length

When you execute a command, a  router will return a limited output cause it has a preconfigured terminal length and if you wanted to see the continuning parts of the result you need to press "enter" key. When using a script we want the router to return an infinite output. To do so every vendor has a different command which we can assign to different variables:

ios_cli_length = " terminal length 0"
junos_cli_length = " set cli screen-length 0"
iosxr_cli_length = " terminal length 0"
vrp_cli_length = " screen-length 0 temporary"

Various Variables

line_break = "\r\n" #need to be send after a command to get it executed like sending "enter" key stroke
timeout_for_reply = 1 #1 second time variable
exclude_start = ("#", "$")

TELNET Class

Init Definition

We need a init definition as we are using a class, so that every object that is created using this class could be identified.

class TELNET(object):
 """connect to hosts"""
 def __init__(self):
  self.connections = []
  self.device_names = []
  self.result_dictionary = {}

You may notice that inside the _init_ definition there is two list and one dictionary variable. We have created these empty  lists and dictionary to pass values between definitions inside the class. You will have a better understanding reading through the other definitions.

Connect Definition

This definition is the longest one in TELNET class. Inside the definition, you may notice that there is a router variable and it is a list. Telnetlib needs router hostname and port number for the connection to get established. And you will also need to enter username and password after getting connected, if it is needed. In "router" the positions of the elements must be like below. Based on your needs, you may enter this variable as a static element in the script file itself or you may make the script read it from a txt/xml file.

router = [ip,software_type,connection_type,port_number,username,password,first_command]
##e.g.
router = [1.2.3.4,ios,telnet,23,admin,password,show ip bgp 8.8.8.8]

 def connect (self, router):
  try:
   connection = telnetlib.Telnet(router[0], router[3])
  except IOError:
   print "IOError, could not open a connection to %s" % router[0]
   return
  """send username"""
  try:
   if router[4] != "":
    connection.expect(login_phrase)
    connection.write(router[4] + line_break)
  except IOError:
   #Send failed
   print "sending username %s failed" % router[4]
   return
  """send password"""
  try:
   if router[5] != "":
    connection.expect(password_phrase)
    connection.write(router[5] + line_break)
  except IOError:
   #Send failed
   print "sending username %s failed" % router[5]
   return

The upper part of the "connect" definition is for getting connected to the router and sending  username and password. The lower part, which you can see below, is for setting the terminal length based on router type and getting the device_name as a variable.

Why we are trying to take the device name as a variable ? Whenever the connection type is telnet, after executing a command you must be sure that you get the whole output. So if there is delay how long should you wait for the output, or how many bytes you should get ? The best way that i could find is to wait for the router_name to be send through the connection. This way even the execution of the command takes long, or there is a delay in network you can be sure that you get the whole output.


  """set terminal length and take device name"""
  try :
   if router[1] == "ios-xr":
    time.sleep(timeout_for_reply)
    connection.write(iosxr_cli_length + line_break)
    device_name = connection.read_until(iosxr_cli_length).split()[-len(iosxr_cli_length.split(' '))]
   elif router[1] == "junos":
    time.sleep(timeout_for_reply)
    connection.write(junos_cli_length + line_break)
    device_name = connection.read_until(">").split()[-1]
   elif router[1] == "vrp":
    time.sleep(timeout_for_reply)
    connection.write(vrp_cli_length + line_break)
    device_name = connection.read_until(vrp_cli_length).split()[-len(vrp_cli_length.split(' '))]
   elif router[1] == "ios":
    time.sleep(timeout_for_reply)
    connection.write(ios_cli_length + line_break)
    device_name = connection.read_until(ios_cli_length).split()[-len(ios_cli_length.split(' '))]
   else:
    print router[1] + " is not an appropriate connection type"
    sys.exit(1)
  except IOError:
   #Send failed
   print "setting terminal length failed"
   return
  self.connections.append(connection)
  self.device_names.append(device_name)

If you examine inside the if confidition for  every router type we wait "timeout_for_reply" second, then send the terminal length command (line break is for "enter" key stroke). And for last we catch the device name by splitting the received data and getting the last value. You may add a "print device_name" statement to see the device name.


At the end of the "connect definition" we created a connections and device_names dictionaries, so that we can pass the active connection to the next definitions

Execute Definition

At this definition the aim is to execute commands, and if needed multiple commands in order. As we get the connection info as a variable from "connect" definition, the definiton is:

 def execute (self, router):
  for conn in self.connections:
   for device in self.device_names:
    conn.write(line_break) #if executing more than one, line break will push device name again and next read_until wont get stuck
    conn.read_until(device)
    conn.write(router[6]+line_break+line_break)
    time.sleep(1)
    catch_end_of_output = [device+" "+line_break, device+line_break]
    self.result_dictionary[router[0]] = conn.expect(catch_end_of_output)[-1]

You may notice that catch_end_of_output is a dictionary and it contains device_name+ line_break elements with and without an empty space character. If you are execu_ing a long command, which is longer than the terminal width, the first part of the command  will be resent back to you from the router including the host name.(you may check this pcap file).It is not enough to wait only for the device name to get the output, as you will get it before executing the command. To solve this problem we can send another line_break after command execution (that is why you see 2 line+breaks for conn.write) and catch device_name+line_break in output.
Why our list has 2 elements with and without space character. This is because some routers return device name after line break with a space character while others send it without a space in order which i realised in various capture files. Close Definition As we get connected to the router with "connect" definition, we also need to close that session. For that purpose the definition is:

 def close(self):
  for conn in self.connections:
   conn.close

Reading Router Info From a Text File


After close definition, the TELNET class is ready to be used. Now we need the script to read connection information from a txt file.

## open routers.txt, clear comments, get all data as "lines" variable
f = open('routers.txt', "r")
lines =  [n for n in f.readlines() if not n.startswith(exclude_start)] #read the lines that does not start with the characters defined in exclude_estart variable
f.close()
total_connections = len(lines) #determine number of routers/connections by counting the lines

#split "lines" variable and turn every line into new variables named as routerx which contains connection infos
for x in range(0, total_connections):
    globals()['router%s' % x] = (lines[x]).split(',')

You may also try getting the data from an xml file. Getting data from an xml file is easier than getting it from a text  cause you wont' need parsing the text file. You may have a look at xml.etree.elementtree python library from https://docs.python.org/2/library/xml.etree.elementtree.html

Connecting, Executing and Printing

It is time to connect and execute the commands as all infos and class are ready. We will create a loop and for every router create an instance of the class (object) so that we session is created and command is executed.

##connect, execute command and print 
##connect, execute command and print  
for x in range(0, total_connections):
 telnet = TELNET()
 telnet.connect(globals()["router"+str(x)])
 telnet.execute(globals()["router"+str(x)])
 result= telnet.result_dictionary[(globals()["router"+str(x)][0])].split("\r\n")
 for line in result:
  print line
 telnet.close

Trial


As everything is ready lets try adding two globally available looking glass and try getting BGP info for 8.8.8.8. The routers.txt file must be in the same folder with the script. Here is the text info:

#Comments here
##
##
route-server.eu.gblx.net,ios,telnet,23,,,show ip bgp 8.8.8.8
route-server.ip.att.net,junos,telnet,23,rviews,rviews,show route protocol bgp 8.8.8.8

Here is the result when we call the script in Windows PowerShell


 

Internetworking Hints Copyright © 2011 -- Template created by O Pregador -- Powered by Blogger