How and when we use automation in the network

IT infrastructures are becoming increasingly complex, and we now typically work with tens or hundreds of switches, routers and firewalls.

If we need to apply the same command to multiple devices, then ansible is the easiest way.

If we need to use the same command, but with different parameters, then Python and netmiko will come in handy.

Using ansible we can poll multiple switches with several different commands and write the output of the commands to text files, but with Python and netmiko we can combine the output of several different commands, writing only the information we need into a single CSV output file.

Why CSV? A CSV file is convenient because we can open it in Excel and easily hide columns we don't need, group them, or organize them by columns we need.

If we create one such file daily from all switches, we can easily see the difference in the state of the connected devices simply by opening the two files in the Notepad++ editor in comparison mode.

I've combined three commands that we typically use to record and monitor the status of all switches and all connected devices.

Here are the commands:

  • show interface status

  • show mac address-table

  • show cdp neighbor

My Python program accesses all switches in a set, executes all three commands, and concatenates the output of all three commands into a single file.
Now we don't need to connect to each switch separately and execute all three commands one after another.

For demonstration purposes, I created a very simple infrastructure consisting of two switches and a few connected devices.

The output file looks like this

python program

#!/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()

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *