Netmiko и автоматизация управления коммутаторами Cisco

от автора

Привет, Хабр.

Здесь уже недавно были статьи про netmiko и автоматизацию управления коммутаторами Cisco.

Я хочу продолжить эту тему дальше в контексте взаимодействия сетевого отдела и отдела поддержки пользователей. (DSS digital site support как их называют)

Какие вопросы обычно возникают в процессе взаимодействия?

  • DSS необходимо подключить новых пользователей, или новые принтеры, или новые видеокамеры к коммутаторам и подобные вопросы.

  • DSS посылает запрос в сетевой отдел для настройки нескольких портов на коммутаторах Cisco для подключения устройств.

  • Сетевой отдел должен настроить несколько портов на коммутаторах Cisco в режиме access и соответствующий User vlan или Printer vlan.

Иногда на коммутаторах есть свободные, ранее настроенные порты, но у DSS нет информации для каких vlan эти порты настроены. Поэтому DSS посылает запрос в сетевой отдел.

Моё решение предлагает:

  1. Автоматическую генерацию отчёта о всех портах коммутаторов Cisco в виде excel файла и рассылку этого отчёта в отдел поддержки.

    Имея такую информацию специалисты поддержки могут сразу подключить новых пользователей если они видят свободные порты в коммутаторах и знают что порты в правильном vlan.

    Решение осуществлено на python и может запускаться или каждую ночь по cron, или любой момент из jenkins. В jenkins это просто кнопка «создать отчет».

  2. Специалист DSS может просто отредактировать Excel файл с новыми значениями vlan на требуемых портах и отослать этот файл на исполнение в jenkins и практически сразу сконфигурировать нужные vlan на нужных портах. Сетевой отдел не будет задействован. Эта задача будет ограничена только изменением vlan только на access портах. Порты trunk никак нельзя будет изменить с помощью этого скрипта.

Если вы не знакомы с jenkins, то это бесплатная графическая оболочка вместо командной строки и плюс логи, кто запускал, когда и каков результат.

Что необходимо? Виртуальная машина linux, ansible, python, netmiko, inventory file ansible в формате yaml.

И запускаться задача будет на любой группе свичей из inventory file.

Вот пример inventory file ansible:

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 core_switch:   hosts:     core_switch1:       ansible_host: 192.168.38.141     core_switch2:       ansible_host: 192.168.38.142 sw:   hosts:     access_switch3:       ansible_host: 192.168.38.143     access_switch4:       ansible_host: 192.168.38.144     access_switch5:       ansible_host: 192.168.38.145     access_switch6:       ansible_host: 192.168.38.146     access_switch7:       ansible_host: 192.168.38.147

Вот python программа, которая обращается ко всем коммутаторам из заданной группы и считывает информацию после выполнение команд «show interface status» «show cdp neighbor»

