Netmiko и автоматизация управления коммутаторами Cisco
Привет, Хабр.
Здесь уже недавно были статьи про netmiko и автоматизацию управления коммутаторами Cisco.
Я хочу продолжить эту тему дальше в контексте взаимодействия сетевого отдела и отдела поддержки пользователей. (DSS digital site support как их называют)
Какие вопросы обычно возникают в процессе взаимодействия?
DSS необходимо подключить новых пользователей, или новые принтеры, или новые видеокамеры к коммутаторам и подобные вопросы.
DSS посылает запрос в сетевой отдел для настройки нескольких портов на коммутаторах Cisco для подключения устройств.
Сетевой отдел должен настроить несколько портов на коммутаторах Cisco в режиме access и соответствующий User vlan или Printer vlan.
Иногда на коммутаторах есть свободные, ранее настроенные порты, но у DSS нет информации для каких vlan эти порты настроены.
Поэтому DSS посылает запрос в сетевой отдел.
Моё решение предлагает:
Автоматическую генерацию отчёта о всех портах коммутаторов Cisco в виде excel файла и рассылку этого отчёта в отдел поддержки.
Имея такую информацию специалисты поддержки могут сразу подключить новых пользователей если они видят свободные порты в коммутаторах и знают что порты в правильном vlan.
Решение осуществлено на python и может запускаться или каждую ночь по cron, или любой момент из jenkins.
В jenkins это просто кнопка «создать отчет».Специалист DSS может просто отредактировать Excel файл с новыми значениями vlan на требуемых портах и отослать этот файл на исполнение в jenkins и практически сразу сконфигурировать нужные vlan на нужных портах. Сетевой отдел не будет задействован. Эта задача будет ограничена только изменением vlan только на access портах. Порты trunk никак нельзя будет изменить с помощью этого скрипта.
Если вы не знакомы с jenkins, то это бесплатная графическая оболочка вместо командной строки и плюс логи, кто запускал, когда и каков результат.
Что необходимо? Виртуальная машина linux, ansible, python, netmiko, inventory file ansible в формате yaml.
И запускаться задача будет на любой группе свичей из inventory file.
Вот пример inventory file ansible:
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
Вот python программа, которая обращается кто всем коммутаторам из заданной группы и считывает информацию после выполнение команд «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()
И вот итоговый csv файл:
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,
Выходной файл можно дополнить столбцами mac address, ip address, vendor, lldp neighbor, uptime, downtime и др. Если у вас есть Cisco Call Manager и IP телефоны то можно дополнить столбцом с номером телефона, что значительно облегчит поиск телефонов.
Эта программа на тестовой стадии, я не проверял на стековых коммутаторах, у меня их нет под рукой, я проверял только на виртуальных коммутаторах Cisco. Также можно адаптировать для коммутаторов Juniper и Aruba.
Я буду рад услышать ваши любые комментарии.