ИТ-инфраструктуры становятся все более сложными, теперь мы обычно работаем с десятками и сотнями коммутаторов, маршрутизаторов и межсетевых экранов.
Если нам нужно применить одну и ту же команду к нескольким устройствам, то проще всего будет 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()
Комментарии (42)
diksrv
28.09.2024 12:41Зачем это если есть SNMP?
CCNPengineer Автор
28.09.2024 12:41всегда есть много разных решений. я не против вашего решения. если у вас есть пример как запросить по SNMP и все выходные данные собрать в один файл. поделитесь.
net_racoon
28.09.2024 12:41Ну также, скриптом. Но только я так и не понял зачем надо это все собирать?
CCNPengineer Автор
28.09.2024 12:41в одном файле видеть все свичи и все порты. вместо того чтобы подключаться к каждому и запускать три команды последовательно
здесь у меня три команды для примера. на самом деле можно добавить столбцы с номером телефона, вендор, ошибки на порту, имя сервера, имя пользователя итд.
net_racoon
28.09.2024 12:41в одном файле видеть все свичи и все порты. вместо того чтобы подключаться к каждому и запускать три команды последовательно
ну мне кажется эти команды нужно запускать только в случае какого-то дебага. В обычной ситуации зачем это делать?
CCNPengineer Автор
28.09.2024 12:41да. в случае дебага и поиска неисправности моя программа может ускорить поиск. и если по расписанию программа запускается и сохраняет этот файл, всегда будет полезно сравнить а как работал данный принтер вчера? и почему принтер сегодня не работает
sekuzmin
28.09.2024 12:41Могу ошибаться, но выглядит как велосипед в виде самописаного мониторинга/репортинга.
Но net_racoon может задать еще три раза вопрос почему/зачем и мы сможем понять причинно-следственную связь (Five whys)
net_racoon
28.09.2024 12:41Могу ошибаться, но выглядит как велосипед в виде самописаного мониторинга/репортинга.
Вот и я про то же. Мне кажется, как я писал выше, если архитектура сети "правильная", то такие костыли не нужны.
CCNPengineer Автор
28.09.2024 12:41архитектура сети "правильная", с этим вопросов нет.
но при выполнении любых работ я лично раньше сохранял все текстовые файлы до и после.
например даже при заливке нового софта на свичи.
теперь я сохраняю все текстовые файлы плюс один файл до и после выполнения работ.
но сравниваю только один файл
CCNPengineer Автор
28.09.2024 12:41обычная ситуация. вам звонят и говорят что принтер не работает. и хорошо если сообщат мак адрес принтера. вы начнете искать по мак адресу во всех свичах и не найдете если патч корд не подключен.
далее вы начнете искать в всех текстовых файлах мак адресов если у вас есть сохраненные.
но вам могут сообщить только имя принтера. придется искать по имени IP адрес, потом искать мак адрес если есть сохраненные файлы со всех свичей.
в моем случае вся информация в одном файле, что ускорит поиск
sekuzmin
28.09.2024 12:41Если организация работает согласно ITIL/ITSM, то как правило получаешь инцидент из мониторинговой системы или звонка пользователя с указанным CI или именем принтера. Принтер и другие устройства должны быть в CMDB, с актуальной информацией куда он подключен.
Если нет, то полностью согласен, что поиск в текстовом файле быстрее.
CCNPengineer Автор
28.09.2024 12:41Принтер и другие устройства должны быть в CMDB, с актуальной информацией куда он подключен. ?
не знаю где такое есть. тогда моя программа может посылать всю информацию в CMDB, не только принтера но все телефоны, видеокамеры, лаптопы и пр.
CCNPengineer Автор
28.09.2024 12:41здесь конечно многие люди рассмеются " Принтер и другие устройства должны быть в CMDB, с актуальной информацией "
net_racoon
28.09.2024 12:41На принтер вешается QR-code с его именем МАКом и т.п. QR-code привязывается к заявке. Дальше ищете информацию по нему в NMS, он же у вас добавлен в мониторинг?
Если принтер не поддерживает SNMP, то идем на свич и смотрим что на порту происходит. Опять же, привязку МАК-свич можно посмотреть в мониторилке. Железки МАК таблицу умеют отдавать по SNMP.
CCNPengineer Автор
28.09.2024 12:41В обычной ситуации зачем это делать?
есть много обычных ситуаций.
например электрики проводили согласованные работы.
отключали несколько зданий и снова подключили.
а мне нужно достоверно установить что состояние сети вернулось в исходное.
а значит все порты, все мак адреса, все CDP вернулись в исходное состояние.
можно конечно просмотреть сотню другую текстовых файлов и сравнить с предыдущими файлами.
в моем случае нотепад++ и плагин compare
и сразу цветные строки показывают разницу.
net_racoon
28.09.2024 12:41например электрики проводили согласованные работы.
А зачем у вас электрики в сеть лезут?
а мне нужно достоверно установить что состояние сети вернулось в исходное.
Для этого есть мониторинг, он может отслеживать что у устройства изменился сосед, например.
CCNPengineer Автор
28.09.2024 12:41электрики проводили согласованные работы.
отключали несколько зданий и снова подключили.
net_racoon
28.09.2024 12:41отключали несколько зданий и снова подключили.
Ну т.е. электричество отключали? Как тогда это может на сеть повлиять?
sekuzmin
28.09.2024 12:41Я не хочу набрасывать, но выглядит так, что бесперебойного питания для сетевого оборудования нет. Интересно это ограничения бюджета?
net_racoon
28.09.2024 12:41Да я, честно говоря, не понимаю причем тут логическое состояние сети и питание. Если нужно убедиться что все поднялось- есть мониторинг. А тут какие-то скрипты-лесопеды и текстовые файлы.
CCNPengineer Автор
28.09.2024 12:41бесперебойного питания для сетевого оборудования нет. это не ко мне вопрос. у вас нет гарантии что вы не встретите такое в своей жизни.
sekuzmin
28.09.2024 12:41Тут ничего удивительного, если для кастомера это не критично и он не хочет инвестировать.
sekuzmin
28.09.2024 12:41а мне нужно достоверно установить что состояние сети вернулось в исходное.
Т.е. какой-либо системы мониторинга нет?
CCNPengineer Автор
28.09.2024 12:41у нас в одной компании был мониторинг. на аутсорсе. у вас нет гарантии что вы не встретите такое в своей жизни. они мониторили какие то устройства.
Minashvili_George
28.09.2024 12:41+1Спасибо что поделились! Смотивировали попытаться адаптировать и применить у себя ))
sukharichev
А что, во все эти циски до сих пор api нормальные не завезли?
CCNPengineer Автор
есть https://developer.cisco.com/docs/dna-center/overview/#intent-api-northbound
The RESTful Catalyst Center Intent API uses HTTPS verbs (GET, POST, PUT, and DELETE) with JSON structures to discover and control the network. For more information, see Intent API.
вам обязательно нужно HTTPS GET, POST, ?
sukharichev
Да мне-то лично ничего не надо, у меня микроты, линукс и специальнообученный сетевик. Меня просто поражает, что в продуктах за такие деньги и с такой массой ресурсов у разработчика в 2024 году все еще надо дергать cli и парсить питоном консольный вывод, вместо того, чтоб получить нормальный json или хотя бы xml. И что еще более существенно, я так понимаю, конфиги нельзя хранить в гите в тексте, а каждый раз фигачить теми же самыми cli-коммандами из плейбуки.
В микроте этого тоже нет, но он и стоит копейки. А вот если роутер на линуксе, там все бесплатно и прекрасно - ansibe + git раскладывает все по /etc и версионность и идемпотентность. жалко свичей на чистом линуксе нет.
CCNPengineer Автор
" нормальный json или хотя бы xml "
json нужен когда данные передаются с сервера на сервер. например с бакенда на фронтенд.
В моем случае это не требуется. в моем случае мне нужна удобочитаемость. CSV файл открывается в Excel. JSON тоже открывется в Excel, но зачем?
Конфиги Cisco можно хранить очень многими способами. Но моя статья не об этом.
sukharichev
Верно, просто в json или xml они уже структурированы, и парсить их во что угодно, в том числе csv удобнее, чем консольный вывод. Это у цисок он еще сам по себе вменяемый, а у некоторых (того же микрота dhcp leases например), он бывает настолько ужасный и неравномерный, с кучей лишнего, что его парсить сложно.
CCNPengineer Автор
я применяю textfsm для мак адресов
connection.send_command(comm3, use_textfsm=True)
net_racoon
Если архитектура сети правильная, зачем вам что-то дергать и парсить?
Можно, но зачем? Почему просто на FTP не хранить?
sukharichev
Правильная архитектура сети не означает же, что вы никогда не будете выполнять изменения конфигурации, или вам никогда не понадобятся данные с устройств? А дергать их для этого очень удобно по Api, если он полноценный.
И конфиги хранятся в гите, вы видите, кто, когда и зачем поменял (и в чем именно накосячил).
"Можно, но зачем? Почему просто на FTP не хранить?"
Потому что преимущества IaaС начинаются уже от десятков устройств, а хранить бинарные конфиги на фтп это каменный век, наверное?
net_racoon
Если понадобятся я зайду и посмотрю на железку, т.к. там точно самая актуальная и достоверная информация. Если понадобится по многим устройствам- есть для этого NMS и подобные системы, зачем для этого городить свои велосипеды?
Как часто вам такое требуется? Чтобы найти виноватого, можно использовать архив конфигурации+логирование команд на удаленный сервер. Обе эти вещи и так должны быть у вас.
Речь про текстовые конфигурации и про функцию archive на Cisco. Бинарные конфиги это у микротиков.
Pinkbyte
Cumulus Linux вроде ставится на какие-то свичи, но ценник там скорее всего конский
net_racoon
Его можно пощупать в виртуалке.
На самом деле Cumulus - это очень хороший пример. Все это хорошо, пока не начнешь его настраивать. Потому что нормальный CLI скрывает за собой всю внутреннюю кухню ОС устройства. А на "чистом линуксе" все эти пляски придется делать самому и спрашивается зачем это надо?
CCNPengineer Автор
у Cisco есть решение. нужно заплатить много денег, купить лицензии и сервера, потом настроить сервера
и можно будет обращаться к Cisco свичам по "API uses HTTPS verbs (GET, POST, PUT, and DELETE) with JSON "
и можно будет те же самые команды CLI запустить и получить выход в JSON формате, а у меня в CSV.
потребуются фронтенд бакенд тимлид и пр.
sukharichev
вот хотелось бы то же самое, но без отдельных серваков и денег. У китайцев, видимо, из коробки есть:
https://www.juniper.net/documentation/us/en/software/junos/rest-api/index.html
У вьятта-линукс есть
https://docs.vyos.io/en/equuleus/automation/vyos-api.html
И даже у микрота, оказывается, что-то все-таки есть.
https://wiki.mikrotik.com/wiki/Manual:API#Protocol
а у циски либо за деньги, либо в отдельных старших моделях. Непонятно.
axelk
Разве Джунипер китайский? Вроде бы его HPE поглотила недавно
sukharichev
Я почему-то думал, что китайский. даже не знаю, как так вышло :) Вы правы, основаны в США Индусом и американцем, и никогда не имели отношения к Китаю.
CCNPengineer Автор
кстати если заплатить много денег, купить лицензии и сервера, потом настроить сервера Cisco Catalist Center, то можно будет обращаться к серверу по API а сервер будет обращаться к свичу по CLI и сервер будет конвертировать CLI выход в API