Привет, Хабр.
Здесь уже недавно были статьи про 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.
Я буду рад услышать ваши любые комментарии.
Комментарии (7)
net_racoon
23.07.2024 06:31А почему нельзя использовать dot1x? У вас нет IP/влан плана где написано какой влан для чего? Ну и свободные порты должны быть выключены ИМХО.
CCNPengineer Автор
23.07.2024 06:31в разных организациях разные требования безопасности. иногда конечно нужно выключать свободные порты. если в помещение могут зайти посторонние лица и воткнуть посторонние устройства в коммутатор то конечно нужно выключить все свободные порты. например отделение банка куда заходят клиенты с улицы.
по dot1x можно отдельно долго обсуждать
CCNPengineer Автор
23.07.2024 06:31для тех организаций которым нужно выключать неиспользуемые порты как раз моя программа может помочь. можно отслеживать и выключать такие порты автоматически. и также DSS своими силами могут включать порты при необходимости
net_racoon
23.07.2024 06:31Все равно не понимаю зачем эти костыли, если есть dot1x?
CCNPengineer Автор
23.07.2024 06:31я не совсем понимаю вас. о чем dot1x? аутентификация пользователей в АД ? и на портах коммутатора? есть много разных вариантов внедрения. в том числе можно выдавать какой то vlan какому то пользователю. но не принтеру, не PLC, не видеокамере.
внедрение dot1x на порядки сложнее чем одна пайтон программа.
моя программа совсем для другой цели
NAI
По какой причине вы не стали пользоваться ансибловым get-facts? Он же вроде умеет брать информацию с циски. Ну или по крайней мере, выглядит так как будто парсер можно было допилить, после чего задача решалась бы полностью ансиблом
CCNPengineer Автор
хороший вопрос, спасибо
если выполнить ансибл get-facts то в переменной ansible_facts.net_interfaces есть информация об интерфейсах
"GigabitEthernet1/1": { "bandwidth": 1000000, "description": "011", "duplex": "Auto", "ipv4": [], "lineprotocol": "up", "macaddress": "5000.0003.0005", "mediatype": "RJ45", "mtu": 1500, "operstatus": "up", "type": "iGbE" },
но нет access vlan
и нет подключенного mac address, ip address и номера телефона
и вторая программа которая меняет vlan на интерфейсе согласно CSV файла у меня получилась на пайтон легко но не на ансибл.