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

Здесь уже недавно были статьи про 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.

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

Комментарии (7)


  1. NAI
    23.07.2024 06:31

    По какой причине вы не стали пользоваться ансибловым get-facts? Он же вроде умеет брать информацию с циски. Ну или по крайней мере, выглядит так как будто парсер можно было допилить, после чего задача решалась бы полностью ансиблом


    1. CCNPengineer Автор
      23.07.2024 06:31

      хороший вопрос, спасибо

      если выполнить ансибл 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 файла у меня получилась на пайтон легко но не на ансибл.


  1. net_racoon
    23.07.2024 06:31

    А почему нельзя использовать dot1x? У вас нет IP/влан плана где написано какой влан для чего? Ну и свободные порты должны быть выключены ИМХО.


    1. CCNPengineer Автор
      23.07.2024 06:31

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

      по dot1x можно отдельно долго обсуждать


    1. CCNPengineer Автор
      23.07.2024 06:31

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


      1. net_racoon
        23.07.2024 06:31

        Все равно не понимаю зачем эти костыли, если есть dot1x?


        1. CCNPengineer Автор
          23.07.2024 06:31

          я не совсем понимаю вас. о чем dot1x? аутентификация пользователей в АД ? и на портах коммутатора? есть много разных вариантов внедрения. в том числе можно выдавать какой то vlan какому то пользователю. но не принтеру, не PLC, не видеокамере.

          внедрение dot1x на порядки сложнее чем одна пайтон программа.

          моя программа совсем для другой цели