on 11-04-2024 06:54 AM
Network engineers often manage a complex web of routers interconnected by intricate routing protocols, such as OSPF (Open Shortest Path First) and BGP (Border Gateway Protocol). In such environments, troubleshooting or verifying the network's health can be time-consuming. Engineers must typically log into each router individually, execute diagnostic commands, and interpret the results manually. This process, repeated across dozens or even hundreds of devices, becomes not only tedious but also error-prone.
Thankfully, Python offers an efficient solution by automating repetitive tasks. With Python, it’s possible to connect to a large number of routers, issue predefined commands, and compile the results in a structured format, all without manual intervention. This approach not only saves time but also enhances accuracy and scalability.
In one example, I developed a Python program that remotely connects to multiple routers and executes the commands 'show ip interface' and 'show interface' on each device. The program extracts essential details and consolidates them into a single CSV file, making it easier to analyze the network’s status at a glance.
The output CSV includes critical columns like:
One of the program’s advantages is its flexibility; it allows columns to be easily added or removed based on monitoring needs. For example, engineers can expand the CSV output with additional fields as required or streamline it by removing less critical data.
Additionally, this program is designed to provide daily insights into network performance. Each time it runs, it reads and resets all interface counters. By running this program daily, network engineers gain a rolling, day-by-day view of metrics like error counts, allowing them to detect and troubleshoot issues that arise overnight or on a day-by-day basis.
With Python automation, network engineers can transition from repetitive tasks to a more efficient, data-driven approach to network management, enabling quicker identification of issues and long-term performance insights.
In my network automation program, I leverage an Ansible inventory file for device management. This file, which was already in place, lists all routers and their details, simplifying the setup process. However, alternative inventories like Cisco’s Genie or even a simple text file can be used based on network needs.
r4#show ip interface
FastEthernet0/0 is up, line protocol is up
Internet protocol processing disabled
FastEthernet0/0.10 is up, line protocol is up
Internet address is 10.1.34.4/24
Broadcast address is 255.255.255.255
Address determined by non-volatile memory
MTU is 1500 bytes
Helper address is not set
Directed broadcast forwarding is disabled
Outgoing access list is not set
Inbound access list is not set
Proxy ARP is enabled
Local Proxy ARP is disabled
Security level is default
Split horizon is enabled
ICMP redirects are always sent
ICMP unreachables are always sent
ICMP mask replies are never sent
IP fast switching is enabled
IP fast switching on the same interface is enabled
IP Flow switching is disabled
IP CEF switching is enabled
IP CEF Fast switching turbo vector
IP multicast fast switching is enabled
IP multicast distributed fast switching is disabled
IP route-cache flags are Fast, CEF
Router Discovery is disabled
IP output packet accounting is disabled
IP access violation accounting is disabled
TCP/IP header compression is disabled
--More--
-------------- after TEXTFSM parsing 'show ip interface' <class 'list'>
[{'interface': 'FastEthernet0/0', 'link_status': 'up', 'protocol_status': 'up', 'ip_address': '', 'prefix_length': '', 'vrf': '', 'mtu': '', 'ip_helper': [], 'outgoing_acl': '', 'inbound_acl': ''},
{'interface':'FastEthernet0/0.10', 'link_status': 'up', 'protocol_status': 'up', 'ip_address': '10.1.34.4', 'prefix_length': '24', 'vrf': '', 'mtu': '1500', 'ip_helper': [], 'outgoing_acl': '', 'inbound_acl': ''},
{'interface': 'FastEthernet0/1', 'link_status': 'up', 'protocol_status': 'up', 'ip_address': '192.168.38.104', 'prefix_length': '24', 'vrf': '', 'mtu': '1500', 'ip_helper': [], 'outgoing_acl': '', 'inbound_acl': ''},
{'interface': 'Loopback0', 'link_status': 'up', 'protocol_status': 'up', 'ip_address': '4.4.4.4', 'prefix_length': '32', 'vrf': '', 'mtu':'1514', 'ip_helper': [], 'outgoing_acl': '', 'inbound_acl': ''}]
To facilitate connections and data gathering from network devices, the program uses the netmiko library, which simplifies SSH management across a range of network devices. Additionally, I employ textfsm, a library initially developed by Google, to parse command-line output into structured data in the form of a list of dictionaries. This parsing is crucial for generating a consistent CSV output, where each row represents a device and its interface statistics.
For those looking to understand or adapt this program, familiarity with lists and dictionaries in Python, as well as the netmiko and textfsm libraries, is essential. With this approach, network engineers can automate device monitoring, achieve daily updates on error counters, and easily scale their automation across various network environments.
Ansible inventory file in YAML format:
all:
vars:
ansible_user: admin
ansible_password: admin
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: ios
ansible_become: yes
ansible_become_method: enable
ansible_become_password: cisco
ansible_host_key_auto_add: yes
routers:
hosts:
r1:
ansible_host: 192.168.38.101
r2:
ansible_host: 192.168.38.102
r3:
ansible_host: 192.168.38.103
r4:
ansible_host: 192.168.38.104
python:
#!/usr/bin/python3
# Copyright (c) 2023-2024 T.Temirgaliyev The MIT License
# usage " python cisco_.py --hosts_file hosts --group sw1 " define set of switches
import yaml, argparse, csv, subprocess
from netmiko import ConnectHandler
def parse_arguments(): # to parse command-line arguments
parser = argparse.ArgumentParser(description = ' Netmiko Script to Connect to Routers and Run Commands ')
parser.add_argument('--hosts_file', required=True, help = ' Path to the Ansible hosts file ')
parser.add_argument('--group', required=True, help = ' Group of routers to connect to from Ansible hosts file ')
return parser.parse_args()
def ping_ip(ip_address): # Use ping command to check if switch alive
param = '-c' # for linux os
command = ['ping', param, '2', ip_address] # Build the ping command
try:
subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True) # Execute the ping command
return "yes"
except subprocess.CalledProcessError:
return "no"
########### Main function
def main():
args = parse_arguments() # Parse command-line arguments
with open(args.hosts_file, 'r') as file: # Load ansible hosts file in yaml format
hosts_data = yaml.safe_load(file)
global_vars = hosts_data['all']['vars'] # Extract global variables
# Extract router details for the specified group
if args.group not in hosts_data:
print(f"Group {args.group} not found in hosts file.")
return
routers = hosts_data[args.group]['hosts'] # Extract group of devices
output_list = []
# Connect to each switch and execute the specified commands
for router_name, router_info in routers.items(): # loop for each switch in group
if ping_ip(router_info['ansible_host']) == "no": # check if host alive
print( ' switch offline --------- ', router_name,' ',router_info['ansible_host'])
continue
else:
print( ' switch online --------- ', router_name,' ',router_info['ansible_host'])
de_type = ''
if global_vars['ansible_network_os'] == 'ios': # check if cisco ios
de_type = 'cisco_ios'
netmiko_connection = { # Create Netmiko connection dictionary
'device_type': de_type,'host': router_info['ansible_host'],
'username': global_vars['ansible_user'],
'password': global_vars['ansible_password'],
'secret': global_vars['ansible_become_password'],
}
connection = ConnectHandler(**netmiko_connection) # Establish SSH connection
connection.enable() # Enter enable mode
outputd1 = connection.send_command('show interface', use_textfsm=True) # Execute the specified command
outputd2 = connection.send_command('show ip interface ', use_textfsm=True)
connection.send_command("clear counters", expect_string="\[confirm\]") # clear interface counters
connection.send_command("\n", expect_string="#")
connection.disconnect() # Disconnect from device
for entry in outputd2: # Remove square brackets
if entry['ip_address'] > []:
entry['ip_address'] = entry['ip_address'][0]
else:
entry['ip_address'] = ''
if entry['prefix_length'] > []:
entry['prefix_length'] = entry['prefix_length'][0]
else:
entry['prefix_length'] = ''
for port_entry in outputd2: # Loop through the ports_list
input_errors, output_errors, input_rate, output_rate = '', '', '', '' # Initialize as an empty string
for i_entry in outputd1:
if port_entry['interface'] == i_entry['interface']:
input_errors , output_errors = i_entry['input_errors'] , i_entry['output_errors'] # Assign the matching info if found
input_rate , output_rate = i_entry['input_rate'] , i_entry['output_rate']
break
port_entry['input_rate'] , port_entry['output_rate'] = input_rate , output_rate # Add info to the port entry
port_entry['input_errors'] , port_entry['output_errors'] = input_errors , output_errors
outputd2_uswip =[{'switch_ip': router_info['ansible_host'], **d} for d in outputd2]
outputd2_usw =[{'switch': router_name, **d} for d in outputd2_uswip]
output_list.extend(outputd2_usw) # add switch info to total output file
output_filem = args.group + '_merg.csv' # merge 2 in 1 file
with open(output_filem, mode='w', newline='') as file: # Write the updated data to a new CSV file
fieldnames = output_list[0].keys()
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(output_list)
print("New CSV file has been created as ", args.group , '_merg.csv')
#################### Entry point of the main
if __name__ == '__main__':
main()
This is pretty much exactly what the RADKit tool is designed to do: https://radkit.cisco.com/
People might wonder why it’s necessary to generate this CSV file and whether it’s truly beneficial. The answer lies in the program's next functionality: verifying the correctness of routing across the infrastructure.
Routing setups can be highly complex, often involving multiple routing protocols and redistribution. After making changes on some specific routers, it's crucial to ensure that routing remains accurate across the network. To address this, the program includes an automated availability check between routers.
This check uses two nested loops to iterate through each router, issuing a Ping command from each router's loopback interface to the loopback interfaces of all other routers. This allows an immediate verification of connectivity across the network. If every router successfully pings each other router, we know that routing is functioning as expected.
for entry in output_list:
if (entry['interface'] == 'Loopback0') and (entry['switch'] == router_name):
source_ip , swi = entry['ip_address'], entry['switch']
else:
continue
for entry2 in output_list2:
if (entry2['interface'] == 'Loopback0') and (swi != entry2['switch']):
dest_ip = entry2['ip_address']
else:
continue
comm = 'ping '+dest_ip+' repeat 2 source '+source_ip
outputd11 = connection.send_command(comm, use_textfsm=True)
print(router_name,comm,outputd11[0]['response_stream'][0])
Find answers to your questions by entering keywords or phrases in the Search bar above. New here? Use these resources to familiarize yourself with the community: