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:
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.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.