Как и когда мы применяем автоматизацию в сети

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

Если нам нужно применить одну и ту же команду к нескольким устройствам, то проще всего будет ansible.

Если нам нужно применить одну и ту же команду, но с разными параметрами, то в этом случае Python и netmiko пригодятся.

Используя ansible, мы можем опрашивать несколько коммутаторов несколькими разными командами и записывать вывод команд в текстовые файлы, но с Python и netmiko мы можем объединить вывод нескольких разных команд, записав только нужную нам информацию в один выходной CSV-файл.

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

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

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

Вот команды:

  • show interface status

  • show mac address-table

  • show cdp neighbor

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

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

66d83943930cce404923759ef54ee9a1.png

Выходной файл выглядит так

4f5a3c8ee4eeab95b232dce05be9ed47.png

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()

© Habrahabr.ru