Pull to refresh

Зачем сетевику Python

Reading time 10 min
Views 51K
«Сетевому администратору необходимо уметь программировать» — эта фраза часто вызывает возражения у многих сетевиков.

— Зачем? Руками оно надёжнее.
— Зато можно автоматизировать типовые операции.
— И положить кучу устройств, если что-то пойдёт не так?
— Положить кучу устройств можно и руками.

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

Если посмотреть в сторону производителей сетевого оборудования,
то окажется, что та же cisco уже давно предлагает разнообразные варианты для автоматизации работы с сетевым оборудованием: от TCL на IOS до Python на NX-OS и IOS-XR . Называется всё это network automation или network programmability, и у Cisco есть курсы по этому направлению.

И Cisco здесь не одинока: Juniper c PyEZ, HP, Huawei и тд.

Множество инструментов — Netconf, Restconf, Ansible, Puppet и Python, Python, Python. Анализ конкретных инструментов отложим на потом, перейдём к конкретному примеру.

Второй вопрос, который иногда вызывает бурные дискуссии, как правило приводящий к полному непониманию друг друга: «А нужны сетевику сетевые устройства в DNS?».
Оставим подробный анализ позиций участников на потом, сформулируя задачу, которая привела к Python и SNMP. А началось всё с traceroute.

Несмотря на наличие разнообразных систем мониторинга, которые бдят и видят многое, MPLS-TE, который разворачивает трафик причудливым образом, верный ICMP и утилиты traceroute и ping во многих случаях способны дать нужную информацию быстро и сейчас. Но вывод traceroute только ввиде IP адресов в большой сети потребует дополнительных усилий для понимания того, откуда именно пришли пакеты. Например, мы видим, что прямой и обратный трафик от пользователя идёт через разные маршрутизаторы, но по каким именно? Решение очевидно, занести адреса маршрутизаторов в DNS. А для корпоративных сетей, где редко используют unnumbered, ставя на соединители отдельные адреса, в случае занесения адресов интерфейсов в DNS, можно будет быстро понять, через какой интерфейс пакет ICMP вышел с маршрутизатора.

Однако вести вручную базу DNS на большой сети требует очень больших трудозатрат не самого сложного труда. А ведь доменное имя интерфейса будет состоят из названия интерфейса, description интерфейса, hostname маршрутизатора и названия домена. Всё это маршрутизатор несёт в своей конфигурации. Главное это собрать и правильно склеить и привязать к правильному адресу.

Значит эту задачу надо автоматизировать.

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

Вторая мысль, использовать то, что даёт нужные ответы на универсальные запросы к оборудованию разных вендоров. Ответ был очевиден — SNMP. Он, при всех своих особенностях, реализован в ПО любого вендора.

Итак, начнём


Ставим Python.
sudo apt-get install python3

Нам понадобятся модули для работы с SNMP, IP адресами, со временем. Но для их установки необходимо поставить pip. Правда, сейчас он идёт в комплекте с python.
sudo apt install python3-pip

А теперь ставим модули.
pip3 install pysnmp

pip3 install datetime

pip3 install ipaddress

Попробуем получить с маршрутизатора его hostname. SNMP использует для запросов к хосту OID. На OID хост вернёт информацию, соответствующую этому OID. Хотим получить hostname — нужно запрашивать 1.3.6.1.2.1.1.5.0.

И так первый скрипт, который запрашивает только hostname.

# import section
from pysnmp.hlapi import *
from ipaddress import *
from datetime import datetime

# var section

#snmp
community_string = 'derfnutfo'  # From file
ip_address_host = '192.168.88.1'  # From file
port_snmp = 161
OID_sysName = '1.3.6.1.2.1.1.5.0'  # From SNMPv2-MIB hostname/sysname

# function section

def snmp_getcmd(community, ip, port, OID):
    return (getCmd(SnmpEngine(),
                   CommunityData(community),
                   UdpTransportTarget((ip, port)),
                   ContextData(),
                   ObjectType(ObjectIdentity(OID))))

def snmp_get_next(community, ip, port, OID):
    errorIndication, errorStatus, errorIndex, varBinds = next(snmp_getcmd(community, ip, port, OID))
    for name, val in varBinds:

        return (val.prettyPrint())

#code section

sysname = (snmp_get_next(community_string, ip_address_host, port_snmp, OID_sysName))
print('hostname= ' + sysname)

Запускаем и получаем:
hostname= MikroTik

Разберём скрипт поподробнее:

Сначала мы импортируем необходимые модули:

1. pysnmp — обеспечивает работу скрипта с хостом по SNMP