#!/usr/bin/python3  import yaml import argparse from netmiko import ConnectHandler import csv import subprocess  # Function to parse command-line arguments def parse_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 it alive     param = '-c' # for linux os     # Build the command     command = ['ping', param, '1', ip_address]     try:         # Execute the command         subprocess.check_output(command, stderr=subprocess.STDOUT, universal_newlines=True)         return "yes"     except subprocess.CalledProcessError:         return "no"   # Main function def main():     # Parse command-line arguments     args = parse_arguments()     # Load the hosts file     with open(args.hosts_file, 'r') as file:         hosts_data = yaml.safe_load(file)     # Extract global variables     global_vars = hosts_data['all']['vars']     # 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']     comm1='sho int statu | beg Port'     comm2='sho cdp nei | beg Device'      output_filed = args.group + '_inter_des.csv' #      output_filec = args.group + '_inter_cdp.csv' #     STRd = "Hostname,IP_address,Interface,State,Description,Vlan"  #      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"  # with ip     with open(output_filec, "w", newline="") as out_filec:         writer = csv.writer(out_filec)         out_filec.write(STRc)         out_filec.write('\n')    # Connect to each router and execute the specified command     for router_name, router_info in routers.items():         if ping_ip(router_info['ansible_host']) == "no":   # check if host alive              print( ' offline --------- ', router_name,'  ',router_info['ansible_host'])             continue         else:              print( '  online --------- ', router_name,'  ',router_info['ansible_host'])         # Create Netmiko connection dictionary         netmiko_connection = {             'device_type': 'cisco_ios',             'host': router_info['ansible_host'],             'username': global_vars['ansible_user'],             'password': global_vars['ansible_password'],             'secret': global_vars['ansible_become_password'],         }          # Establish SSH connection         connection = ConnectHandler(**netmiko_connection)         # Enter enable mode         connection.enable()         # Execute the specified command         outputd1 = connection.send_command(comm1)         outputd2 = connection.send_command(comm2)         # Print the output         print(f"  ------------ Output from {router_name} ({router_info['ansible_host']}):")         print(f" ")         lines = outputd1.strip().split('\n')         lines = lines[1:]         for line in lines:             swi=router_name             ipad= router_info['ansible_host']             por=line[:9].replace(' ', '')         # port             sta =  line[29:41].replace(' ', '')    # interface connected or notconnected             des =  line[10:28].replace(' ', '')    # existing description             vla = line[42:46].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:                 f.write(STR)                 f.write('\n')         lines1 = outputd2.strip().split('\n')         lines1 = lines1[1:]  # This correctly removes the first line (header)         filtered_lines =  lines1         try:             first_empty_index = filtered_lines.index('')             # Keep only the lines before the first empty line             filtered_lines = filtered_lines[:first_empty_index]         except ValueError:             # No empty line found, do nothing             pass         lines1 = filtered_lines        # cleaned_text         print(' filtered_lines ', filtered_lines)         for line in lines1:             rlin1 =  line[:16]             dot_position = rlin1.find('.')             rlin2 = rlin1[:dot_position]     # remove domain name from name             rlin =  rlin2 + '|' + line[58:67] + '|' + line[68:]             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  # with ip             with open(output_filec, 'a') as f:                 f.write(STRc)                 f.write('\n')         print(f"  ------------ end")         connection.disconnect()    # Disconnect from device     output_filem = args.group + '_merg.csv' #     with open(output_filed, mode='r') as file:         reader = csv.DictReader(file)         sw_inter_des_data = list(reader) # Read the sw_inter_cdp.csv file into a list of dictionaries     with open(output_filec, mode='r') as file:         reader = csv.DictReader(file)         sw_inter_cdp_data = list(reader) # Create a lookup dictionary for sw_inter_cdp_data based on Hostname, IP_address, and Interface     cdp_lookup = {         (row['Hostname'], row['IP_address'], row['Interface']): row['New_Description']         for row in sw_inter_cdp_data     } # Add the New_Description to sw_inter_des_data     for row in sw_inter_des_data:         key = (row['Hostname'], row['IP_address'], row['Interface'])         row['New_Description'] = cdp_lookup.get(key, '')      # Write the updated data to a new CSV file     with open(output_filem, mode='w', newline='') as 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 script if __name__ == '__main__':     main() 

И вот итоговый csv файл:

Hostname,IP_address,Interface,State,Description,Vlan,New_Description access_switch3,192.168.38.143,Gi0/0,connected,PORT00,1,R3725|3725|Fas0/0 access_switch3,192.168.38.143,Gi0/1,connected,PORT11,1, access_switch3,192.168.38.143,Gi0/2,connected,002,1, access_switch3,192.168.38.143,Gi0/3,connected,003,1, access_switch3,192.168.38.143,Gi1/0,connected,sw2|Gig0/0,1,sw2||Gig0/0 access_switch3,192.168.38.143,Gi1/1,connected,011,20, access_switch3,192.168.38.143,Gi1/2,connected,12_012345678901123,22, access_switch3,192.168.38.143,Gi1/3,connected,13_012345678901234,23, access_switch4,192.168.38.144,Gi0/0,connected,sw1|Gig1/0,1,sw1||Gig1/0 access_switch4,192.168.38.144,Gi0/1,connected,,1, access_switch4,192.168.38.144,Gi0/2,connected,,1, access_switch4,192.168.38.144,Gi0/3,connected,,1, access_switch4,192.168.38.144,Gi1/0,connected,,1, access_switch4,192.168.38.144,Gi1/1,connected,,1, access_switch4,192.168.38.144,Gi1/2,connected,,1, access_switch4,192.168.38.144,Gi1/3,connected,,1, 

Выходной файл можно дополнить столбцами mac address, ip address, vendor, lldp neighbor, uptime, downtime и др. Если у вас есть Cisco Call Manager и IP телефоны то можно дополнить столбцом с номером телефона, что значительно облегчит поиск телефонов.

Эта программа на тестовой стадии, я не проверял на стековых коммутаторах, у меня их нет под рукой, я проверял только на виртуальных коммутаторах Cisco. Также можно адаптировать для коммутаторов Juniper и Aruba.

Я буду рад услышать ваши любые комментарии.


ссылка на оригинал статьи https://habr.com/ru/articles/830558/