Netmiko и автоматизация управления коммутаторами Cisco

9036419c2c96a7cdfb9f04de932b4abb

Привет, Хабр.

Здесь уже недавно были статьи про netmiko и автоматизацию управления коммутаторами Cisco.

Я хочу продолжить эту тему дальше в контексте взаимодействия сетевого отдела и отдела поддержки пользователей. (DSS digital site support как их называют)

Какие вопросы обычно возникают в процессе взаимодействия?

  • DSS необходимо подключить новых пользователей, или новые принтеры, или новые видеокамеры к коммутаторам и подобные вопросы.

  • DSS посылает запрос в сетевой отдел для настройки нескольких портов на коммутаторах Cisco для подключения устройств.

  • Сетевой отдел должен настроить несколько портов на коммутаторах Cisco в режиме access и соответствующий User vlan или Printer vlan.

Иногда на коммутаторах есть свободные, ранее настроенные порты, но у DSS нет информации для каких vlan эти порты настроены.
Поэтому DSS посылает запрос в сетевой отдел.

Моё решение предлагает:

  1. Автоматическую генерацию отчёта о всех портах коммутаторов Cisco в виде excel файла и рассылку этого отчёта в отдел поддержки.

    Имея такую информацию специалисты поддержки могут сразу подключить новых пользователей если они видят свободные порты в коммутаторах и знают что порты в правильном vlan.

    Решение осуществлено на python и может запускаться или каждую ночь по cron, или любой момент из jenkins.
    В jenkins это просто кнопка «создать отчет».

  2. Специалист 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.

Я буду рад услышать ваши любые комментарии.

© Habrahabr.ru