Ковыряемся в Annet, часть 1, Сетап лабы, взаимодействие с API-Netbox
Python
]
Привет, друг!
Это неотъемлемая часть Ковыряемся в Annet, часть 0, там все пояснения, поэтому сразу к делу.
По ходу написания статьи вышло так, что вся статья отдана под Netbox и работу с ним. Что касается сетапа лабы, он быдет следующий:
- Поднять Netbox в докере
- Настроить минимум объектов Netbox (используя API) чтобы annet хватило данных для дальнейшей работы с устройствами напрямую
- Поднять парочку коробок в pnet/eve-ng, подойдет vios (других вендоров пока не будем трогать)
Со всем остальным будем разбираться в следующей части.
TL;DR
- Читаем README в github-репе.
- Самостоятельно добавляем в pnetlab/eve-ng девайсы с назначенными IP-адресами в netbox - это будет ключевым фактором для Annet к доступу до устройства
- ???
- PROFIT!
Netbox
Netbox - опен-сорс проект по концепции Source of Truth. В SoT содержится эталонное представление об инфраструктуре, а все остальные системы берут информацию из нее и распространяют на всю сеть, если нужно что-то добавить или проверяют совпадает ли план с фактом, если нужно понять дифф текущих конфигов на устройствах в работе.
За деталями прошу в статью к Марату в очередной части АДСМ.
Ставить Netbox руками то еще удовольствие, поэтому заюзаем docker версию.
Инструкция из quickstart-гайда вполне подходит:
git clone -b release https://github.com/netbox-community/netbox-docker.git
cd netbox-docker
tee docker-compose.override.yml <<EOF
services:
netbox:
ports:
- 8000:8080
EOF
docker compose pull
docker compose up
Чтобы начать пользовать Netbox, нужен superuser. Для этого надо подключиться к запущенному контейнеру и создать его:
docker compose exec netbox /opt/netbox/netbox/manage.py createsuperuser
Далее все как в любом другом сервисе с веб-мордой, логинимся по созданному ранее superuser и готово.
Канал про автоматизацию, поэтому и наполнять Netbox тоже попробуем не в Web-GUI, а через API + Python. У меня опыта автоматизации netbox не было, поэтому код будет дубовый и только для выполнения текущей задачи.
Не используйте примеры кода из моих статей в проде, они несут чисто функциональный смысл и попытки чему-нибудь научиться.
Предположим, что netbox у нас чистый, накидаем минимум полезной инфы. которая нам нужна для взаимодействия с annet:
- site
- manufacturers
- device role
- device type
- device
- ipam prefix
- ipam address
Если коротко, то в Netbox все сгруппировано по функциям.
Netbox требует, чтобы до создания вышестоящего объекта, были созданы все необходимы объекты уровнем ниже. На схеме попробовал изобразить это визуально, ибо текстом было ну совсем убого…
Пунктирными линиями обозначены необязательные связи, сплошной - обязательные:
Автоматизировать будем по API, поэтому начнем сразу с нее.
Если не сталкивались с темой API, рекомендую отличную обзорную статью Марата в цикле АДСМ. Заметки. RESTful API
Найти доку по API можно на странице Netboxа. В двух вариантах:
- эндпоинты REST API, где можно потыкать и поглядеть через
GET
где что лежит, какую структуру имеет и что возвращает - целый инструмент под названием Swagger, спецификация REST API, где можно делать уже не только GET, но и все остальное. И это, в целом, вполне удобно.
Как взаимодействовать с API?
Начнем издалека и потихоньку будем продвигаться в сторону python. Подробно опишу на примере с созданием Site, а дальше уже по мере необходимости, да и в комменты всегда можно дойти, если вопросы остались.
Будем использовать сначала Swagger, чуть позже дойдем до приложухи Postman (опен-сорс аналог Postman), а уже после поглядим на скрипты.
Эндпоинтов в API Netboxа много, поэтому пользуемся ctrl+f и ищем site
, а если прям совсем точно, то /api/dcim/sites/
:
Из всех доступных методов, чтобы создать новый объект, нам понадобится метод POST:
Если не знакомы с типами данных, не проблема, сейчас коротко пробежимся по тому, что из себя представляет схема объекта Site.
Для начала - это JSON (JavaScript Object Notation) - текстовый формат обмена данными, основанный на JavaScript.
JSON основан на двух структурах данных:
- Коллекция пар ключ/значение. В разных языках, эта концепция реализована как объект, запись, структура, словарь, хэш, именованный список или ассоциативный массив.
- Упорядоченный список значений. В большинстве языков это реализовано как массив, вектор, список или последовательность.
Объект - неупорядоченный набор пар ключ/значение. Объект начинается с {
открывающей фигурной скобки и заканчивается }
закрывающей фигурной скобкой. Каждое имя сопровождается :
двоеточием, пары ключ/значение разделяются ,
запятой.
Массив - упорядоченная коллекция значений. Массив начинается с [
открывающей квадратной скобки и заканчивается ]
закрывающей квадратной скобкой. Значения разделены ,
запятой.
Значение - может быть строкой в двойных кавычках, числом, true, false, null, объектом или массивом. Эти структуры могут быть вложенными.
Посмотрим на объект WritableSiteRequest
(он же словарь в терминах Python) - описывает Site в NetBox со всеми присущими ему параметрами. Содержит обязательные и опциональные поля.
Основные ключи внутри объекта:
name
(string) : Название сайта, строка;slug
(string) : Уникальный идентификатор сайта (в нижнем регистре), строка;status
(string): Текущий статус сайта (например,planned
,active
).
Вложенные объекты
Тут все прямолинейно, один объект лежит внутри основного объекта == вложенный объект. В нашем случае это объекты (которые при этом еще и являются ключами для основного WritableSiteRequest
):
region
(object): Регион сайта, объект;group
(object): Группа сайтов, объект;tenant
(object): Владелец, связанный с сайтом, объект.
Каждый объект (region
, group
, tenant
) включает свои ключи, относящиеся только к этому объекту:
name
(string): Название, строка;slug
(string): Уникальный идентификатор, строка;description
(string): Описание, строка.
Дополнительные пары ключ: значение, которые не являются обязательными для создания объекта Site:
facility
,time_zone
,description
: Общие сведения о сайте;physical_address
,shipping_address
: Физический и почтовый адреса;comments
: Дополнительные заметки;latitude
иlongitude
: Географические координаты сайта.
Массивы
Ключами основного объекта могут быть массивы (они же списки в терминах Python). Например tags
представляет из себя массив, внутри которого лежит объект NestedTagRequest
. Каждый объект в массиве tags описывает один тег.
В Python это называется “список словарей”, это удобно, когда нужно поместить несколько сложных объектов в рамках одной сущности, например повесить теги на сайт. Выглядеть на практике это может как-то так:
### вывод обрезан
"tags": [
{
"id": 1,
"url": "http://192.168.2.126:8000/api/extras/tags/1/",
"display_url": "http://192.168.2.126:8000/extras/tags/1/",
"display": "tag1",
"name": "tag1",
"slug": "tag1",
"color": "9e9e9e"
},
{
"id": 2,
"url": "http://192.168.2.126:8000/api/extras/tags/2/",
"display_url": "http://192.168.2.126:8000/extras/tags/2/",
"display": "tag2",
"name": "tag2",
"slug": "tag2",
"color": "ff5722"
}
],
### вывод обрезан
В ответе от моего Netbox-а в части тегов мы видим следующее:
tags
: ключ внутри основного объектаWritableSiteRequest
со значением (value) в виде массива из объектов.- А внутрях видим два объекта
NestedTagRequest
, каждый из которых обозначает конкретный тег.
WritableSiteRequest и NestedTagRequest — на самом деле классы, которые можно найти в исходном коде NetBox. Эти классы представляют собой сериализаторы, использующиеся для работы с API. Они определяют структуру данных, отправляемых в запросах. Это территория фреймворка Django (на нем написан Netbox), туда мы пока копать не будем.
Пример в Swagger
При создании чего-либо в netbox есть обязательные поля и необязательные. По красным звездам в схеме видно, что обязательные только name
и slug
.
Возвращаемся в предыдущую вкладку с примером запроса и выкидываем лишне. Остается основной объект (без него совсем никак) и два ключа со значением типа string
:
{
"name": "Singapore",
"slug": "singapore"
}
Тыкаем на Try it out, вставляем этот коротенький запрос в Request и пробуем execute. Готово! Сайт создан, можно пойти и убедиться:
Все круто, но давайте разберемся, что netbox вернул в Response подробнее. Начнем с запроса curl:
curl -X 'POST' \
'http://192.168.2.126:8000/api/dcim/sites/' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H 'X-CSRFTOKEN: COT74rKHGiigxaZgjaGzU6mRdRtK6Nmh7PxEbCrGxXn72Vl6dnYQOzbqtaaKScTg' \
-d '{ "name": "Singapore", "slug": "singapore" }'
В запросе curl есть некий заголовое X-CSRFTOKEN
. Обычно CSRF-токен получают в отдельном запросе или через авторизацию, и он подтверждает, что запрос выполняется авторизованным пользователем. Мы же в дальнейшем будем юзать API token и вот его нам и надо сейчас создать.
Идем в WEB netbox в правый верхний угол, тычем на авторизованного юзера и далее API-Tokens. Добавляем и готово, теперь у нас есть API-токен, чуть позже им воспользуемся.
Сначала удалим наш сайт, чтобы заодно поглядеть на метод DELETE.
Скроллим ниже, видим, что есть уже понятный нам /api/dcim/sites/
, но чуть ниже есть то же самое, но /api/dcim/sites/{id}/
. Почему?
Разница между /api/dcim/sites/
и /api/dcim/sites/{id}/
связана с тем, как API управляет коллекцией объектов и отдельными объектами.
Различие между /api/dcim/sites/
и /api/dcim/sites/{id}/
/api/dcim/sites/
— это конечная точка коллекции.
- Используется для операций над коллекцией объектов site, например, создания нового объекта site (с помощью POST) или получения списка всех сайтов (с помощью GET).
- Когда вы отправляете запрос на эту конечную точку без указания {id}, сервер предполагает, что вы хотите работать со всеми объектами данного типа.
Например:
GET /api/dcim/sites/
— получить список всех сайтов.POST /api/dcim/sites/
— создать новый сайт.
/api/dcim/sites/{id}/
— это конечная точка для конкретного объекта.
{id}
в URL — это плейсхолдер (так зовется та штука, когда пишут “Привет {username}” - {username} это плейсхолдер (заглушка)), обозначающий уникальный идентификатор (ID) конкретного объекта site.- Если вы знаете ID объекта, то можете обращаться к нему напрямую для выполнения операций над этим конкретным сайтом. Это полезно для получения, обновления или удаления одного конкретного объекта.
Например:
GET /api/dcim/sites/1/
— получить информацию о сайте с ID = 1.PUT /api/dcim/sites/1/
— обновить сайт с ID = 1.DELETE /api/dcim/sites/1/
— удалить сайт с ID = 1.
Почему /api/dcim/sites/{id}/
может быть полезней?
Например зная конкретный id для объекта его можно обновить/удалить без передачи каких-либо обязательных полей, чтобы однозначно определить искомый объект.
Предположим, что мы не знаем ID нашего сайта, его логичнее спросить методом GET используя эндпоинт /api/dcim/sites/
. Сделаем это.
Все в том же Swagger-е если развернув метод GET для запроса по сайту, вы обнаружите кучу параметров, которые можно задать. Если их не задавать, то вернутся вообще все сайты. Мы же хотим конкретный, поэтому ищем поле slug
и пишем туда singapore
и тычем execute. Получаем, конечно же, очередной json:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": 1,
"url": "http://192.168.2.126:8000/api/dcim/sites/1/",
"display_url": "http://192.168.2.126:8000/dcim/sites/1/",
"display": "Singapore",
"name": "Singapore",
"slug": "singapore",
"status": {
"value": "active",
"label": "Active"
},
"region": null,
"group": null,
"tenant": null,
"facility": "",
"time_zone": null,
"description": "",
"physical_address": "",
"shipping_address": "",
"latitude": null,
"longitude": null,
"comments": "",
"asns": [],
"tags": [],
"custom_fields": {},
"created": "2024-11-07T21:26:35.195729Z",
"last_updated": "2024-11-07T21:26:35.195741Z",
"circuit_count": 0,
"device_count": 0,
"prefix_count": 0,
"rack_count": 0,
"virtualmachine_count": 0,
"vlan_count": 0
}
]
}
Как вы могли уже понять, "id": 1
и есть наш искомый id.
Возвращаемся в эндпоинт /api/dcim/sites/{id}/
и гордо вписываем единичку и выполняем запрос!
Через метод GET удобно изучать сложные конструкции. Например увидеть, как именно закрываются необязательные объекты, например
null
, пустая строка, 0, пустой массив или объект.
Готово, сайта больше нет. Теперь создадим его заново, но запрос уже будет с нашим API-токеном.
Пример в Curl
Чтобы не повторяться, давайте соберем запрос прям в curl. Сам запрос можно дернуть из Swagger, убрать X-CSRFTOKEN
и добавить заголовок Authorization: Token {ваш API-токен}
:
curl -X 'POST' \
'http://192.168.2.126:8000/api/dcim/sites/' \
-H 'accept: application/json' \
-H 'Authorization: Token {ваш API-токен}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Singapore",
"slug": "singapore"
}'
Вставим это дело в cli. Оттуда (где вы юзаете cli), что логично, должен быть доступен ваш Netbox. Получим JSON по созданному объекту:
{
"id":4,
"url":"http://192.168.2.126:8000/api/dcim/sites/4/",
"display_url":"http://192.168.2.126:8000/dcim/sites/4/",
"display":"Singapore",
"name":"Singapore",
"slug":"singapore",
"status":{
"value":"active",
"label":"Active"
},
"region":null,
"group":null,
"tenant":null,
"facility":"",
"time_zone":null,
"description":"",
"physical_address":"",
"shipping_address":"",
"latitude":null,
"longitude":null,
"comments":"",
"asns":[
],
"tags":[
],
"custom_fields":{
},
"created":"2024-11-07T22:10:51.643016Z",
"last_updated":"2024-11-07T22:10:51.643047Z"
}
И вот теперь… Что же мы передавали в curl?
Тип запроса: POST
Используется для создания нового ресурса (в данном случае — сайта) на сервере. Передается опцией -X, --request <method>
URL Endpoint:
http://192.168.2.126:8000/api/dcim/sites/
- Это URL-адрес API-эндпоинта в NetBox для создания объекта сайта.
Заголовки (Headers):
Заголовки помогают решить недопонимания между клиентом и сервером. Иными словами - описывают происходящее взаимодействие.
accept: application/json
— Указывает, что клиент ожидает ответ в формате JSON.Authorization: Token {ваш API-токен}
— Заголовок для аутентификации; содержит API-токен.{ваш API-токен}
нужно заменить на фактический токен.Content-Type: application/json
— Указывает, что данные, отправляемые на сервер, находятся в формате JSON.
Все заголовки передаются опцией -H, --header <header/@file>
Тело запроса:
Ну и тело запроса, которое вы уже видели и знаете что к чему (но не совсем). Передается опцией -d, --data <data>
.
На основе инфы выше можно брать и идти играться со Swagger и попробовать посоздавать, позапрашивать и поудалять объекты т.е GET, POST, DELETE. Методы PUT/PATCH оставим на попозже.
Накидываем необходимые объекты
Очередной вариант взаимодействия с API - программные платформы с различными удобствами по взаимодействию с API.
Продолжаем погружение с учетом пройденного. Напомню, нам нужно собрать в Netbox следующий минимум:
- site
- manufacturers
- device role
- device type
- device
- ipam prefix
- ipam address
Тут мы можем уже прикинуть какие минимальные JSON-ы с данными нам нужны, чтобы положить их в тело запроса и определим для них эндпоинты, а потом подумаем куда (и как) их лучше послать.
Мы точно хотим видеть новый сайт:
/api/dcim/sites/
{
"name": "Singapore",
"slug": "singapore"
}
Хотим назначить некоторого производителя, пусть будет Cisco:
/api/dcim/manufacturers/
{
"name": "Cisco",
"slug": "cisco"
}
Ролей устройств будет две. Предположим, что у нас будет 2 access и 1 destributed коммутаторов:
/api/dcim/device-roles/
{
"name": "Access Switch",
"slug": "access-switch"
}
{
"name": "Destributed Switch",
"slug": "destributed-switch"
}
Тип устройства будет один и тот же в рамках лабы, назовем его vIOS:
/api/dcim/device-types/
{
"manufacturer": {
"name": "Cisco",
"slug": "cisco"
},
"model": "vIOS",
"slug": "vios"
}
После мы можем создать некоторый префикс, чтобы в дальнейшем дойти к вопросу primary-ip для устройства:
/api/ipam/prefixes/
{
"prefix": "192.168.10.0/24",
}
И создать необходимые адреса, которые будут частью созданного префикса:
/api/ipam/ip-addresses/
{
"address": "192.168.10.0/24",
"status": "active"
}
А теперь мы можем создать непосредственно само устройство:
/api/dcim/devices/
{
"name": "string",
"device_type": {
"manufacturer": {
"name": "string",
"slug": "string"
},
"model": "string",
"slug": "string"
},
"role": {
"name": "string",
"slug": "string"
},
"site": {
"name": "string",
"slug": "string"
}
"primary_ipv4": "string"
}
Так же нам не помешает уже после создания, внутри каждого устройства:
- присвоить IP-адрес;
- отметить management интерфейс как основной (primary)
Если не сталкивались с изучением API - самое сложное, на мой взгляд, момент познания кто, в какой последовательности, какими ручками и куда обращается. Но уже, если считать в часах, спустя условных 15 часов становится сильно проще.
Поехали пробовать следующий инструмент.
Пример в Postman
Postman - это набор инструмент для тестирования различных API. Он не то чтобы прям бесплатный, например создать учетку, чтобы снять некоторые ограничения, придется и там есть подписка, но она нам ни к чему, бесплатной версии за глаза.
Можно работать из веба, но рекомендую поставить приложуху, пошустрее будет.
В чем отличие Postman от того же Swagger? А в том, что все удобнее и быстрее, сейчас покажу.
Чтобы не рисовать стрелочки заново, украду из статьи Марата пару картинок про Postman. Они уже не совсем совпадают с тем, что есть сейчас, но этот момент я закрою чуть дальше уже своими картинками :) А пока, описание кнопок и прочего разного для GET:
И для POST:
Обе картинки взяты из статьи АДСМ. Заметки. RESTful API
Вставляем url, добавляем заголовки и пр. полезное, тычем Send и получаем вывод, все предсказуемо. Но кое в чем Postman удобнее чем Swagger - импортом ВСЕЙ спецификации API и переменными на всю коллекцию.
Интерфейс Postman выглядит перегруженным, но нам не потребуется весь имеющийся функционал.
Из Swagger заберем слепок API в виде YAML, делается выгрузка, внезапно, во мелкой ссылке /api/schema/
, но по факту это ссылка вида: http://192.168.2.126:8000/api/schema/swagger-ui/
:
Дальше нужно запихать файл в Import
обычным drag&drop:
Сначала идем в настройки импорта, крутим ниже, там нужно нажать галку Always inherit authentication
:
Зачем нам это? чтобы задать в родителе коллекции аутентификацию, а во всех остальных “детях” будет стоять что-то типа:
Далее выбираем Postman Collection и готовое:
Далее слева появится полная коллекция запросов API с преднастроенными параметрами, заголовками и глобальной переменной - BaseURL (она распространяется на все эндпоинты)
Т.к мы выставили настройку по наследованию секретов, нам достаточно указать заголовок Authorization в верхнем родителе, в остальных докинется автоматом:
И останется докинуть в Variables
наш BaseUrl:
Дальше выбираем что нужно и сразу тыкаем Send, например попробуем получить Status:
API-ключ подтянулся от родителя, ответ 200ОК:
Еще из удобного, слева есть APIs, где можно найти то же самое описалово со схемами по каждому эндпоинту, что мы видели в Swagger:
Postman достаточно мощная штука, например, можно конфигурить Flows (что-то типа скрипта из запросов):
Из более полезного, можно прикрутить mock-сервер, это когда локально запускается условный бэкэнд, в который все так же можно кидаться запросами, как и в настоящий, но возвращать он будет то, что мы настроим изначально. Удобно, когда нет доступа до сервера, а потестировать надо. В сетевой автоматизации тоже есть такая штука, когда можно собрать mock-сервер условной коробки и рассказать ей что отдавать на определенные команды.
На этом, пожалуй, все. Я с Postman плотно работать не пробовал, если есть опыт и я что-то упустил полезного, приходите в чатик телеги.
Далее уже можно с помощью любого из средств, описанных выше, создать в Netbox все, что нам необходимо
Пример скрипта Python + API Netbox
У нас остался еще один способ (на самом деле два) т.к есть netbox-python и pynetbox, но мы же учимся, поэтому пока будут превозмогания без сторонних библиотек. Скрипт на питоне с применением прямых API запросов.
Глобально, особенно во времена развития нейронок, как я для себя понял, есть три пути:
- Писать код самому (долго)
- Использовать нейронки и бездумно копипастить код, в надежде, что все заработает (может быть очень быстро, но пользы для обучения будет немного)
- Использовать оба подхода т.е брать нейронку в копайлоты и пробовать разбираться по дороге (что-то посередине)
Я решил ехать по 3-му пути т.к предполагал, что это будет проще (ахахах) и быстрее.
Можно все написать в одном файле т.к тема статьи не про архитектуру кода и пр., но я хочу попробовать придерживаться золотой середины, поэтому минимально таки поделим наш код на директории и файлы.
Структура кода
Пробежимся по структуре кода и на этом закончим. Остальное переедет на следующие части.
mkdir netbox
cd netbox
tree
~/netbox
.
├── README.md
├── config.yml
├── inventory
│ └── inventory.yml
├── main.py
├── requirements.txt
├── scripts
│ └── create_site.py
└── utils
├── api_utils.py
├── config_loader.py
├── object_utils.py
└── utils.py
Небольшая ремарка, я попробовал сразу делать код модульным т.е create_site.py
это скрипт, который содержит в себе логику по старту всех вспомогательных вещей из utils
. Если нужно сделать что-то еще, пишем отдельный скрипт и при необходимости дергаем уже существующие функции из предсказуемых мест.
Начинается все в main.py
через argparse
принимается yml-файлик из inventory/inventory.yml
и разбирается на составляющие, затем передается в create_site.py
где реализована основная логика задачи.
По дороге я накидывал комментарии с описаловом, должны выглядеть достаточно последовательно. Если не понятно/что-то совсем говно, приходит дискутировать в чат.
В create_site.py
импортируем вспомогательные функции из utils.object_utils
там я старался приземлять функции имеющие отношение к манипуляциям с объектами Netbox.
В utils.object_utils
есть импорты из utils.api_utils
и utils.utils
. В api_utils
собрано все, что взаимодействует с API, да и удобно сделать импорт config_loader
в одном месте. В utils
собраны совсем вспомогательные функции, которые делают легче обработку какой-либо логики в остальных кусках кода.
config_loader
говорит сам за себя, тут лежит функция обработки config.yml
, который хранится в корне. Путь до config.yml
определяется через pathlib
.
В общем то и весь код. Без классов, датаклассов и прочего можного, а еще без нормального алгоритма поиска свободного имени, если оно уже присутствует в Netbox. Да, грубый перебор и чем больше устройств, тем дольше :) А если роли разные, но device_type одинаков. то интерфейсы из второй роли будут повторно пробовать пихаться в тот же device_type (спрятано за банальную проверку на наличие в Netbox-е и logger.debug)
Чуть ниже расскажу как завести у себя и потыкать, если будет желание.
Как читать inventory.yml
Изначальную задумку проще всего объяснить на основе инвентори:
site_name: "msk"
manufacturer_name: "Cisco"
devices:
- role: "access-switch"
model: "vIOS-a"
count: 2
name_suffix: "swa"
role_color: "1da670"
subnet: "192.168.10.0/24"
interfaces:
- interface_range: "Gi1/0/[1-24]"
interface_type: "1000base-t"
- interface_range: "vlan10"
interface_type: "virtual"
primary: True
- interface_range: "vlan20"
interface_type: "virtual"
primary: False
- role: "destributed-switch"
model: "vIOS-d"
count: 1
name_suffix: "swd"
role_color: '00ffff'
subnet: "192.168.10.0/24"
interfaces:
- interface_range: "Gi1/0/[1-24]"
interface_type: "1000base-t"
- interface_range: "vlan10"
interface_type: "virtual"
primary: True
prefix: "192.168.10.0/24"
Я попробовал заюзать декларативный подход, когда необходимое можно описать в файлике, а на уровне кода логика разберется что и куда.
Все поля в yml - обязательные т.к не обкладывал никакими проверками.
Читаем инвентори сверху вниз - требуется создать сайт с названием msk
и проивзодителя оборудования Cisco
. Далее все будет плясать от устройства. Внутри devices
может быть сколько нужно блоков с разными видами устройств. Указываем имя роли - role
, модель - model
, количество устройств - count
, которое нужно будет создать в рамках этой роли, цвет роли внутри Netbox - role_color
и задаем подсеть, откуда устройству будут запрошены IP-адреса у Netbox-а. Блок interfaces
нужен для назначения в созданный device_type, который будет создан на основе model
.
Отдельно создается prefix
, в который netbox, по дефолту, сложит те адреса, которые были созданы для устройств.
В каждом интерфейсе есть указатель primary
, который отвечает за то, на какой интерфейс будет назначен IP-адрес устройства.
Если после добавить еще интерфейсов в существующий device_type, уже созданные устройства его не подхватят, поэтому это выглядит как отдельный скрипт и тут не учитывается.
Создание интерфейсов работает как строка (что напишешь, то и будет), но есть логика по генерации списка интерфейсво аки в самом Netbox-е аля Gi1/0/[1-24]
говорит, что Gi1/0/
будет оставлено как есть, а диапазон в []
будет разобран по порядку, в итоге получается что-то типа:
gi1/0/1
gi1/0/2
gi1/0/3
..
gi1/0/24
count
- это число (int), все остальные поля - строки. Никаких проверок нет, все отдано на откуп Netboxу и REST кодам с ошибками. Это сложнее дебажить, но с обкладыванием тестами я бы просидел еще дольше, а некстхоп уже вот вот :)
Немного про данные, которые принимает Netbox и их надо передавать так, и больше никак.
Все виды интерфейсов, доступные в interface_type
:
- `virtual` - Virtual
- `bridge` - Bridge
- `lag` - Link Aggregation Group (LAG)
- `100base-fx` - 100BASE-FX (10/100ME FIBER)
- `100base-lfx` - 100BASE-LFX (10/100ME FIBER)
- `100base-tx` - 100BASE-TX (10/100ME)
- `100base-t1` - 100BASE-T1 (10/100ME Single Pair)
- `1000base-t` - 1000BASE-T (1GE)
- `1000base-tx` - 1000BASE-TX (1GE)
- `2.5gbase-t` - 2.5GBASE-T (2.5GE)
- `5gbase-t` - 5GBASE-T (5GE)
- `10gbase-t` - 10GBASE-T (10GE)
- `10gbase-cx4` - 10GBASE-CX4 (10GE)
- `100base-x-sfp` - SFP (100ME)
- `1000base-x-gbic` - GBIC (1GE)
- `1000base-x-sfp` - SFP (1GE)
- `10gbase-x-sfpp` - SFP+ (10GE)
- `10gbase-x-xfp` - XFP (10GE)
- `10gbase-x-xenpak` - XENPAK (10GE)
- `10gbase-x-x2` - X2 (10GE)
- `25gbase-x-sfp28` - SFP28 (25GE)
- `50gbase-x-sfp56` - SFP56 (50GE)
- `40gbase-x-qsfpp` - QSFP+ (40GE)
- `50gbase-x-sfp28` - QSFP28 (50GE)
- `100gbase-x-cfp` - CFP (100GE)
- `100gbase-x-cfp2` - CFP2 (100GE)
- `200gbase-x-cfp2` - CFP2 (200GE)
- `400gbase-x-cfp2` - CFP2 (400GE)
- `100gbase-x-cfp4` - CFP4 (100GE)
- `100gbase-x-cxp` - CXP (100GE)
- `100gbase-x-cpak` - Cisco CPAK (100GE)
- `100gbase-x-dsfp` - DSFP (100GE)
- `100gbase-x-sfpdd` - SFP-DD (100GE)
- `100gbase-x-qsfp28` - QSFP28 (100GE)
- `100gbase-x-qsfpdd` - QSFP-DD (100GE)
- `200gbase-x-qsfp56` - QSFP56 (200GE)
- `200gbase-x-qsfpdd` - QSFP-DD (200GE)
- `400gbase-x-qsfp112` - QSFP112 (400GE)
- `400gbase-x-qsfpdd` - QSFP-DD (400GE)
- `400gbase-x-osfp` - OSFP (400GE)
- `400gbase-x-osfp-rhs` - OSFP-RHS (400GE)
- `400gbase-x-cdfp` - CDFP (400GE)
- `400gbase-x-cfp8` - CPF8 (400GE)
- `800gbase-x-qsfpdd` - QSFP-DD (800GE)
- `800gbase-x-osfp` - OSFP (800GE)
- `1000base-kx` - 1000BASE-KX (1GE)
- `2.5gbase-kx` - 2.5GBASE-KX (2.5GE)
- `5gbase-kr` - 5GBASE-KR (5GE)
- `10gbase-kr` - 10GBASE-KR (10GE)
- `10gbase-kx4` - 10GBASE-KX4 (10GE)
- `25gbase-kr` - 25GBASE-KR (25GE)
- `40gbase-kr4` - 40GBASE-KR4 (40GE)
- `50gbase-kr` - 50GBASE-KR (50GE)
- `100gbase-kp4` - 100GBASE-KP4 (100GE)
- `100gbase-kr2` - 100GBASE-KR2 (100GE)
- `100gbase-kr4` - 100GBASE-KR4 (100GE)
- `ieee802.11a` - IEEE 802.11a
- `ieee802.11g` - IEEE 802.11b/g
- `ieee802.11n` - IEEE 802.11n
- `ieee802.11ac` - IEEE 802.11ac
- `ieee802.11ad` - IEEE 802.11ad
- `ieee802.11ax` - IEEE 802.11ax
- `ieee802.11ay` - IEEE 802.11ay
- `ieee802.11be` - IEEE 802.11be
- `ieee802.15.1` - IEEE 802.15.1 (Bluetooth)
- `ieee802.15.4` - IEEE 802.15.4 (LR-WPAN)
- `other-wireless` - Other (Wireless)
- `gsm` - GSM
- `cdma` - CDMA
- `lte` - LTE
- `4g` - 4G
- `5g` - 5G
- `sonet-oc3` - OC-3/STM-1
- `sonet-oc12` - OC-12/STM-4
- `sonet-oc48` - OC-48/STM-16
- `sonet-oc192` - OC-192/STM-64
- `sonet-oc768` - OC-768/STM-256
- `sonet-oc1920` - OC-1920/STM-640
- `sonet-oc3840` - OC-3840/STM-1234
- `1gfc-sfp` - SFP (1GFC)
- `2gfc-sfp` - SFP (2GFC)
- `4gfc-sfp` - SFP (4GFC)
- `8gfc-sfpp` - SFP+ (8GFC)
- `16gfc-sfpp` - SFP+ (16GFC)
- `32gfc-sfp28` - SFP28 (32GFC)
- `32gfc-sfpp` - SFP+ (32GFC)
- `64gfc-qsfpp` - QSFP+ (64GFC)
- `64gfc-sfpdd` - SFP-DD (64GFC)
- `64gfc-sfpp` - SFP+ (64GFC)
- `128gfc-qsfp28` - QSFP28 (128GFC)
- `infiniband-sdr` - SDR (2 Gbps)
- `infiniband-ddr` - DDR (4 Gbps)
- `infiniband-qdr` - QDR (8 Gbps)
- `infiniband-fdr10` - FDR10 (10 Gbps)
- `infiniband-fdr` - FDR (13.5 Gbps)
- `infiniband-edr` - EDR (25 Gbps)
- `infiniband-hdr` - HDR (50 Gbps)
- `infiniband-ndr` - NDR (100 Gbps)
- `infiniband-xdr` - XDR (250 Gbps)
- `t1` - T1 (1.544 Mbps)
- `e1` - E1 (2.048 Mbps)
- `t3` - T3 (45 Mbps)
- `e3` - E3 (34 Mbps)
- `xdsl` - xDSL
- `docsis` - DOCSIS
- `bpon` - BPON (622 Mbps / 155 Mbps)
- `epon` - EPON (1 Gbps)
- `10g-epon` - 10G-EPON (10 Gbps)
- `gpon` - GPON (2.5 Gbps / 1.25 Gbps)
- `xg-pon` - XG-PON (10 Gbps / 2.5 Gbps)
- `xgs-pon` - XGS-PON (10 Gbps)
- `ng-pon2` - NG-PON2 (TWDM-PON) (4x10 Gbps)
- `25g-pon` - 25G-PON (25 Gbps)
- `50g-pon` - 50G-PON (50 Gbps)
- `cisco-stackwise` - Cisco StackWise
- `cisco-stackwise-plus` - Cisco StackWise Plus
- `cisco-flexstack` - Cisco FlexStack
- `cisco-flexstack-plus` - Cisco FlexStack Plus
- `cisco-stackwise-80` - Cisco StackWise-80
- `cisco-stackwise-160` - Cisco StackWise-160
- `cisco-stackwise-320` - Cisco StackWise-320
- `cisco-stackwise-480` - Cisco StackWise-480
- `cisco-stackwise-1t` - Cisco StackWise-1T
- `juniper-vcp` - Juniper VCP
- `extreme-summitstack` - Extreme SummitStack
- `extreme-summitstack-128` - Extreme SummitStack-128
- `extreme-summitstack-256` - Extreme SummitStack-256
- `extreme-summitstack-512` - Extreme SummitStack-512
- `other` - Other
Цветовая палитра для color
:
class ColorChoices(ChoiceSet):
COLOR_DARK_RED = 'aa1409'
COLOR_RED = 'f44336'
COLOR_PINK = 'e91e63'
COLOR_ROSE = 'ffe4e1'
COLOR_FUCHSIA = 'ff66ff'
COLOR_PURPLE = '9c27b0'
COLOR_DARK_PURPLE = '673ab7'
COLOR_INDIGO = '3f51b5'
COLOR_BLUE = '2196f3'
COLOR_LIGHT_BLUE = '03a9f4'
COLOR_CYAN = '00bcd4'
COLOR_TEAL = '009688'
COLOR_AQUA = '00ffff'
COLOR_DARK_GREEN = '2f6a31'
COLOR_GREEN = '4caf50'
COLOR_LIGHT_GREEN = '8bc34a'
COLOR_LIME = 'cddc39'
COLOR_YELLOW = 'ffeb3b'
COLOR_AMBER = 'ffc107'
COLOR_ORANGE = 'ff9800'
COLOR_DARK_ORANGE = 'ff5722'
COLOR_BROWN = '795548'
COLOR_LIGHT_GREY = 'c0c0c0'
COLOR_GREY = '9e9e9e'
COLOR_DARK_GREY = '607d8b'
COLOR_BLACK = '111111'
COLOR_WHITE = 'ffffff'
Виртуальное окружение
Создадим для наших поделий отдельное виртуальное окружения Python.
Настоятельно рекомендую разобраться с виртуальным окружением, перед тем как спешно ставить различные модули и засорять main.
Если коротко, для чего нужно виртуальное окружение:
- изоляция проектов
- изоляция зависимостей по каждому проекту
- не нужно плясать с установкой конкретной версии python в main т.к легко создается виртуальное окружение с нужной версией python
Для начала, глобально установим pip - пакетный менеджер python
О различии pip и pip3 наглядно описано у Наташи в книге.
sudo apt install -y python3-pip
Я буду пользоваться venv т.к он входит в состав стандартной библиотеки python. Есть еще virtualwrapper, но я к нему так и не привык.
Подробнее про варианты виртуального окружения
В моем случае без установки этого пакета, venv отказывался создавать окружение с python 3.12
sudo apt install -y python3.12-venv
Создадим директорию для хранения наших окружений и перейдем в нее:
mkdir venv
cd venv
Создаем виртуальное окружение:
python3.10 -m venv annet
python3.12
- это версия python, которую хотим видеть внутри окружения.
Получим директорию с набором файлов:
bin include lib lib64 pyvenv.cfg
Переходим в окружение:
source venv/annet/bin/activate
(annet) aik@unknown:~$
Теперь в командной строке отображается название среды (annet)
.
Это означает, что виртуальное окружение активно и все пакеты pip будут установлены только в рамках окружения annet
.
Для выхода из виртуального окружения:
deactivate
Теперь можно ставить пакеты используя pip. Модули, установленные в виртуальном окружении, доступны только из виртуального окружения.
Как использовать
Код залил на гитхаб, в README описано как пользоваться.
Короткий гайд:
- Ставим netbox на виртуалку
- Создаем API-ключ
- Клонируем репу и заходим в корень:
git clone git@github.com:woohung/netbox_automation_learning.git
cd netbox
- Создаем виртуальное окружение и заходим в него
- Ставим необходимые пакеты
pip install -r requirements.txt
- Прописываем API-ключ и URL netbox-а в файл
config.py
- Правим по желанию
inventory/inventory.yml
- Запускаем скрип в корне
python main.py inventory/inventory.yml
- ???
- PROFIT!
Нормальный вывод кода должен быть +- такой:
INFO:utils.api_utils:Site 'msk' created successfully with ID: 55
INFO:utils.api_utils:Manufacturer 'Cisco' created successfully with ID: 11
INFO:utils.api_utils:Prefix 192.168.10.0/24 created successfully with ID: 16
INFO:utils.api_utils:Device type 'vIOS' created successfully with ID: 50
INFO:utils.object_utils:Created new device type 'vIOS' with ID: 50
INFO:utils.api_utils:Interface 'Gi1/0/1' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/2' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/3' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/4' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/5' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/6' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/7' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/8' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/9' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/10' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/11' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/12' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/13' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/14' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/15' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/16' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/17' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/18' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/19' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/20' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/21' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/22' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/23' created successfully.
INFO:utils.api_utils:Interface 'Gi1/0/24' created successfully.
INFO:utils.api_utils:Interface 'vlan10' created successfully.
INFO:utils.api_utils:Device role access-switch created successfully with ID: 53
INFO:utils.api_utils:Device msk-swa-01 created successfully with ID: 500
INFO:utils.object_utils:Created device 'msk-swa-01' with ID: 500
INFO:utils.api_utils:Address 192.168.10.1/24 created successfully with ID: 275
INFO:utils.object_utils:Using existing prefix '192.168.10.1/24' with ID: 275
INFO:utils.api_utils:Successfully updated ipam/ip-addresses with ID 275
INFO:utils.object_utils:Assigned IP ID '275' to interface 'vlan10' (ID: 11463)
INFO:utils.api_utils:Primary IP for device ID 500 set to IP ID 275.
INFO:utils.api_utils:Device msk-swa-02 created successfully with ID: 501
INFO:utils.object_utils:Created device 'msk-swa-02' with ID: 501
INFO:utils.api_utils:Address 192.168.10.2/24 created successfully with ID: 276
INFO:utils.object_utils:Using existing prefix '192.168.10.2/24' with ID: 276
INFO:utils.api_utils:Successfully updated ipam/ip-addresses with ID 276
INFO:utils.object_utils:Assigned IP ID '276' to interface 'vlan10' (ID: 11488)
INFO:utils.api_utils:Primary IP for device ID 501 set to IP ID 276.
INFO:utils.object_utils:Using existing device type 'vIOS' with ID: 50
INFO:utils.api_utils:Device role destributed-switch created successfully with ID: 54
INFO:utils.api_utils:Device msk-swd-01 created successfully with ID: 502
INFO:utils.object_utils:Created device 'msk-swd-01' with ID: 502
INFO:utils.api_utils:Address 192.168.10.3/24 created successfully with ID: 277
INFO:utils.object_utils:Using existing prefix '192.168.10.3/24' with ID: 277
INFO:utils.api_utils:Successfully updated ipam/ip-addresses with ID 277
INFO:utils.object_utils:Assigned IP ID '277' to interface 'vlan10' (ID: 11513)
INFO:utils.api_utils:Primary IP for device ID 502 set to IP ID 277.
Site 'msk' with devices successfully created!
Если запустить повторно с тем же инвентори, код проверит все на существование, но все равно создаст новые устройства по указанному
count
в инвентори.
Промежуточные итоги
Немного постмортема по итогам 3 дней ебки с API Netbox-а, и еще пару дней на редактуру оного текста
В очередной раз убеждаюсь, что не умею вовремя остановиться. С Netbox-ом получилось все как в той сказке..дедка за репку, бабка за дедку…Одно тянет за собой другое и учитывает третье, а еще нужно логикой обработки обмазать и т.п. Просидел я над задачей, как выше сказал, 3 дня (технически три вечера по 5 часов +-) + 2 дня и 2 ночи над редактурой текста статьи.
Немного про опыт с чатгп. Оно помогает понять некоторые моменты или накидать каркас (если с таким дела не имел) но чем больше логики, тем ему сложнее держать контекст и ИИ начинает ломать старое, добавлять новое, тянуть кривые эндпоинты в новый запрос и т.п.
Очень критичен промпт, который отдается в нейронку. Т.е без понимания происходящего написать что-то осмысленное получится маловероятно. Да, оно будет работать (спустя десяток итераций трейсов) но развивать код дальше будет та еще задача.
Подумывал прикрутить условный копайлот в nvim, но пока не взял эту высоту ибо выглядит как геморой, который не очень то и нужен. В VSCode, поговаривают, оно проще, но не проверял т.к не пишу в нем.
Думал на энтузиазме накидаю за вечер некоторое подобие рабочего кода, но по итогу погряз в разборы API Netbox-а т.к вроде все понятно, но вот когда начинаются моменты по связке одного с другим, а тем более в рамках комплексной задачи…Ну да ладно.
Pnetlab вы ведь знаете как поставить? Знаете ведь?…
Если нет, то, по идее, все еще актуальна моя инструкция.
Полезное
Хочешь обсудить тему?
С вопросами, комментариями и/или замечаниями, приходи в чат или подписывайся на Telegram-канал.