2. ipaddress — обеспечивает работу с адресами. Проверка адресов на корректность, проверка на вхождения адреса в адрес сети и тд.

3. datetime- получение текущего времени. В данной задаче нужен для организации логов.

Потом заводим четыре переменных:

1. community
2. адрес хоста
3. порт SNMP
4. значение OID

Две функции:

1. snmp_getcmd
2. snmp_get_next

Первая функция посылает запрос GET указанному хосту, по указанному порту, с указанным comminity и OID.
Вторая функция это генератор snmp_getcmd. Наверное разбивать на две функции было не совсем правильно, но уж так получилось:)

В этом скрипте не хватает некоторых вещей:

1. В скрипт необходимо загрузить ip адреса хостов. Например, из текстового файла. При загрузке необходимо проверить загружаемый адрес на корректность, иначе pysnmp может очень сильно удивиться и скрипт остановится с traceback. Непринципиально, откуда вы будете брать адреса из файла, из базы даных, но вы должны быть уверены, что адреса, которые вы получили — корректные. И так, источник адресов текстовый файл, одна строка — один адрес в десятичной форме.

2. Сетевое оборудование может быть выключено на момент опроса, может быть неправильно настроено, в итоге pysnmp выдаст в этом случае совершенно не то, что мы ждём и при дальнейшей обработке полученной информации получим остановку скрипта с traceback. Нужен обработчик ошибок для нашего взаимодействия по SNMP.

3. Нужен лог файл, в который будут записываться обработанные ошибки.

Загружаем адреса и создаём лог файл


Вводим переменную для имени файла.
Пишем функцию check_ip на проверку корректности адреса.
Пишем функцию get_from_file загрузки адресов, которая проверяет каждый адрес на корректность и если это не так, записывает об этом сообщение в лог.
Реализуем загрузку данных в список.

filename_of_ip = 'ip.txt' # имя файла с Ip адресами
#log
filename_log = 'zone_gen.log' #   

def check_ip(ip): # проверка ip адреса корректность
    try:
        ip_address(ip)
    except ValueError:
        return False
    else:
        return True

def get_from_file(file, filelog): # выбирает ip адреса из файла. одна строка - один адрес в десятичной форме
    fd = open(file,'r')
    list_ip = []
    for line in fd:
       line=line.rstrip('\n')
       if check_ip(line):
           list_ip.append(line)
       else:
            filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ': Error Мусор в источнике ip адресов ' + line)
            print('Error Мусор в источнике ip адресов ' + line)
    fd.close()
    return list_ip

#code section

#открываем лог файл
filed = open(filename_log,'w')

# записываем текущее время
filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + '\n')

ip_from_file = get_from_file(filename_of_ip, filed)

for ip_address_host in ip_from_file:
    sysname = (snmp_get_next(community_string, ip_address_host, port_snmp, OID_sysName))
    print('hostname= ' + sysname)

filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + '\n')
filed.close()

Создадим файл ip.txt
192.168.88.1
172.1.1.1
12.43.dsds.f4
192.168.88.1

Второй адрес в этом списке не отвечает на snmp. Запустим скрипт и убедимся в необходимости обработчика ошибок для SNMP.
Error ip 12.43.dsds.f4
hostname= MikroTik
Traceback (most recent call last):
File "/snmp/snmp_read3.py", line 77, in print('hostname= ' + sysname)
TypeError: Can't convert 'NoneType' object to str implicitly

Process finished with exit code 1

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

Создаём обработчик ошибок для pysnmp


В функции snmp_get_next уже есть вывод ошибок errorIndication, errorStatus, errorIndex, varBinds. В varBinds выгружаются полученные данные, в переменные, начинающиеся с error, выгружается информация по ошибкам. Это только нужно правильно обработать. Так как в дальнейшем в скрипте будет ещё несколько функций по работе с snmp, имеет смысл обработку ошибок вынести в отдельную функцию.

def errors(errorIndication, errorStatus, errorIndex, ip, file):
    #обработка ошибок В случае ошибок возвращаем False и пишем в файл
    if errorIndication:
        print(errorIndication, 'ip address ', ip)
        file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + str(errorIndication) + ' = ip address = ' + ip + '\n')
        return False
    elif errorStatus:
        print(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + '%s at %s' % (errorStatus.prettyPrint(), errorIndex and varBinds[int(errorIndex) - 1][0] or '?'))
        file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + '%s at %s' % (errorStatus.prettyPrint(), errorIndex and varBinds[int(errorIndex) - 1][0] or '?' + '\n'))
        return False
    else:
        return True

