on 09-30-2024 05:31 AM
IT infrastructures are becoming increasingly complex, now we usually work with tens and hundreds of switches, routers and firewalls.
If we need to apply the same command to a number of devices, then the easiest way is ansible.
If we need to apply the same command but with different parameters, then in this case python and netmiko useful.
Using ansible we can poll multiple switches with several different commands and write the output of the commands to text files,
but with python and netmiko we can combine the output from several different commands, only the information we need into one output CSV file.
Why CSV ? The CSV file is convenient, because we can open it in the Excel, and easily hide the columns we don’t need, group or order by the columns we need.
If we create such one file daily from all switches, we can easily see the difference in the state of the connected devices, simply by opening two files in notepad++ editor in comparison mode.
I combined three commands that we usually use to record and track the status of all switches, and all connected devices.
These are the commands:
- show interface status
- show mac address-table
- show cdp neighbor
My python program accesses all switches from a predefined set, executes all three commands, and combines the output of all three commands into one file.
Now, we don't need to connect to each switch separately and execute all three commands one after another. We also don't need to search for information in different text files, now all the information is in one file.
For demonstration purposes, I created a very simple infrastructure consisting of two switches and several connected devices.
The output file looks like this, information from "show cdp neighbor" is compressed in one short word and consist of name, platform, remote port, and can be used as a new interface description.
Python program is simple, to understand it you need to know variable types as 'string', 'integer', 'list', 'dictionary' and how to open close text file. It also creates 3 temporary text files, I decided to leave them for a better understanding of the principle of operation.
#!/usr/bin/python3
# Copyright (c) 2023-2024 T.Temirgaliyev The MIT License
# usage " python cisco_switch_info_to_csv.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_filed = args.group + '_inter_des.csv' #
output_filec = args.group + '_inter_cdp.csv' #
output_filema = args.group + '_inter_mac.csv' #
STRd = "Hostname,IP_address,Interface,State,Description,Vlan" # column names status
with open(output_filed, "w", newline="") as out_filed:
writer = csv.writer(out_filed)
out_filed.write(STRd)
out_filed.write('\n')
STRc = "Hostname,IP_address,Interface,New_Description" # column names cdp
with open(output_filec, "w", newline="") as out_filec:
writer = csv.writer(out_filec)
out_filec.write(STRc)
out_filec.write('\n')
STRm = "Hostname,IP_address,Interface,mac,vlan" # column names mac
with open(output_filema, "w", newline="") as out_filema:
writer = csv.writer(out_filema)
out_filema.write(STRm)
out_filema.write('\n')
# 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
comm1 = 'show int status | begin Port'
comm2 = 'show cdp neighb | begin Device'
comm3 = 'show mac addres dynam'
outputd1 = connection.send_command(comm1) # Execute the specified command
if (outputd1.replace(' ', '') == ''):
print(router_info['ansible_host'],' empty -- router , continue with next')
continue # exclude router from switches
outputd2 = connection.send_command(comm2)
outputd31 = connection.send_command(comm3, use_textfsm=True) # mac textfsm
connection.disconnect() # Disconnect from device
print(f" ------------ Output from {router_name} ({router_info['ansible_host']}):") # Print the output
print(' mac textfsm ------- ', type(outputd31))
print(outputd31) # mac textfsm
print(" ------------")
lines = outputd1.strip().split('\n') #### parse 'show interface status'
lines = lines[1:]
for line in lines:
if (line == '') or (line.startswith("Port")):
continue
swi=router_name
ipad= router_info['ansible_host']
por=line[:9].replace(' ', '') # port
sta = line[29:41].replace(' ', '') # interface status connected or notconnect
des = line[10:28].replace(' ', '') # existing description
vla = line[42:47].replace(' ', '') # vlan
print("switch ",swi," port ",por, 'state ',sta," Descr ",des," vlan ", vla )
STR = swi + "," + ipad + "," + por +"," + sta +"," + des + "," + vla # +"," # with ip
with open(output_filed, 'a') as f: # write to file
f.write(STR)
f.write('\n')
lines1 = outputd2.strip().split('\n') #### parse 'show cdp n'
lines1 = lines1[1:] # This correctly removes the first line (header)
for line in lines1:
if (line == '') or (line.startswith("Devic")):
continue
rlin1 = line[:16]
dot_position = rlin1.find('.')
rlin2 = rlin1[:dot_position] # remove domain name from switch name
rlin = rlin2 + '|' + line[58:67] + '|' + line[68:] # new interface description
ndes = rlin.replace(' ', '') # remove all spaces
por=line[17:33]
por1 = por[0:2]+por[3:33] # remove 3rd char from port name
por=por1.replace(' ', '')
swi=router_name
ipad= router_info['ansible_host']
print("switch ",swi," port ",por, " Descr ", ndes )
STRc = swi + "," + ipad + "," + por +"," + ndes # switch name with ip
with open(output_filec, 'a') as f:
f.write(STRc)
f.write('\n')
print(f" ------------ end")
###### --------------------------------------------- #### parse 'show mac address-table' texfsm
for entry in outputd31: # Remove square brackets from 'destination_port' values
entry['destination_port'] = entry['destination_port'][0]
outputd31_sorted = sorted(outputd31, key=lambda x: x['destination_port']) # Sort the list by 'destination_port'
unique_data31 = []
ports_seen = {}
# Count occurrences of each port
for entry in outputd31_sorted:
port = entry['destination_port']
if port in ports_seen:
ports_seen[port] += 1
else:
ports_seen[port] = 1
# Keep only ports that appear once
unique_data31 = [entry for entry in outputd31_sorted if ports_seen[entry['destination_port']] == 1]
# Output the result
for entry in unique_data31:
print(entry)
STRm = swi + "," + ipad + "," +entry['destination_port'] + "," +entry['destination_address'] + "," + entry['vlan_id'] #
with open(output_filema, 'a') as f:
f.write(STRm)
f.write('\n')
output_filem = args.group + '_merg.csv' # mrge 2 in 1
with open(output_filed, mode='r') as file:
reader = csv.DictReader(file)
sw_inter_des_data = list(reader) # Read descr file into a list of dictionaries
with open(output_filec, mode='r') as file:
reader = csv.DictReader(file)
sw_inter_cdp_data = list(reader) # Read cdp file into a list of dictionaries
with open(output_filema, mode='r') as file:
reader = csv.DictReader(file)
sw_inter_mac_data = list(reader) # Read mac file into a list of dictionaries
cdp_lookup = { # Create a lookup dictionary for sw_inter_cdp_data based on Hostname, IP_address, and Interface
(row['Hostname'], row['IP_address'], row['Interface']): row['New_Description']
for row in sw_inter_cdp_data
}
mac_lookup = { # Create a lookup dictionary for sw_inter_cdp_data based on Hostname, IP_address, and Interface
(row['Hostname'], row['IP_address'], row['Interface']): row['mac']
for row in sw_inter_mac_data
}
for row in sw_inter_des_data:
key = (row['Hostname'], row['IP_address'], row['Interface'])
row['New_Description'] = cdp_lookup.get(key, '') # Add the New_Description to sw_inter_des_data
row['mac'] = mac_lookup.get(key, '') # Add mac
with open(output_filem, mode='w', newline='') as file: # Write the updated data to a new CSV file
fieldnames = sw_inter_des_data[0].keys()
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(sw_inter_des_data)
print("New CSV file with added New_Description column has been created as ", args.group , '_merg.csv')
#################### Entry point of the main
if __name__ == '__main__':
main()
Amazing @Tagir Temirgaliyev! Thanks for sharing!
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: