В первой части Зачем сетевику Python скрипт научился собирать hostname оборудования по SNMP. К сожалению, по некоторым причинам вторая часть, мягко говоря, подзадержалась, но в ней я расскажу, как собрать и обработать остальные данные, необходимые для формирования файлов зон.

Сформулируем задачу, которую надо решить: необходимо сформировать доменные имена для маршрутизаторов и их интерфейсов. Скрипт должен из подобной конфигурации (в данном случае cisco):
hostname R1.greenhouse
domain service.prov.ru

interface FastEthernet0/1.24
description Line to Red House
ip address 10.24.1.1 255.255.255.254
сформировать две записи, для прямой зоны и для обратной зоны:
Fa0/1.24.Line_to_Red_House.R1.greenhouse.service.prov.ru. IN A 10.24.1.1

1.1.24.10.in-addr.arpa. IN PTR Fa0/1.24.Line_to_Red_House.R1.greenhouse.service.prov.ru.

Есть плохая новость: это неправильные доменные имена. По правилам вы можете использовать только цифры, буквы и “-”. Причём дефис не может стоят первым и последним. Для соответствия правилам запись должна выглядеть примерно так:
Fa0-1-24.Line-to-Red-House.R1.greenhouse.service.prov.ru. IN A 10.24.1.1
С читабельностью есть явные проблемы. А если имя интерфейса будет начинаться не цифры, отличной от нуля, да ещё с двоеточием? После приведения такого к стандарту, понять, что и где в этих цифрах и буквах становится довольно сложно. А хотелось бы, увидев вывод traceroute краем глаза, сразу понимать, что, где и как, а не заниматься интерпретацией вывода методом гадания. Поэтому bind был настроен с опцией, которая позволяет работать с нестандартными именами. А скрипт, соответственно, делает максимально читабельные имена. Стыковка DNS сетевиков и глобального DNS это немного отдельный разговор, но проще всего выделить для сетевого оборудования отдельный домен, например, network.megaproduct.org.

Для формирования файлов зон нам необходимо собрать значения hostname маршрутизаторов, имена интерфейсов, значение description на этих интерфейсов и ip-адреса этих интерфейсов. Единственным исключением будет запись, содержащая ip-адрес, принадлежащий интерфейсу Loopback0. Эта запись не будет содержать имя интерфейса и значение его description, так как это доменное имя маршрутизатора.

Как найти нужный OID?


Давайте определимся с OIDами для получения имён интерфейсов, ip адресов и description с этих интерфейсов. Наша цель универсальность, поэтому будем использовать OID стандартных MIB файлов, которые реализованы у всех вендоров. Как правило, самый простой способ найти нужный OID, это не чтение документации или гугление, а использование snmpwalk или любого MIB браузера, который вам больше понравится. Смотрим, что может выдать конкретное оборудование, выбираем нужное. Запустив snmpwalk, вы получите все OID, поддерживаемые устройством со их значениями на момент опроса.

Для того, чтобы начать работу с snmpwalk необходимо поставить пакет snmp и snmp-mibs-downloader:
sudo apt-get install snmp

sudo apt-get install snmp-mibs-downloader
Если необходимо, чтобы утилиты работали с именованными OID в конфигурационном файле /etc/snmp/snmp.conf добавляем используемые mib или ключевое слово ALL.
Конечно, очевидно, что использовать ALL не рекомендуется, количество файлов огромно, поиск по всем файлам подряд во многих случаях явно не очень хорошая идея. Однако, в случае написания программы и поиска нужного MIB файла это допустимо. Не забудьте только потом оставить только нужное и необходимое.
$ cat /etc/snmp/snmp.conf
# As the snmp packages come without MIB files due to license reasons, loading
# of MIBs is disabled by default. If you added the MIBs you can reenable
# loading them by commenting out the following line.
mibs :SNMPv2-MIB:RFC1213-MIB:IF-MIB

#mibs :ALL
zw@note:~$
Проверяем, увидел snmp наши MIB:
host$ snmptranslate 1.3.6.1.2.1.31.1.1.1.18
IF-MIB::ifAlias

host$ snmptranslate -On IF-MIB::ifAlias
.1.3.6.1.2.1.31.1.1.1.18
Делаем запрос к реальной коробке:
host$ snmpget -v 2c -c tilitili 10.239.192.2 1.3.6.1.2.1.31.1.1.1.18.1
IF-MIB::ifAlias.1 = STRING: Line_to_OUT
Последняя цифра (1), которую я добавил — это ifindex. Ifndex это номер интерфейса, назначенный snmp агентом операционной системы оборудования.

Нумерация производится при инициализации агента (при загрузке ОС). После каждой перезагрузке оборудования она будет другой, если вы не укажет маршрутизатору запоминать назначенные ifindex (гуглить по ключевым словам ifindex persist).

В работе с интерфейсами всё логично, берём OID, добавляем ifindex, делаем запрос – получаем description интерфейса с этим Ifindex.

Запросим snmpwalk, который запросит с роутера все доступные OIDы и их текущие значения:
host$ snmpwalk -v 2c -c tilitili 10.239.192.2

RFC1213-MIB::ifDescr.1 = STRING: «FastEthernet0/0»
RFC1213-MIB::ifDescr.2 = STRING: «FastEthernet0/1»
RFC1213-MIB::ifDescr.3 = STRING: «Null0»
RFC1213-MIB::ifDescr.36 = STRING: «Loopback0»
RFC1213-MIB::ipAdEntAddr.10.239.192.2 = IpAddress: 10.239.192.2
RFC1213-MIB::ipAdEntAddr.10.239.192.90 = IpAddress: 10.239.192.90
RFC1213-MIB::ipAdEntAddr.11.0.0.1 = IpAddress: 11.0.0.1
IF-MIB::ifName.1 = STRING: Fa0/0
IF-MIB::ifName.2 = STRING: Fa0/1
IF-MIB::ifName.3 = STRING: Nu0
IF-MIB::ifName.36 = STRING: Lo0
IF-MIB::ifAlias.1 = STRING: Line_to_OUT
IF-MIB::ifAlias.2 = STRING: Line_to_poligon
IF-MIB::ifAlias.3 = STRING:
IF-MIB::ifAlias.36 = STRING:
В выводе snmpwalk есть что почитать, он очень многословен, поэтому его лучше фильтровать.

Можно обратить внимание, интерфейсы Fastethernet идут с 1 ifindex, а Loopback c 36. Любопытная деталь: ifDesc – выдаёт полные названия интерфейсов, ifAlias – отдаёт description интерфейсов, а ifName – выдаёт сокращенные названия интерфейсов. Использование полных названий интерфейсов было отброшено сразу, имена из-за содержимого description и так будут длинные, а полные имена интерфейсов сделают их ещё длиннее.

Если нам нужны OID, значения которых мы знаем, их проще искать из вывода snmpwalk или MIB браузер, используя поиск или фильтрацию. Если нам нужен OID, например, текущего значения утилизации CPU, то это проще искать в google или в документации:)

Поищем нужные OID по значениям ip адресов, которые нам известны:
host$ snmpwalk -v 2c -c tilitili 10.239.192.2 | grep 10.239.192.2

RFC1213-MIB::ipAdEntAddr.10.239.192.2 = IpAddress: 10.239.192.2
RFC1213-MIB::ipAdEntIfIndex.10.239.192.2 = INTEGER: 36
RFC1213-MIB::ipAdEntNetMask.10.239.192.2 = IpAddress: 255.255.255.255
RFC1213-MIB::ipAdEntBcastAddr.10.239.192.2 = INTEGER: 1
RFC1213-MIB::ipAdEntReasmMaxSize.10.239.192.2 = INTEGER: 18024
RFC1213-MIB::ipRouteDest.10.239.192.2 = IpAddress: 10.239.192.2
RFC1213-MIB::ipRouteIfIndex.10.239.192.2 = INTEGER: 36
и так далее
И из вывода сразу видно, что нет OID, добавив к которому ifindex, мы бы получали ip адреса интерфейса с этим ifindex. Обратите внимание:

RFC1213-MIB::ipAdEntAddr. + ip адрес – возвращает ip адрес.
RFC1213-MIB::ipAdEntIfIndex. + ip адрес – возвращает ifindex

Таким образом, нам сначала нужно собрать ip адреса и ifndex интерфейсов с этими ip адресами, а потом по ifndex собрать имена и description интерфейсов.

Переходим к сбору данных:SNMPGETNEXT и Python


Ранее использовавшийся для сбора hostname метод snmpget для решения этой задачи совершенно не подойдёт. Этому методу нужен точный OID, а у нас такого нет. В таких ситуациях используется другой метод — snmpgetnext. В утилитах snmp за этот метод отвечает утилита snmpgetnext.

