Netmiko and Cisco Switch Management Automation

I want to continue this topic further in the context of the interaction between the network department and the user support department. (DSS digital site support as they are called)

What questions usually arise during the interaction process?

  • DSS needs to connect new users, or new printers, or new video cameras to switches and similar issues.

  • DSS sends a request to the network department to configure multiple ports on Cisco switches to connect devices.

  • The network department needs to configure multiple ports on the Cisco switches in access mode and the appropriate User vlan or Printer vlan.

Sometimes switches have free, previously configured ports, but DSS has no information for which VLANs these ports are configured.
Therefore, DSS sends a request to the network department.

My solution suggests:

  1. Automatic generation of a report on all Cisco switch ports in the form of an excel file and sending this report to the support department.

    With this information, support specialists can immediately connect new users if they see free ports in the switches and know that the ports are in the correct VLAN.

    The solution is implemented in python and can be launched either every night via cron, or at any time from jenkins.
    In jenkins it's just a “generate report” button.

  2. A DSS specialist can simply edit an Excel file with new vlan values ​​on the required ports and send this file to be executed in jenkins and almost immediately configure the required vlans on the required ports. The network department will not be involved. This task will be limited to changing vlans on access ports only. Trunk ports cannot be changed in any way using this script.

If you are not familiar with jenkins, it is a free graphical shell instead of the command line, plus logs of who ran it, when, and what the result was.

What is needed? Linux virtual machine, ansible, python, netmiko, ansible inventory file in yaml format.

And the task will be launched on any group of switches from the inventory file.

Here is an example of an ansible inventory file:

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

Here is a python program that contacts all switches from a given group and reads the information after executing the commands “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()

And here is the final csv file:

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,

The output file can be supplemented with columns mac address, ip address, vendor, lldp neighbor, uptime, downtime, etc. If you have Cisco Call Manager and IP phones, you can supplement it with a column with the phone number, which will make it much easier to search for phones.

This program is at the testing stage, I have not tested it on stack switches, I do not have them at hand, I have tested it only on Cisco virtual switches. It can also be adapted for Juniper and Aruba switches.

I would be glad to hear any comments you may have.

Similar Posts

Leave a Reply

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