📜 ⬆️ ⬇️

Python for network engineers: getting started

Probably, many network engineers have already realized that the administration of network equipment only through the CLI is too time consuming and unproductive. Especially when running dozens or hundreds of devices, often configured in a single pattern. To remove a local user from all devices, check the configurations of all routers for compliance with some rules, count the number of enabled ports on all switches — these are typical examples of tasks that cannot be solved without automation.



This article is mainly for network engineers who are not familiar with or very little familiar with Python. We will consider an example of a script for solving some practical problems, which you can immediately use in your work.
')

To begin, tell you why I chose Python.

Firstly, it is an easy-to-learn programming language that allows you to solve a very wide range of tasks.

Secondly, large network equipment manufacturers, such as Cisco, Juniper, Huawei, are implementing Python support on their equipment. The language has a future in the network sphere, and learning it will not be a waste of time.

Thirdly, the language is very common. Many useful libraries have been written for it, there is a large community of programmers, and you can find answers to most of the questions on the Internet in the first lines of the search results.

I am engaged in the design and implementation of a little network projects. In one of them it was necessary to solve two problems at once.

  1. Go through several hundred branch routers and make sure that they are configured uniformly. For example, the interface Tunnel1 is used to communicate with the data center, not Tunnel0 or Tunnel99. And that these interfaces are configured in the same way, with the exception of their IP addresses, of course.
  2. Reconfigure all routers, including add a static route through the IP address of the local provider. That is, this command will be unique for each router.

The script came to the rescue in Python. Its development and testing took one day.

The first thing to do is install Python and the highly desirable PyCharm CE. Download and install Python 3 (now the latest version is 3.6.2). When installing, select “Customize installation” and at the stage of “Advanced Options” set a checkbox next to “Add Python to environment variables”.

PyCharm CE is a free development environment with a very convenient debugger. Download and install.

The second step is to install the necessary netmiko library. It is needed to interact with devices via SSH or telnet. Install the library from the command line:

pip install netmiko

The third step is to prepare the source data and the script for our tasks.

We will use the text file “ip.txt” as input data. Each line of the file should contain the IP address of the device to which we connect. A comma can be used to specify a username and password for a specific device. If you do not do this, then those that you enter when running the script will be used. Spaces will be ignored. If the first character in the “#” line, then it is considered a comment and is ignored. Here is an example of a valid file:



The script itself consists of two parts: the main program and the doRouter() function. Inside it, you connect to the router, send commands to the CLI, and receive and analyze responses. The input data for the function are: IP address of the router, login and password. If a problem occurs, the function will return the IP address of the router, we will write it in a separate file fail.txt. If everything went well, a message will simply be displayed on the screen.

Why do we need to make interaction with routers in a separate function, and not to do everything in a loop in the main program? The main reason is the duration of the script. Connecting to all routers in turn took me 4 hours. Mainly due to the fact that some of them did not respond and the script waited a long time for the timeout to expire. Therefore, we will run in parallel in 10 copies of functions. In my case, this reduced the script execution time to 10 minutes.

We now consider the main program in more detail.

For the sake of security, we will not store the login and password in the script. Therefore, we will display an invitation to enter them. And when you enter a password, it will not be displayed. These global variables are used in the doRouter procedure. I had problems with getpass working in PyCharm under Windows. The script worked correctly, only if you run it in Debug mode, not Run. On the command line, everything worked flawlessly. Also, the script was tested in OS X, there were no problems with PyCharm.

 user_name = input("Enter Username: ") pass_word = getpass() 

Then we read the file with IP addresses. The try…except construction will correctly handle the error of reading the file. At the output we get an array of data for connection connection_data , containing the IP address, login and password.

 try: f = open('ip.txt') connection_data=[] filelines = f.read().splitlines() for line in filelines: if line == "": continue if line[0] == "#": continue conn_data = line.split(',') ipaddr=conn_data[0].strip() username=global_username password=global_password if len(conn_data) > 1 and conn_data[1].strip() != "": username = conn_data[1].strip() if len(conn_data) > 2 and conn_data[2].strip() != "": password = conn_data[2].strip() connection_data.append((ipaddr, username, password)) f.close() except: sys.exit("Couldn't open or read file ip.txt") 

Next, create a list of processes and run them. I set the process creation method as “spawn” so that the script works the same on Windows and OS X. The number of created processes will be equal to the number of IP addresses. But no more than 10 will be executed at the same time. In the list of routers_with_issues write what is returned to the doRouter function. In our case, these are the IP addresses of the routers with which there were problems.

 multiprocessing.set_start_method("spawn") with multiprocessing.Pool(maxtasksperchild=10) as process_pool: routers_with_issues = process_pool.map(doRouter, connection_data, 1) process_pool.close() process_pool.join() 

The process_pool.join() command is needed so that the script doRouter() for the completion of all instances of the doRouter() functions and only then continues to execute the main program.

At the end we create / rewrite a text file in which we will have the IP addresses of the unconfigured routers. We also display this list on the screen.

 failed_file = open('fail.txt', 'w') for item in routers_with_issues: if item != None: failed_file.write("%s\n" % item) print(item) 

Now we will analyze the procedure doRouter() . The first thing to do is to process the input. Using ReGex, we verify that the correct IP address was transferred to the function.

 ip_check = re.findall("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", ip_address) if ip_check == []: print(bcolors.FAIL + "Invalid IP - " + str(ip_address) + bcolors.ENDC) return ip_address 

Next, create a dictionary with the necessary data to connect and connect to the router.

 device = { 'device_type': 'cisco_ios', 'ip': ip_address.strip(), 'username': username, 'password': password, 'port': 22, } try: config_ok = True net_connect = ConnectHandler(**device) 

We send commands and analyze the received response from the router. It will be placed in the cli_response variable. In this example, we check the current settings. The result is displayed on the screen. This part needs to be changed for different tasks. In this script, we check the current configuration of the router. If it is correct, then we make changes. If the check config_ok problems, then set the variable config_ok to False and do not apply the changes.

 cli_response = net_connect.send_command("sh dmvpn | i Interface") cli_response = cli_response.replace("Interface: ", "") cli_response = cli_response.replace(", IPv4 NHRP Details", "").strip() if cli_response != "Tunnel1": print(str(ip_address)+" - " + bcolors.WARNING + "WARNING - DMVPN not on Tunnel1. " + cli_response+ " " + bcolors.ENDC) config_ok=False 

Here the following string operations will be useful.

OperationDescriptionExample
+String concatenations3 = s1 + s2
>>> print ('Happy New' + str (2017) + 'Year')
Happy New 2017 Year
len (s)Determining the length of the string
[]Substring selection (index starts from zero)s [5] - the sixth character
s [5: 7] - symbols from the sixth to the eighth
s [-1] - the last character, the same as s [len (s) -1]
s.split ()
s.join ()
Split lines
Combine strings
>>> 'Peter, Lesha, Kohl'.split (', ')
['Petya', 'Lesha', 'Kohl']

>>> ','. join ({'Peter', 'Lesha', 'Kohl'})
'Lesha, Peter, Kohl'
str (l)
list (s)
Convert list to string
Convert string to list
>>> str (['1', '2', '3'])
"['1', '2', '3']"

>>> list ('Test')
['T', 'e', ​​'s', 't']
%Pattern Formatting>>> s1, s2 = 'Mitya', 'Vasilisa'
>>> '% s +% s = love'% (s1, s2)
'Mitya + Vasilisa = love'
fVariable substitution>>> a = 'Maxim'
>>> f'Name {a} '
'Name Maxim'
str.find (substr)Search substr in string str
Returns the position of the first substring found
>>> 'This is a text'.find (' a ')
eight
str.replace (old, new)Replacing the substring old with the substring new in the string str>>> newstr = 'This is a text'.replace (' is', 'is not')
>>> print (newstr)
This is not a text
str.strip ()
str.rstrip ()
Remove spaces and tabs at the beginning and end (or only at the end)>>> 'This is a text \ t \ t \ t'.strip ()
'This is a text'

To solve the problem of adding a static route, first you need to determine the IP address of the next-hop . In my case, the easiest way is to look at the next-hop address of existing static routes.

 cli_response2=net_connect.send_command("sh run | i ip route 8.8.8.8 255.255.255.255") if cli_response2.strip() == "": print(str(ip_address)+" — " + bcolors.FAIL + "WARNING — couldn't find static route to 8.8.8.8" + bcolors.ENDC) config_ok=False ip_next_hop = "" if cli_response2 != "": ip_next_hop = cli_response2.split(" ")[4] if ip_next_hop == "": print(str(ip_address)+" — " + bcolors.FAIL + "WARNING — couldn't find next-hop IP address " + bcolors.ENDC) config_ok=False 

You can send one or more configuration commands at once. I did not work well sending more than 5 teams at the same time, if necessary, you can simply repeat the design several times.

 config_commands = ['ip route 1.1.1.1 255.255.255.255 '+ip_next_hop, 'ip route 2.2.2.2 255.255.255.255 '+ip_next_hop] net_connect.send_config_set(config_commands) 


Full script.
 import sys from netmiko import ConnectHandler from getpass import getpass import time import multiprocessing import re start_time = time.time() class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' def doRouter(connection_data): ip_address = connection_data[0] username = connection_data[1] password = connection_data[2] ip_check = re.findall("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$", ip_address) if ip_check == []: print(bcolors.FAIL + "Invalid IP - " + str(ip_address) + bcolors.ENDC) return ip_address device = { 'device_type': 'cisco_ios', 'ip': ip_address.strip(), 'username': username, 'password': password, 'port': 22, } try: config_ok = True net_connect = ConnectHandler(**device) cli_response = net_connect.send_command("sh dmvpn | i Interface") cli_response = cli_response.replace("Interface: ", "") cli_response = cli_response.replace(", IPv4 NHRP Details", "").strip() if cli_response != "Tunnel1": print(str(ip_address)+" - " + bcolors.WARNING + "WARNING - DMVPN not on Tunnel1. " + cli_response+ " " + bcolors.ENDC) config_ok=False cli_response2=net_connect.send_command("sh run | i ip route 1.1.1.1 255.255.255.255") if cli_response2.strip() == "": print(str(ip_address)+" - " + bcolors.WARNING + "WARNING - couldn't find static route to 8.8.8.8" + bcolors.ENDC) config_ok=False ip_next_hop = "" if cli_response2 != "": ip_next_hop = cli_response2.split(" ")[4] if ip_next_hop == "": print(str(ip_address)+" - " + bcolors.WARNING + "WARNING - couldn't find next-hop IP address " + bcolors.ENDC) config_ok=False if config_ok: config_commands = ['ip route 1.1.1.1 255.255.255.255 '+ip_next_hop, 'ip route 2.2.2.2 255.255.255.255 '+ip_next_hop] net_connect.send_config_set(config_commands) print(str(ip_address) + " - " + "Static routes added") else: print(str(ip_address) + " - " + bcolors.FAIL + "Routes weren't added because config is incorrect" + bcolors.ENDC) return ip_address if config_ok: net_connect.send_command_expect('write memory') print(str(ip_address) + " - " + "Config saved") net_connect.disconnect() except: print(str(ip_address)+" - "+bcolors.FAIL+"Cannot connect to this device."+bcolors.ENDC) return ip_address print(str(ip_address) + " - " + bcolors.OKGREEN + "Router configured sucessfully" + bcolors.ENDC) if __name__ == '__main__': # Enter valid username and password. Note password is blanked out using the getpass library global_username = input("Enter Username: ") global_password = getpass() try: f = open('ip.txt') connection_data=[] filelines = f.read().splitlines() for line in filelines: if line == "": continue if line[0] == "#": continue conn_data = line.split(',') ipaddr=conn_data[0].strip() username=global_username password=global_password if len(conn_data) > 1 and conn_data[1].strip() != "": username = conn_data[1].strip() if len(conn_data) > 2 and conn_data[2].strip() != "": password = conn_data[2].strip() connection_data.append((ipaddr, username, password)) f.close() except: sys.exit("Couldn't open or read file ip.txt") multiprocessing.set_start_method("spawn") with multiprocessing.Pool(maxtasksperchild=10) as process_pool: routers_with_issues = process_pool.map(doRouter, connection_data, 1) # doRouter - function, iplist - argument process_pool.close() process_pool.join() print("\n") print("#These routers weren't configured#") failed_file = open('fail.txt', 'w') for item in routers_with_issues: if item != None: failed_file.write("%s\n" % item) print(item) #Completing the script and print running time print("\n") print("#This script has now completed#") print("\n") print("--- %s seconds ---" % (time.time() - start_time)) 

After preparing the script, you can execute it from the command line or from PyCharm CE. From the command line, run the command:

python script.py

I recommend using PyCharm CE. There we create a new project, a Python file (File → New ...) and paste our script into it. Put the ip.txt file in the folder with the script and run the script (Run → Run)

We get the following result:

 bash ~/PycharmProjects/p4ne $ python3 script.py Enter Username: cisco Password: Invalid IP - 10.1.1.256 127.0.0.1 - Cannot connect to this device. 1.1.1.1 - Cannot connect to this device. 10.10.100.227 - Static routes added 10.10.100.227 - Config saved 10.10.100.227 - Router configured sucessfully 10.10.31.170 - WARNING - couldn't find static route to 8.8.8.8 10.10.31.170 - WARNING - couldn't find next-hop IP address 10.10.31.170 - Routes weren't added because config is incorrect 2.2.2.2 - Cannot connect to this device. #These routers weren't configured# 10.1.1.256 127.0.0.1 217.112.31.170 1.1.1.1 2.2.2.2 #This script has now completed# 

A few words on how to debug the script. The easiest way to do this is in PyCharm. Mark the line on which we want to stop the execution of the script, and run the execution in debug mode. After the script stops, you can see the current values ​​of all variables. Check that the correct data is being transmitted and received. Using the “Step Into” or “Step Into My Code” buttons you can continue the script execution step by step.



Limitations of the described version of the script:


This script was written to solve specific problems. However, it is universal and, I hope, will help someone else in the work. And most importantly - will serve as the first step in the development of Python.

The following resources were used when writing the script:


Alexander Garshin, leading engineer and designer of data transmission systems of Jet Infosystems

Source: https://habr.com/ru/post/336608/


All Articles