И теперь добавляем в функцию snmp_get_next обработку ошибок и запись в лог файл. Функция теперь должна возвращать не только данные, но и сообщение о том, были ли ошибки.


def snmp_get_next(community, ip, port, OID, file):
    errorIndication, errorStatus, errorIndex, varBinds = next(snmp_getcmd(community, ip, port, OID))
    if errors(errorIndication, errorStatus, errorIndex, ip, file):
        for name, val in varBinds:
            return (val.prettyPrint(), True)
    else:
        file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : Error snmp_get_next ip = ' + ip + ' OID = ' + OID + '\n')
        return ('Error', False)

Теперь необходимо немного переписать code section, с учётом того, что теперь есть сообщения об успешности запроса. Кроме этого, добавим несколько проверок:

1. Sysname меньше, чем три символа. Запишем в лог файл, чтобы потом присмотреться по пристальнее.

2. Обнаружим, что некоторые Huawei и Catos отдают на запрос только hostname. Так как отдельно выискивать для них OID совершенно не хочется (не факт, что он вообще есть, может это ошибка ПО), добавим таким хостам domain вручную.

3. Обнаружим, что хосты с неправильным comminity ведут себя по разному, большинство инициирует срабатывание обработчика ошибок, а некоторые почему-то отвечают, что скрипт воспринимает как нормальную ситуацию.

4. Добавим на время отладки разный уровень логирования, чтобы потом не выковыривать по всему скрипту лишние сообщения.

for ip_address_host in ip_from_file:
    #  получаем sysname hostname+domainname, флаг ошибки   
    sysname, flag_snmp_get = (snmp_get_next(community_string, ip_address_host, port_snmp, OID_sysName, filed))

    if flag_snmp_get:
        # Всё хорошо, хост ответил по snmp
        if sysname == 'No Such Object currently exists at this OID':
            #  а community неверный.надо пропускать хост, иначе словим traceback. Причём ты никак не поймаешь, что проблема в community, поэтому всегда надо запрашивать hostname, который отдают все устройства    
            print('ERROR community', sysname, ' ', ip_address_host)
            filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'ERROR community sysname = ' + sysname + '  ip = ' + ip_address_host + '\n')
        else:
            if log_level == 'debug':
                filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + '  sysname ' + sysname + ' type ' + str(type(sysname)) + ' len ' + str(len(sysname)) + ' ip ' + ip_address_host + '\n')
            if len(sysname) < 3
                if log_level == 'debug' or log_level == 'normal':
                    filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'Error sysname  3  = ' + sysname + '  ip = ' + ip_address_host + '\n')
            if sysname.find(domain) == -1:
                # что-то отдало hostname без домена, например Huawei или Catos
                sysname = sysname + '.' + domain
                  if log_level == 'debug' or log_level == 'normal':
                    filed.write("check domain     : " + sysname + " " + ip_address_host + " " + "\n")

        print('hostname= ' + sysname)

Проверим этот скрипт на том же файле ip.txt
Error Мусор в источнике ip адресов 12.43.dsds.f4
hostname= MikroTik.mydomain.ru
No SNMP response received before timeout ip address 172.1.1.1
hostname= MikroTik.mydomain.ru

Всё отработало штатно, мы поймали все ошибки, скрипт пропустил хосты с ошибками. Теперь этим скриптом можно собрать hostname cо всех устройств, отвечающих на snmp.

Полный текст скрипта прячу под спойлер.

Скрипт

# import section
from pysnmp.hlapi import *
from ipaddress import *
from datetime import datetime

# var section

#snmp
community_string = 'derfnutfo'  
ip_address_host = '192.168.88.1'  
port_snmp = 161
OID_sysName = '1.3.6.1.2.1.1.5.0'  # From SNMPv2-MIB hostname/sysname
filename_of_ip = 'ip.txt' #    Ip 
#log
filename_log = 'zone_gen.log'  # для лог файла
log_level = 'debug'

domain='mydomain.ru'

# function section

def snmp_getcmd(community, ip, port, OID):
# type class 'generator' errorIndication, errorStatus, errorIndex, result[3] - список
# метод get получаем результат обращения к устойстройству по SNMP с указаным OID
    return (getCmd(SnmpEngine(),
                   CommunityData(community),
                   UdpTransportTarget((ip, port)),
                   ContextData(),
                   ObjectType(ObjectIdentity(OID))))

