Как и когда мы применяем автоматизацию в сети
ИТ-инфраструктуры становятся все более сложными, теперь мы обычно работаем с десятками и сотнями коммутаторов, маршрутизаторов и межсетевых экранов.
Если нам нужно применить одну и ту же команду к нескольким устройствам, то проще всего будет ansible.
Если нам нужно применить одну и ту же команду, но с разными параметрами, то в этом случае Python и netmiko пригодятся.
Используя ansible, мы можем опрашивать несколько коммутаторов несколькими разными командами и записывать вывод команд в текстовые файлы, но с Python и netmiko мы можем объединить вывод нескольких разных команд, записав только нужную нам информацию в один выходной CSV-файл.
Почему CSV? CSV-файл удобен, потому что мы можем открыть его в Excel, и легко скрыть ненужные нам столбцы, сгруппировать или упорядочить по нужным нам столбцам.
Если мы будем ежедневно создавать один такой файл со всех коммутаторов, мы легко сможем увидеть разницу в состоянии подключенных устройств, просто открыв два файла в редакторе Notepad++ в режиме сравнения.
Я объединил три команды, которые мы обычно используем для записи и отслеживания состояния всех коммутаторов и всех подключенных устройств.
Вот команды:
show interface status
show mac address-table
show cdp neighbor
Моя программа на Python обращается ко всем коммутаторам из набора, выполняет все три команды и объединяет вывод всех трех команд в один файл.
Теперь нам не нужно подключаться к каждому коммутатору отдельно и выполнять все три команды одну за другой.
Для демонстрационных целей я создал очень простую инфраструктуру, состоящую из двух коммутаторов и нескольких подключенных устройств.
Выходной файл выглядит так
python программа
#!/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()