ИТ-инфраструктуры становятся все более сложными, теперь мы обычно работаем с десятками и сотнями коммутаторов, маршрутизаторов и межсетевых экранов.

Если нам нужно применить одну и ту же команду к нескольким устройствам, то проще всего будет 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()

Комментарии (28)


  1. sukharichev
    28.09.2024 12:41

    А что, во все эти циски до сих пор api нормальные не завезли?


    1. CCNPengineer Автор
      28.09.2024 12:41
      +1

      есть 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, ?


      1. sukharichev
        28.09.2024 12:41
        +2

        Да мне-то лично ничего не надо, у меня микроты, линукс и специальнообученный сетевик. Меня просто поражает, что в продуктах за такие деньги и с такой массой ресурсов у разработчика в 2024 году все еще надо дергать cli и парсить питоном консольный вывод, вместо того, чтоб получить нормальный json или хотя бы xml. И что еще более существенно, я так понимаю, конфиги нельзя хранить в гите в тексте, а каждый раз фигачить теми же самыми cli-коммандами из плейбуки.
        В микроте этого тоже нет, но он и стоит копейки. А вот если роутер на линуксе, там все бесплатно и прекрасно - ansibe + git раскладывает все по /etc и версионность и идемпотентность. жалко свичей на чистом линуксе нет.


        1. CCNPengineer Автор
          28.09.2024 12:41
          +1

          " нормальный json или хотя бы xml "

          json нужен когда данные передаются с сервера на сервер. например с бакенда на фронтенд.

          В моем случае это не требуется. в моем случае мне нужна удобочитаемость. CSV файл открывается в Excel. JSON тоже открывется в Excel, но зачем?

          Конфиги Cisco можно хранить очень многими способами. Но моя статья не об этом.


          1. sukharichev
            28.09.2024 12:41

            Верно, просто в json или xml они уже структурированы, и парсить их во что угодно, в том числе csv удобнее, чем консольный вывод. Это у цисок он еще сам по себе вменяемый, а у некоторых (того же микрота dhcp leases например), он бывает настолько ужасный и неравномерный, с кучей лишнего, что его парсить сложно.


            1. CCNPengineer Автор
              28.09.2024 12:41
              +1

              я применяю textfsm для мак адресов

              connection.send_command(comm3, use_textfsm=True)


        1. net_racoon
          28.09.2024 12:41

           Меня просто поражает, что в продуктах за такие деньги и с такой массой ресурсов у разработчика в 2024 году все еще надо дергать cli и парсить питоном консольный вывод

          Если архитектура сети правильная, зачем вам что-то дергать и парсить?

          конфиги нельзя хранить в гите в тексте

          Можно, но зачем? Почему просто на FTP не хранить?


          1. sukharichev
            28.09.2024 12:41

            Правильная архитектура сети не означает же, что вы никогда не будете выполнять изменения конфигурации, или вам никогда не понадобятся данные с устройств? А дергать их для этого очень удобно по Api, если он полноценный.
            И конфиги хранятся в гите, вы видите, кто, когда и зачем поменял (и в чем именно накосячил).
            "Можно, но зачем? Почему просто на FTP не хранить?"
            Потому что преимущества IaaС начинаются уже от десятков устройств, а хранить бинарные конфиги на фтп это каменный век, наверное?


            1. net_racoon
              28.09.2024 12:41

              Правильная архитектура сети не означает же, что вы никогда не будете выполнять изменения конфигурации, или вам никогда не понадобятся данные с устройств?

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

              И конфиги хранятся в гите, вы видите, кто, когда и зачем поменял (и в чем именно накосячил).

              Как часто вам такое требуется? Чтобы найти виноватого, можно использовать архив конфигурации+логирование команд на удаленный сервер. Обе эти вещи и так должны быть у вас.

              а хранить бинарные конфиги на фтп это каменный век, наверное?

              Речь про текстовые конфигурации и про функцию archive на Cisco. Бинарные конфиги это у микротиков.


    1. CCNPengineer Автор
      28.09.2024 12:41

      у Cisco есть решение. нужно заплатить много денег, купить лицензии и сервера, потом настроить сервера

      и можно будет обращаться к Cisco свичам по "API uses HTTPS verbs (GET, POST, PUT, and DELETE) with JSON "

      и можно будет те же самые команды CLI запустить и получить выход в JSON формате, а у меня в CSV.

      потребуются фронтенд бакенд тимлид и пр.


      1. sukharichev
        28.09.2024 12:41

        вот хотелось бы то же самое, но без отдельных серваков и денег. У китайцев, видимо, из коробки есть:
        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
        а у циски либо за деньги, либо в отдельных старших моделях. Непонятно.


        1. axelk
          28.09.2024 12:41
          +1

          Разве Джунипер китайский? Вроде бы его HPE поглотила недавно


          1. sukharichev
            28.09.2024 12:41

            Я почему-то думал, что китайский. даже не знаю, как так вышло :) Вы правы, основаны в США Индусом и американцем, и никогда не имели отношения к Китаю.


        1. CCNPengineer Автор
          28.09.2024 12:41
          +1

          кстати если заплатить много денег, купить лицензии и сервера, потом настроить сервера Cisco Catalist Center, то можно будет обращаться к серверу по API а сервер будет обращаться к свичу по CLI и сервер будет конвертировать CLI выход в API


  1. diksrv
    28.09.2024 12:41

    Зачем это если есть SNMP?


    1. CCNPengineer Автор
      28.09.2024 12:41

      всегда есть много разных решений. я не против вашего решения. если у вас есть пример как запросить по SNMP и все выходные данные собрать в один файл. поделитесь.


      1. net_racoon
        28.09.2024 12:41

        Ну также, скриптом. Но только я так и не понял зачем надо это все собирать?


        1. CCNPengineer Автор
          28.09.2024 12:41

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

          здесь у меня три команды для примера. на самом деле можно добавить столбцы с номером телефона, вендор, ошибки на порту, имя сервера, имя пользователя итд.


          1. net_racoon
            28.09.2024 12:41

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

            ну мне кажется эти команды нужно запускать только в случае какого-то дебага. В обычной ситуации зачем это делать?


            1. CCNPengineer Автор
              28.09.2024 12:41

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


              1. sekuzmin
                28.09.2024 12:41

                Могу ошибаться, но выглядит как велосипед в виде самописаного мониторинга/репортинга.

                Но net_racoon может задать еще три раза вопрос почему/зачем и мы сможем понять причинно-следственную связь (Five whys)


                1. net_racoon
                  28.09.2024 12:41

                  Могу ошибаться, но выглядит как велосипед в виде самописаного мониторинга/репортинга.

                  Вот и я про то же. Мне кажется, как я писал выше, если архитектура сети "правильная", то такие костыли не нужны.


                  1. CCNPengineer Автор
                    28.09.2024 12:41

                    архитектура сети "правильная", с этим вопросов нет.

                    но при выполнении любых работ я лично раньше сохранял все текстовые файлы до и после.

                    например даже при заливке нового софта на свичи.

                    теперь я сохраняю все текстовые файлы плюс один файл до и после выполнения работ.

                    но сравниваю только один файл


                1. CCNPengineer Автор
                  28.09.2024 12:41

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

                  далее вы начнете искать в всех текстовых файлах мак адресов если у вас есть сохраненные.

                  но вам могут сообщить только имя принтера. придется искать по имени IP адрес, потом искать мак адрес если есть сохраненные файлы со всех свичей.

                  в моем случае вся информация в одном файле, что ускорит поиск


                  1. sekuzmin
                    28.09.2024 12:41

                    Если организация работает согласно ITIL/ITSM, то как правило получаешь инцидент из мониторинговой системы или звонка пользователя с указанным CI или именем принтера. Принтер и другие устройства должны быть в CMDB, с актуальной информацией куда он подключен.

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


                    1. CCNPengineer Автор
                      28.09.2024 12:41

                      Принтер и другие устройства должны быть в CMDB, с актуальной информацией куда он подключен. ?

                      не знаю где такое есть. тогда моя программа может посылать всю информацию в CMDB, не только принтера но все телефоны, видеокамеры, лаптопы и пр.


                  1. net_racoon
                    28.09.2024 12:41

                    На принтер вешается QR-code с его именем МАКом и т.п. QR-code привязывается к заявке. Дальше ищете информацию по нему в NMS, он же у вас добавлен в мониторинг?

                    Если принтер не поддерживает SNMP, то идем на свич и смотрим что на порту происходит. Опять же, привязку МАК-свич можно посмотреть в мониторилке. Железки МАК таблицу умеют отдавать по SNMP.


  1. Minashvili_George
    28.09.2024 12:41
    +1

    Спасибо что поделились! Смотивировали попытаться адаптировать и применить у себя ))