Работает это так: SNMPGETNEXT берёт OID и возвращает следующий OID и его значение.
host$ snmpgetnext -v 2c -c tilitili 10.239.192.2 1.3.6.1.2.1.4.20.1.1
RFC1213-MIB::ipAdEntAddr.10.239.192.2 = IpAddress: 10.239.192.2

host$ snmpgetnext -v 2c -c tilitili 10.239.192.2 1.3.6.1.2.1.4.20.1.1.10.239.192.2
RFC1213-MIB::ipAdEntAddr.10.239.192.90 = IpAddress: 10.239.192.90
И так далее. Обратите внимание, мы не знаем адреса маршрутизатора при первом запросе, поэтому первое обращение идёт просто на значение OID 1.3.6.1.2.1.4.20.1.1

Попробуем автоматизировать эти запросы. Напишем функцию и вызовем её несколько раз:

def snmp_getnextcmd(community, ip, port, OID):
    # type class 'generator' errorIndication, errorStatus, errorIndex, result[3]
    # метод next для получения значений по порядку, одного за другим с помощью next()
    return (nextCmd(SnmpEngine(),
                    CommunityData(community),
                    UdpTransportTarget((ip, port)),
                    ContextData(),
                    ObjectType(ObjectIdentity(OID))))

g = (snmp_getnextcmd(community_string, ip_address_host, port_snmp, OID_ipAdEntAddr))
print(g)
errorIndication, errorStatus, errorIndex, varBinds = next(g)
for name,val in varBinds:
	print(name.prettyPrint(),' ====== ',val.prettyPrint())

errorIndication, errorStatus, errorIndex, varBinds = next(g)
for name,val in varBinds:
        print(name.prettyPrint(),' ====== ',val.prettyPrint())

errorIndication, errorStatus, errorIndex, varBinds = next(g)
for name,val in varBinds:
        print(name.prettyPrint(),' ====== ',val.prettyPrint())

errorIndication, errorStatus, errorIndex, varBinds = next(g)
for name,val in varBinds:
        print(name.prettyPrint(),' ====== ',val.prettyPrint())

errorIndication, errorStatus, errorIndex, varBinds = next(g)
for name,val in varBinds:
        print(name.prettyPrint(),' ====== ',val.prettyPrint())


Запускаем, смотрим результат:
<generator object nextCmd at 0x7f960364f8e0>
SNMPv2-SMI::mib-2.4.20.1.1.10.239.192.2 ====== 10.239.192.2
SNMPv2-SMI::mib-2.4.20.1.1.10.239.192.90 ====== 10.239.192.90
SNMPv2-SMI::mib-2.4.20.1.1.11.0.0.1 ====== 11.0.0.1
SNMPv2-SMI::mib-2.4.20.1.2.10.239.192.2 ====== 36
SNMPv2-SMI::mib-2.4.20.1.2.10.239.192.90 ====== 1
Переменная g у нас генератор. О чём нам python сразу и говорит, когда выполняет print(g).
А далее, с помощью встроенной функции next, скрипт перебирает OID за OID. Значения ошибок складываются в errorIndication, errorStatus, errorIndex, а результат складывается в список varBinds. Для получения значений из varBinds используем цикл for:

for name,val in varBinds:
        print(name.prettyPrint(),' ====== ',val.prettyPrint())

Про генераторы в python написано много интересных статей, в том числе и на хабре, поэтому не буду углубляться в эту тему.

А теперь у нас получается очень простая последовательность действий:

  1. Получаем адреса по OID 1.3.6.1.2.1.4.20.1.1, перебирая с помощью next, записываем адреса в список. Для примера будем отслеживать адрес 10.239.192.90.
  2. Получаем ifindex по OID 1.3.6.1.2.1.4.20.1.2, записываем в список. Для адреса 10.239.192.90 значение ifindex интерфейса равно 1.
  3. указанием значения ifindex. Для адреса 10.239.192.90 итоговая конструкция будет 1.3.6.1.2.1.31.1.1.1.18.1.
  4. Собираем названия интерфейсов, используя OID 1.3.6.1.2.1.31.1.1.1.1+.ifindex.

Осталось собрать в функцию с snmpgetnext первые два пункта, а остальные два пункта соберём с помощью snmpget. Функция будем сама ходить по OID, перебирать в них значения и останавливаться, когда все OID закончились. OID будем загружать списком. Список с OID закончился — заканчиваем работу. Результат будет возвращаться двумерным списком.

def snmp_getnextcmd_next(community, ip, port, OID, file):
    # метод обрабатывает class generator от def snmp_getnext
    # OID - это список OID в виде list_OID = [OID_ipAdEntAddr,OID_ipAdEntIfIndex,OID_ipAdEntNetMask], где переменные строковые значения
    # в виде '1.2.3.4'
    # возвращаем двумерный список со значениями, по количеству OID
    list_result = [] # для формирования списков первого уровня
    list_result2 = [] # итоговый список
    g = (snmp_getnextcmd(community, ip, port, OID[0])) #начинаем с первого OID
    varBinds = 0
    flag = True
    for oid in list_OID:
        if varBinds != 0:
            for name, val in varBinds:
                list_result2.append(list_result)
                list_result = []
                list_result.append(val.prettyPrint())
        i = 0
        while i <= 0:  # по списку
            errorIndication, errorStatus, errorIndex, varBinds = next(g)
            if errors(errorIndication, errorStatus, errorIndex, ip_address_host, file):
                if str(varBinds).find(oid) != -1:
                    i = 0
                    for name, val in varBinds:
                        list_result.append(val.prettyPrint())
                else:
                    i = i + 1
                    flag = False
            else:
                file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'Error snmp_getnextcmd_next ip = ' + ip + ' OID = '+ OID[0] + '\n')
                print('Error snmp_getnextcmd_next ', False)
                i = i + 1
                flag = False
    list_result2.append(list_result)
    return list_result2

Получаем список, содержащий ip адреса, ifindex, маски. Маски остались после экспериментов, тяжёлое наследие прошлого. Чтобы приступить к следующим этапам по сбору имён интерфейсов, description, необходимо отфильтровать полученные данные. Например, оборудование Huawei, Cisco будут отдавать адреса из сети 127.0.0.0/8, формировать для них доменные имена бессмысленно. В текущей версии скрипта идёт фильтрация по допустимому диапазону IPv4, всё не попадающее в диапазон — удаляется. Очевидно, что для ваших сетей правила фильтрации будут другие.

Собираем имена интерфейсов, description с помощью snmpget и приступаем к формированию файлов зон. Для формирования файла обратных зон необходимо изменить порядок следования октетов, для чего можно воспользоваться reverse_pointer из библиотеки ipaddress или написать что-то своё.
print(ipaddress.ip_address('194.4.5.1').reverse_pointer)

1.5.4.194.in-addr.arpa
У меня скрипт именует файлы обратных зон основываясь на адресах, которые он получает из маршрутизатором, он создаёт словарь, в котором ключ состоит из двух первых октетов ip-адреса. Потом из этих ключей формируются названия файлов. Для этого всё равно необходимо разбирать адрес по октетам. Прикладываю скрипт, который получив список адресов маршрутизаторов должен выдать корректные файлы зон. У меня он именно это делает.

Вроде я про все необходимые кубики для начала работы с оборудованием по snmp с помощью python рассказал. Кубики можно комбинировать как угодно, а можно сделать свои.

Пример скрипта
# скрипт берёт адреса или с базы данных, или из файла.
# скрипт делает первый запрос по snmp sysname, в процессе срабатывает обработчик ошибок, если были ошибки, адрес пропускается
# если сбор sysname прошёл успешно, запрашиваются адреса, ifindex, маски (маски не нужны, это осталось со времени отработки запросов) и тд
# если хост вырубится между запросами - позже будет вываливание скрипта с traceback, так как в testresult будет каша
# на запросах desc и name interface - обработчик работает
# в bind должны быть опция check-names master ignore
# заголовок файлов зон не формируется скриптом, он должен быть в отдельном файле. По умолчанию - named.soa. Скрипт только проставляет $INCLUDE

#import section

from pysnmp.hlapi import *
from ipaddress import *
from datetime import datetime
import mysql.connector
from mysql.connector import errorcode # для работы с mysql неоходимо поставить mysql.connector для python3

# по умолчанию данные из mysql, как пепеключить на файл смотри ниже. формат файла - одна строка один ip адрес в десятичной форме по октетам с разделителем точкой

# var section

#snmp
community_string = 'community'  # community string
port_snmp = 161
OID_ipAdEntAddr = '1.3.6.1.2.1.4.20.1.1'  # From SNMPv2-MIB ip адреса
OID_ifNumber = '1.3.6.1.2.1.2.1.0'  # From RFC1213-MIB количество интерфейсов ifindex
OID_sysName = '1.3.6.1.2.1.1.5.0'  # From SNMPv2-MIB hostname/sysname
OID_ipAdEntIfIndex = '1.3.6.1.2.1.4.20.1.2' # From SNMPv2-MIB ifindex interface
OID_ipAdEntNetMask = '1.3.6.1.2.1.4.20.1.3' # From SNMPv2-MIB
OID_ifAlias = '1.3.6.1.2.1.31.1.1.1.18' # Desc интерфейса. для получения к OID надо добавить ifindex
OID_ifName = '1.3.6.1.2.1.31.1.1.1.1'   # название интерфейса к OID надо добавить ifindex
list_OID = [OID_ipAdEntAddr,OID_ipAdEntIfIndex,OID_ipAdEntNetMask]

#log
filename_log = 'zone_gen.log' # для лог файла
log_level = 'debug' # ('normal'), ('debug'), ('min')

# bind zone files
file_direct_zone = 'spd.esrr.rzd.hosts' # имя файла прямой зоны
domain = 'domain.name.org' # domainname
named_soa = '$INCLUDE /var/bind/named.soa'
direct_zone =[] # список для прямой зоны
reverse_zone = {} # словарь для обратных зон
name_server_record = 'dns.server.ru.        IN      A       10.111.33.21\n'

ip_segment_ds = ['10.0.0.0/8'] # для убирания адресов, не попадающих в диапазон

#var section for mysql
username_db = 'username' # логин для mysql
password_db = 'password' # пароль для mysql
db = 'name_database' # имя базы данных
query_from_db = 'select ip from devices where type = 2 or type = 3;' # пример sql запроса;
host = '192.11.33.123' # адрес базы

# ip from file
filename_of_ip = 'ip.txt' # имя файла с Ip адресами
select_source_ip = 'file' # Флаг откуда брать данные из файла ('file') или confstractor ('mysql')

# function section

def snmp_getnextcmd(community, ip, port, OID):
    # type class 'generator' errorIndication, errorStatus, errorIndex, result[3]
    # метод next для получения значений по порядку, однго за другим с помощью next()
    return (nextCmd(SnmpEngine(),
                    CommunityData(community),
                    UdpTransportTarget((ip, port)),
                    ContextData(),
                    ObjectType(ObjectIdentity(OID))))

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 и значением
    # получаем одно скалярное значение

    errorIndication, errorStatus, errorIndex, varBinds = next(snmp_getcmd(community, ip, port, OID))
    # тут должен быть обработчик errors
    x = []
    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 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

def snmp_getnextcmd_next(community, ip, port, OID, file):
    # метод обрабатывает class generator от def snmp_getnext
    # OID - это список OID в виде list_OID = [OID_ipAdEntAddr,OID_ipAdEntIfIndex,OID_ipAdEntNetMask], где переменные строковые значения
    # в виде '1.2.3.4'
    # возвращаем двумерный список со значениями, по количеству OID
    list_result = [] # для формирования списков первого уровня
    list_result2 = [] # итоговый список
    g = (snmp_getnextcmd(community, ip, port, OID[0])) #начинаем с первого OID
    varBinds = 0
    flag = True
    for oid in list_OID:
        if varBinds != 0:
            for name, val in varBinds:
                list_result2.append(list_result)
                list_result = []
                list_result.append(val.prettyPrint())
        i = 0
        while i <= 0:  # по списку
            errorIndication, errorStatus, errorIndex, varBinds = next(g)
            if errors(errorIndication, errorStatus, errorIndex, ip_address_host, file):
                if str(varBinds).find(oid) != -1:
                    i = 0
                    for name, val in varBinds:
                        list_result.append(val.prettyPrint())
                else:
                    i = i + 1
                    flag = False
            else:
                file.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'Error snmp_getnextcmd_next ip = ' + ip + ' OID = '+ OID[0] + '\n')
                print('Error snmp_getnextcmd_next ', False)
                i = i + 1
                flag = False
    list_result2.append(list_result)

    return list_result2

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

def check_ip2(ip): # проверка ip адреса
    try:
        ip_interface(ip)
    except ValueError:
        return False
    else:
        return True

def get_from_mysql(host, user, password, db_name, query, fd):
    flag = True
    try:
        cnx = mysql.connector.connect(user=user, password = password, host = host ,database = db_name)
        if cnx.is_connected():
            print('Connected to MySQL database')
            fd.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'Connected to MySQL database '+ host + '\n')
            cursor1 = cnx.cursor()
            cursor1.execute(query)
            query_result = []
            i=0
            list = []
            for var in cursor1:
                query_result.append(str(var)[1:-2])
            for var in query_result:
                # проверяем, что то что отдал констрактор это ip адрес а не мусор
                if var.isdigit():
                   if check_ip(int(var)):
                      list.append(str(ip_address(int(var))))
                       #    query_result.append(str(ip_address(int(str(var)[1:-2]))))

    except mysql.connector.Error as err:
        if err.errno == errorcode.ER_ACCESS_DENIED_ERROR:
            print("Something is wrong with your user name or password")
            fd.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + "Something is wrong with your user name or password" + '\n')
            flag = False
        elif err.errno == errorcode.ER_BAD_DB_ERROR:
            print("Database does not exist")
            fd.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + "Database does not exist" + '\n')
            flag = False
        else:
            print(err)
            fd(err)
            flag = False
    cursor1.close()
    cnx.close()

    return (list, flag)

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)
    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')

#получаем список адресов с базы

if select_source_ip == 'file':
    ip_from_confstractor = get_from_file(filename_of_ip, filed)
else:
    if select_source_ip == 'mysql':
        ip_from_confstractor, flag_mysql = get_from_mysql(host , username_db, password_db, db, query_from_db, filed)

if log_level == 'debug':
    filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'ip address from ' + select_source_ip + '\n')
    for var in ip_from_confstractor:
        filed.write(var + '\n')

# получаем общее количество ifindex оказалось не нужно
#number_ifindex = (snmp_get_next(community_string, ip_address_host, port_snmp, OID_ifNumber))

for ip_address_host in ip_from_confstractor:
    # получаем 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
#            print("check domain хост не отдал домен", sysname, " ", ip_address_host , "\n")
                if log_level == 'debug' or log_level == 'normal':
                    filed.write("check domain хост не отдал  домен: " + sysname + " " + ip_address_host + " " + "\n")
            else:
                # это условие по уровню записи в лог
                pass
    #получаем адреса, ifindex, маски грузим в двумерный список
    # testresult[0] = адреса
    # testresult[1] = ifindex
    # testresult[2] = маски
            testresult = snmp_getnextcmd_next(community_string, ip_address_host, port_snmp, list_OID, filed)
            i = 0
            for var in testresult[0]: # убираем адреса не попадающие в ip_segment_ds
                flag = 'False'
                for net in ip_segment_ds:
                   if (IPv4Address(var) in IPv4Network(net)):
                      flag = 'True'
                if flag == 'True':
                    pass
                else:
                    testresult[0].pop(i)
                    testresult[1].pop(i)
                i = i +1

    # зная ifindex, заполняем desc интерфейсов, добавляем отдельным списком, меняем пробелы и дефисы на подчеркивания
    # testresult[3] = desc
            desc = []
            i = 0
            for var in testresult[1]:
                x, flag_snmp_get = snmp_get_next(community_string, ip_address_host, port_snmp, OID_ifAlias+'.'+var, filed)
                if flag_snmp_get:
                    x = x.replace('"', '').replace(' ', '_').replace('(', '').replace(')', '').replace(
                        ',', '.').replace(']','').replace('[','').replace("'","").replace('-_','').replace('_-','')
                    desc.append(x)
                    i = i + 1
                else:
                    i = i +1
                    testresult[1].pop(i) # если хост не ответил на запрос desc по ifindex, не переспрашиваем, а убираем ifindex из списка
                # так как данные для него собрать не удалось. соответственно имя интерфейса для него запрашивать не будем.
                    testresult[0].pop(i) # адрес тоже убираем, иначе произойдёт путаница при сборе файлов зон

            testresult.append(desc) # добавляем третьим

    # зная ifindex, собираем имена интерфейсов
            desc = []
            for var in testresult[1]:
                x, flag_snmp_get = snmp_get_next(community_string, ip_address_host, port_snmp, OID_ifName + '.' + var, filed)
                if flag_snmp_get:
                    x = x.replace('Loopback', 'Lo').replace('loopback0', 'Lo0').replace(' ', '_')
                    desc.append(x)
                    i = i + 1
                else:
                    i = i + 1
                    testresult[1].pop(i)  # если хост не ответил на запрос имени интерфейса по ifindex, не переспрашиваем, а убираем ifindex из списка
            # так как данные для него собрать не удалось.
                    testresult[0].pop(i)  # адрес тоже убираем, иначе произойдёт путаница при сборе файлов зон или tracebrake получим
                    testresult[3].pop(i) # desc убираем, иначем произойдёт путаница при сборе файлов зон или tracebrake получим

            testresult.append(desc) # добавляем четвёртым
        #testresult[4] названия интерфейсов

            i = 0
            for var in testresult[3]:
                if var == 'No_Such_Object_currently_exists_at_this_OID': # для APC и убогих Zyxel, не поддерживают OID desc
                    testresult[3][i] = ''
                    testresult[4][i] = 'Lo0'
                i = i + 1
            if log_level == 'debug':
                for var in testresult:
                    filed.write(str(var) + '\n')
    # формируем данные для  файлов зон
            i=0
            for var in testresult[0]:
                rev = var.split('.')
                revs = rev[:]
                revs.reverse()
                revsstr='.'.join(revs)
    # заполняем словарь обратных зон
                if (rev[0]+'.'+rev[1]) in reverse_zone:
                    if testresult[4][i] == 'Lo0':
                        reverse_zone[rev[0]+'.'+rev[1]].append(revsstr + '.in-addr.arpa.     IN     PTR     ' + sysname + '.')
                    else:
                        reverse_zone[rev[0] + '.' + rev[1]].append(revsstr + '.in-addr.arpa.     IN     PTR     ' + testresult[4][i] + '.' + testresult[3][i] + '.' + sysname + '.')
                else:
                    reverse_zone[rev[0]+'.'+rev[1]] = []
                    if testresult[4][i] == 'Lo0':
                        reverse_zone[rev[0] + '.' + rev[1]].append(revsstr + '.in-addr.arpa.      IN      PTR     ' + sysname + '.')
                    else:
                        reverse_zone[rev[0] + '.' + rev[1]].append(
                            revsstr + '.in-addr.arpa.     IN      PTR     ' + testresult[4][i] + '.' + testresult[3][
                                i] + '.' + sysname + '.')
    # заполняем список прямой зоны
                if testresult[4][i] == 'Lo0':
                    direct_zone.append(sysname + '.' + '    IN    A   ' + var)
                elif testresult[3][i] == '':
                    direct_zone.append(testresult[4][i]+'.' + sysname + '.' +'    IN     A   ' + var)
                else:
                    direct_zone.append(testresult[4][i] + '.' + testresult[3][i] + '.' + sysname + '.' + ' IN     A   ' + var)
                i=i+1
    else:
        print('Error хост не ответил при запросе ip ', ip_address_host)
        filed.write(datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + ' : ' + 'Error хост не ответил при запросе snmp, ip хоста ' + ip_address_host + ' ' + str(flag_snmp_get) + '\n')

# записываем всё собранное в файлы

f=open(file_direct_zone, 'w') # файл прямой зоны
f.write(named_soa + '\n'+'\n')
f.write(name_server_record)
for i in direct_zone:
    i = i.replace('..','.')
    f.write(i + '\n')
f.close()

file_list_reverse = list(reverse_zone.keys())
print(file_list_reverse)
if log_level == 'debug':
    for i in file_list_reverse:
        filed.write(i + '\n')

for i in list(reverse_zone.keys()): # записываем файлы обратных зон
    f=open(i+'.rev','w')
    f.write(named_soa + '\n'+'\n')
    for y in reverse_zone[i]:
        y = y.replace('..', '.')
        f.write(y + '\n')
    f.close()

# пишем в лог время
filed.write('\n' + datetime.strftime(datetime.now(), "%Y.%m.%d %H:%M:%S") + '\n')

#закрываем лог файл
filed.close()


P.S.: Ссылки


  • Про SNMP:

Wiki Debian про SNMP
Хорошая статья «SNMP MIBs и как их готовить»

  • Про mib браузеры:

MIB browser от iReasoning
В бесплатной версии одновременно можно работать только с 10 MIB.
OpManager FREE MIB Browser

  • Про Python:

Скрипт плохо соответствует требованиям PEP8 Style Guide for Python Code, что не очень хорошо.
Если вы желаете красиво и правильно собирать логи с ваших скриптов, то вам сюда logging — Logging facility for Python

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


  1. kovserg
    15.10.2017 00:36

    Как же мне всегда нравилось: для решения простой задачи надо написать кучу кода.
    SMNP = Simple Network Management Protocol
    Видимо слово Simple означает нечто большее чем «просто».

    ps: Сетевеки особо не заморачиваются используют что-нибудь готовое типа PRTG


    1. zw_irk Автор
      15.10.2017 03:18

      Подскажите, что готовое позволяет формировать файлы зон для Bind?


    1. evseev
      15.10.2017 07:30

      Даже простая задача требует внимания и каждая ошибка может дорого обойтись. Что примечательно. Скрипты не ошибаются, они не бывают уставшими и никогда не бывают сонными. И что касается рутины- они далеко впереди человека. Так что лучше скрипты, чем руками.

      PS: а где вы видите кучу кода?


  1. ALexhha
    15.10.2017 01:40

    Я вот только не пойму, какие именно преимущества даёт питон, по сравнению с тем же bash, конкретно в данных примерах?


    1. zw_irk Автор
      15.10.2017 03:23

      Я думаю, что всем будем интересно, если вы реализуете решение это задачи на bash и напишите статью.


      1. kvaps
        15.10.2017 12:32
        +1

        Ну я для подобных вещей использую ansible со стандартными модулями.
        Причем если данные собранные ansible нужно сильно модифицировать перед отправкой, не вижу проблем использовать local_action с shell модулем, где данные можно подготовить с помощью того bash (или python).
        В итоге получаем универсальный playbook с простыми набором задач, где каждая задача делает своё конкретное действие:


        • собирает данные
        • генерирует новые данные на основе полученных
        • записывает конфиг или генерит команды
        • отправляет конфиг или удаленно исполняет команды

        На данный момент, мне кажется что это решение идеальное во всем. Так как оно универсальное, в нем легко разобраться и оно отлично работает.


        PS: Вы не подумайте, мне очень интересна ваша статья и я определенно нашел в ней много полезного для себя. Но в силу того что я не очень хорошо знаю python, я бы реализовал это именно так.
        А каково ваше мнение на счёт ansible?


        1. zw_irk Автор
          15.10.2017 14:48
          +1

          Я в первой статье упоминал и про ansible, и про puppet, и про модули для них от вендоров сетевого оборудования. Далее в планах начать рассказ про использование ansible в сопровождении сетевого оборудования:)


    1. evseev
      15.10.2017 07:43
      +1

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


      1. ndiezel
        15.10.2017 11:00

        Так и пишите — не знаю.


        1. evseev
          17.10.2017 19:23

          Конечно я не знаю правильный ответ на этот вопрос для всего человечества в целом. Если знаете- делитесь ;)


      1. ALexhha
        15.10.2017 13:28

        Я, конечно, может что-то не понимаю, но зачем для таких примитивных задач еще и IDE? А то получается — пишем на питоне потому что стильно/модно/молодежно. Я сам использую питон и люблю его, но в контексте данной статьи я не вижу какие преимущества он дает сетевикам в задачах простейшего парсинга вывода.


        1. zw_irk Автор
          15.10.2017 14:51

          в задачах простейшего парсинга вывода.


          Не совсем понял, парсинг вывода чего вы имеет ввиду? Скрипт не пользуется внешними утилитами.


          1. ALexhha
            15.10.2017 15:22

            Я про изначальную задачу ТС.

            Необходимо сформировать доменные имена для маршрутизаторов и их интерфейсов. Скрипт должен из подобной конфигурации сформировать две записи, для прямой зоны и для обратной зоны
            А по сути обычный парсинг вывода snmpwalk

            fkvf
            за книжку спасибо, давно не общался с Наташей, а она уже и книжки начала писать :)


            1. zw_irk Автор
              15.10.2017 15:25

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


              1. ALexhha
                15.10.2017 16:58

                Скрипт не использует для своей работы внешние утилиты и, соответственно, ничего не парсит.
                т.е. по-вашему функция snmp_getnextcmd_next это не парсинг?


                1. zw_irk Автор
                  15.10.2017 17:36

                  Она собирает данные по snmp, перебирая полученные nextом OIDы и вылавливает ошибки. Полученные данные она складывает в список. Что вы называете парсингом?


        1. evseev
          17.10.2017 19:25

          IDE что бы экономить время. Мы же не про программиста говорим для которого программа- основная часть работы. Для сетевика это скорее побочная нагрузка.


  1. fkvf
    15.10.2017 10:57
    +1

    Python для сетевых инженеров. Книга на gitbook
    www.gitbook.com/book/natenka/pyneng/details


    1. zw_irk Автор
      15.10.2017 11:30

      Хорошая книга, судя по содержанию. Как это я её не заметил.