def snmp_get_next(community, ip, port, OID, file):
# метод обрабатывает class generator от def snmp_get
# обрабатываем errors, выдаём тип class 'pysnmp.smi.rfc1902.ObjectType' с OID (в name) и значением  (в val)
# получаем одно скалярное значение
    errorIndication, errorStatus, errorIndex, varBinds = next(snmp_getcmd(community, ip, port, OID))

    if errors(errorIndication, errorStatus, errorIndex, ip, file):
        for name, val in varBinds:
            return (val.prettyPrint(), True)
    else:
        file.write(datetime.strftime(datetime.now(),
                                     "%Y.%m.%d %H:%M:%S") + ' : Error snmp_get_next ip = ' + ip + ' OID = ' + OID + '\n')
        return ('Error', False)

def get_from_file(file, filelog):  
#Загрузка ip адресов из файла file, запись ошибок в filelog     
     fd = open(file, 'r')
     list_ip = []
     for line in fd:
         line=line.rstrip('\n')
         if check_ip(line):
            list_ip.append(line)
         else:
            filed.write(datetime.strftime(datetime.now(),
                                              "%Y.%m.%d %H:%M:%S") + ': Error    ip  ' + line)
            print('Error    ip  ' + line)
     fd.close()

     return list_ip

def check_ip(ip): 
#  Проверка ip адреса на корректность. False проверка не пройдена.
    try:
       ip_address(ip)
    except ValueError:
        return False
    else:
        return True

def errors(errorIndication, errorStatus, errorIndex, ip, file):
    #  обработка ошибок в случае ошибок возвращаем False и пишем в файл file
    if errorIndication:
       print(errorIndication, 'ip address ', ip)
       file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + str(
                errorIndication) + ' = ip address = ' + ip + '\n')
       return False
    elif errorStatus:
         print(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + '%s at %s' % (
         errorStatus.prettyPrint(),
         errorIndex and varBinds[int(errorIndex) - 1][0] or '?' ))
         file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + '%s at %s' % (
         errorStatus.prettyPrint(),
         errorIndex and varBinds[int(errorIndex) - 1][0] or '?' + '\n'))
         return False
    else:
         return True

#code section

#открываем лог файл
filed = open(filename_log,'w')

# записываем текущее время
filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + '\n')

ip_from_file = get_from_file(filename_of_ip, filed)

for ip_address_host in ip_from_file:
    # получаем sysname hostname+domainname, флаг ошибки
    sysname, flag_snmp_get = (snmp_get_next(community_string, ip_address_host, port_snmp, OID_sysName, filed))

    if flag_snmp_get:
        # Всё хорошо, хост ответил по snmp
        if sysname == 'No Such Object currently exists at this OID':
             # а community неверный.надо пропускать хост, иначе словим traceback. Причём ты никак не поймаешь, что проблема в community, поэтому всегда надо запрашивать hostname, который отдают все устройства
            print('ERROR community', sysname, ' ', ip_address_host)
            filed.write(datetime.strftime(datetime.now(),
                                          "%Y.%m.%d %H:%M:%S") + ' : ' + 'ERROR community sysname = ' + sysname + '  ip = ' + ip_address_host + '\n')
        else:

            if log_level == 'debug':
                filed.write(datetime.strftime(datetime.now(),
                                              "%Y.%m.%d %H:%M:%S") + ' : ' + '  sysname ' + sysname + ' type ' + str(
                    type(sysname)) + ' len ' + str(len(sysname)) + ' ip ' + ip_address_host + '\n')
            if len(sysname) < 3:
                sysname = 'None_sysname'
                if log_level == 'debug' or log_level == 'normal':
                    filed.write(datetime.strftime(datetime.now(),
                                                  "%Y.%m.%d %H:%M:%S") + ' : ' + 'Error sysname  3  = ' + sysname + '  ip = ' + ip_address_host + '\n')
            if sysname.find(domain) == -1:
                # что-то отдало hostname без домена, например Huawei или Catos
                sysname = sysname + '.' + domain
                if log_level == 'debug' or log_level == 'normal':
                    filed.write("check domain     : " + sysname + " " + ip_address_host + " " + "\n")

        print('hostname= ' + sysname)

filed.close()

Теперь осталось собрать имена интерфейсов, description интерфейсов, адреса интерфейсов и правильно разложить в конфигуционные файлы bind. Но об этом во второй части.

P.S.: Отмечу, что по-хорошему сообщения в лог файл следует формировать по-другому принципу.
Например: время спецсимвол код_ошибки спецсимвол описание_ошибки спецсимвол дополнительная_информация. Это поможет потом настроить автоматическую обработку лога.
UPD: исправление ошибок.
Tags:
Hubs:
+22
Comments 45
Comments Comments 45

Articles