cancel
Showing results for 
Search instead for 
Did you mean: 
cancel
524
Views
1
Helpful
1
Comments
Tagir Temirgaliyev
Spotlight
Spotlight

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

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

If we need to apply the same command but with different parameters, then in this case python and netmiko useful.

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 from several different commands, only the information we need into one output CSV file.

Why CSV ? The CSV file is convenient, because we can open it in the Excel, and easily hide the columns we don’t need, group or order by the columns we need.

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

I combined three commands that we usually use to record and track the status of all switches, and all connected devices.

These are the commands:

- show interface status
- show mac address-table
- show cdp neighbor

My python program accesses all switches from a predefined set, executes all three commands, and combines the output of all three commands into one file.
Now, we don't need to connect to each switch separately and execute all three commands one after another. We also don't need to search for information in different text files, now all the information is in one file.

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

1.png

 

The output file looks like this, information from "show cdp neighbor" is compressed in one short word and consist of name, platform, remote port, and can be used as a new interface description.

2.png

Python program is simple, to understand it you need to know variable types as 'string', 'integer', 'list', 'dictionary' and how to open close text file. It also creates 3 temporary text files, I decided to leave them for a better understanding of the principle of operation.

 

#!/usr/bin/python3
#   Copyright (c) 2023-2024 T.Temirgaliyev  The MIT License
#   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()
    
    

 

Comments
SamuelGLN
Spotlight
Spotlight

Amazing @Tagir Temirgaliyev! Thanks for sharing!

Getting Started

Find answers to your questions by entering keywords or phrases in the Search bar above. New here? Use these resources to familiarize yourself with the community: