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

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

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

У нет netbox есть хорошо задокументированная API из коробки, для python есть несколько библиотек. Сисадминский грех этим не воспрользоваться.

Работать с ipam будем за счет запросов к апишке. Остается только продумать, как проверять доступность хоста по тому или иному ip. Очевидное, что приходит на ум это nmap.

Подготовка

Получение токена

Прежде чем начать, нужно сделать несколько приготовлений. Создать отдельного пользователя, и сгенерить API-Токен для него. Поэтому переходим в админку сервиса, и создаем нового пользователя: netbox-py-adm1.png

После перехода в админ панель, жмем на кнопку добавить пользователя. Указываем логин/пароль для пользователя, и сохраняемся. netbox-py-adm2.png

Если сейчас попытаемся залогиниться из под этого пользователя, панелька будет вся пустая. Что бы поправить это, нужно новому пользователю выдать права. Так как через этого пользователя будем только удалять/добавлять ip-адреса, наградим его доступом к ipam-разделу. В админке переходим в раздел Users/Permissions. netbox-py-adm3.png

В окне редактора прав (permissions), указываем имя и краткое описание для этой политики. Разрешаем действия по добавлению/удалению и изменению объектов. В поле с выбором типа объектов в списке выбираем: IPAM > IP Address и IPAM > IP Range netbox-py-adm4.png

Небольшой апдейт: В этом поинте нужно также выставить галочку, разрещающую просмотр (Can view), иначе не сможем парсить ранее добавленые адреса в ipam.

Спускаемся в конец страницы к полю выбора пользователей. И перетаскиваем нашего пользователя в правое окно. И сохраняемся. netbox-py-adm5.png

Теперь остается выдать токен для пользователя, идем в раздел Users/Tokens. Добавляем новый токен, в окне добавления выбираем пользователя. Опционально можно указать дату истечения срока работы токена и указать IP-адреса с которым можно будет подключаться. Далее жмем сохранить. netbox-py-adm6.png

На выходе, полученный токен сохраняем.

Установка зависимостей

Для работы с api нужно установить питоновские библиотеки, ну и интепретатор python соответственно. Все зависимости ставяться через питоновкий пакетный менеджер pip.

$ pip install python-nmap pynetbox

Проверять доступность хоста будем через библиотеку python-nmap, для работы которой также нужно поставить пакет nmap.

$ yum install -y nmap

Пишем код

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

Импорт либ

Начинаем с импорта требуемых библиотек:

import sys
import nmap
import pynetbox
import ipaddress
import urllib3
import socket
  • sys - используется для перехвата аргументов, которые передаются при запуске скрипта. По моему замыслу, я планирую передавать на вход программе адрес сети, которую нужно будет отсканить.
  • nmap - эта либа используется проверки доступности хоста.
  • pynetbox - либа с помошью которой будем искать, удалять или добавлять ip-адреса.
  • ipaddress - c этой библиотечкой, я смогу легко работать с сеткой. Получать длину префикса, список адресов входящих в определенный сабнет
  • urllib3 - добавил этот пакет, я отключения варнингов для самоподписных ssl-сертификатов.
  • socket - этот пакет включил, для дальнейшего получения dns имени хоста

Объявление переменных

Теперь я объявляю входные переменные, тут все просто.

# General vars
netbox_url = '<NETBOX_SERVER_URL>'
netbox_api = '<NETBOX_API_TOKEN>'
nb = pynetbox.api(url=netbox_url, token=netbox_api)

# Disable SSL and SSL Warnings
nb.http_session.verify = False
urllib3.disable_warnings()
  • netbox_url - эта переменная содержит урл-адрес netbox сервера
  • netbox_api - здесь лежит API-токен, который мы сгенерили ранее.
  • nb - инициализируем экземпляр класса api, в качестве агрументов передаем url и token.
  • nb.http_session.verify = False - тут я отключаю проверку ssl-сертификата, так как у меня используется само подписной серт.
  • urllib3.disable_warnings() - и тут отключаю нотификации относительно сертификата

Основной раздел

Так скажем entry часть программы, проверяем длинну переданных аргументов программе.

if __name__ == '__main__':
    if len(sys.argv) <= 1:
        print("You not present network, please run: \n -> python .\\netbox_scan.py 192.168.0.0/24")
    else:
        app_run(sys.argv[1])

Тут же реализовано условие, если колличество аргументов меньше или равно одному, принтануть сообщение с подсказкой как правильно запускать скриптец. Иначе запустить метод - app_run(sys.argv[1]), в метод передаем агрумент из командной строки, то есть адрес сети.

def app_run(subnet):
    ip_list = ipaddress.ip_network(subnet)
    for ip in ip_list:
        last_octet = str(ip).split('.')
        if (int(last_octet[-1]) != 255) and (int(last_octet[-1]) != 0):
            nmap_host_state = ip_scan(str(ip))
            netbox_ip_id, netbox_ip = ip_check(str(ip))
            netb_ipam_update(netbox_ip_id , 
                             str(ip), 
                             ip_list.prefixlen, 
                             nmap_host_state
            )

В этом методе получаем список всех ip-адресов, используя либу ipaddress и результат лежит в переменной ip_list. Далее циклом начинаем переберать все ip входящие в сетку, которую мы указали при запуске скрипта. В этом же цикле исключаем адреса, с последним октетом .0 и .255.

Проверка IP

Вызываем функцию ip_scan,

def ip_scan(ip_address):
    nm = nmap.PortScanner()
    nm.scan(hosts=ip_address, arguments='-sP -R')
    if len(nm.all_hosts()) != 0:
        return nm[ip_address]['status']['state']
    else: 
        return 'down'

Эта функция проверяем доступность хоста, и возвращает строкое значение - down или up. И значение помещается в переменную nmap_host_state.

Следующим этапов, в этом же цикле вызываем функцию ip_check,

def ip_check(ip_address):
    ip_search_status = nb.ipam.ip_addresses.filter(address=ip_address)
    if ip_search_status:
        for item in ip_search_status:
            return item.id, item.display
    return 0, '0.0.0.0/0'

И данный метод ходит в нетбокс, ищет наш ip и возвращает его данные dispay_address и id. Если никаких данных в процессе не было найдено, возвращается пустые значения.

Принятие решения об IP

Теперь после того, как мы чекнули доступность хоста по указанному ip и проверили наличия этого ip в базе ipam, время принимать решение что сделать с ним дальше. И это решение принимается уже в следующем методе netb_ipam_update, эта функция состоит условного оператора:

def netb_ipam_update(netbox_ip_id, netbox_ip, prefix, nmap_host_state):
    if (nmap_host_state == 'up') and (netbox_ip_id == 0):
        netbox_add_ip(netbox_ip, prefix)
    elif (nmap_host_state == 'down') and (netbox_ip_id != 0):
        netbox_remove_ip(netbox_ip_id)
    return True

При вызове функции передаются агрументы - id номер ip-адреса в netbox, ip-адрес в netbox, длина префикса сети и статус хоста с этим адресов.

В условии функции, такая логика. Если хост в апе, и нетбокс id номер равен нулю, значит этого хоста нет в базе. Хост новый, и принимается решение добавить его. Второе условие, если хост в дауне (недоступен), и его нетбокс id номер не равен нулю, то принимается решение об удалении этого хоста их базы. Все остальные условия просто пропускается.

Функции добавления/удаления

В зависимости выполннего условия запускается другой встроенный метод, у меня их два. Первый добавляем новый ip, второй удаляем. Функция, которая добавляет:

def netbox_add_ip(ip, prefix):
    ip_dns_name = get_dns_by_host(ip)
    ip_add_result = nb.ipam.ip_addresses.create(
        address = f'{ip}/{prefix}',
        dns_name = ip_dns_name
    )

На вход этому методу передаем ip-адрес и префикс сети. Внутри метода проверяем dns имя этого ip, и далее совершаем библиотечный вызов к апи нетбокса, создаем новый объект.

Функция, которая удаляет:

def netbox_remove_ip(ip_id):
    ip_search_result = nb.ipam.ip_addresses.get(id=ip_id)
    if ip_search_result is not None:
        ip_search_result.delete()

Этому методу передаем id номер существующего объекта, находим этот объект и посредством библиотечного вызова к апи удалем его из базы.

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

Прикручиваем скриптик

Завершающим этапом, прикрутиваем скриптец в cron или в scheduler. И думаю понятен тот момент, хост с которого запускается скрипт должен имет доступ в смежные сети.

Добавляем в cron задания:

# *  *  *  *  * user-name  command to be executed
0 */4 * * * root /bin/python3.8 /opt/scripts/network_scan.py '192.168.15.0/24'
0 */8 * * * root /bin/python3.8 /opt/scripts/network_scan.py '192.168.20.0/24'

После запуска скрипта, если посмотрим в changelog netbox будет винда такая история: netbox-py-result.png

Я применил скриптец на трех сетках, и как можете обратить внимание на скрине ниже, значительно выросла утилизация адресного пространства в этих сетях. netbox-py-result1.png

Дополнительные ссылки