How and when we use automation in the network
IT infrastructures are becoming increasingly complex, and we now typically work with tens or hundreds of switches, routers and firewalls.
If we need to apply the same command to multiple devices, then ansible is the easiest way.
If we need to use the same command, but with different parameters, then Python and netmiko will come in handy.
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 of several different commands, writing only the information we need into a single CSV output file.
Why CSV? A CSV file is convenient because we can open it in Excel and easily hide columns we don't need, group them, or organize them by columns we need.
If we create one such file daily from all switches, we can easily see the difference in the state of the connected devices simply by opening the two files in the Notepad++ editor in comparison mode.
I've combined three commands that we typically use to record and monitor the status of all switches and all connected devices.
Here are the commands:
show interface status
show mac address-table
show cdp neighbor
My Python program accesses all switches in a set, executes all three commands, and concatenates the output of all three commands into a single file.
Now we don't need to connect to each switch separately and execute all three commands one after another.
For demonstration purposes, I created a very simple infrastructure consisting of two switches and a few connected devices.
The output file looks like this
python program
#!/usr/bin/python3
# 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()