evgen@spb: ~/2026/08/netbox-4-5-upgrade-i-smena-tipa-custom-field/

Апгрейд NetBox 4.4 → 4.5 и кастомное поле, которое пришлось чинить в PostgreSQL руками

NetBox у меня — источник правды. Из него живёт документация по железу, из него же скрипт раз в полчаса заливает хосты в Icinga Director. Поэтому апгрейд такой штуки — это не «обновил и пошёл дальше», а «обновил и проверил всё, что к ней присосалось».

Плюс рядом лежала задача, которая на первый взгляд решается за две минуты: сменить у кастомного поля тип «выбор» на «множественный выбор». Не решается. Ни через веб-интерфейс, ни через API. Пришлось идти в базу и тащить за собой все уже записанные значения.

Обе истории — в одной статье, потому что вывод у них общий.

Что проверить до апгрейда

Мажорная версия не меняется (4.4 → 4.5), так что промежуточные остановки не нужны — с 4.4.6 идём сразу на 4.5.10. Но два системных требования подросли, и оба способны превратить получасовую задачу в вечер.

Python

В 4.5 поддерживаются 3.12, 3.13 и 3.14. Поддержка 3.10 и 3.11 прекращена.

python3 --version

Если там 3.10 или 3.11 — сначала ставим подходящий интерпретатор (на Ubuntu удобнее всего через deadsnakes PPA) и пересоздаём виртуальное окружение. Именно на этом месте чаще всего и выясняется, что апгрейд был запланирован слишком оптимистично.

PostgreSQL

Четырнадцатая версия объявлена устаревшей и будет удалена в NetBox 4.7. Рабочий минимум — 15. Сейчас на 14 всё заведётся, но обновление СУБД стоит вынести в отдельную задачу, а не пристёгивать к этой: две миграции одновременно — это две причины отката вместо одной.

Бэкап

Дамп базы и снимок директории:

sudo -u postgres pg_dump netbox | gzip > /var/backups/netbox-$(date +%F).sql.gz
sudo tar czf /var/backups/netbox-dir-$(date +%F).tar.gz /opt/netbox

Второе нужнее, чем кажется: upgrade.sh пересоздаёт venv, и если в нём жили пакеты, поставленные когда-то руками, восстанавливать их придётся по памяти.

Ломающиеся изменения, важные для автоматизации

Главная особенность этого апгрейда в том, что он ломает не интерфейс, а то, что ходит в API. Веб-морда после обновления выглядит идентично — а скрипты молча перестают работать.

Что изменилосьКого затрагивает
Убрано получение токена в открытом виде через API, параметра ALLOW_TOKEN_RETRIEVAL больше нетскрипты, которые вытаскивали токен программно
Старый формат токенов объявлен устаревшим, новый — v2 с префиксом nbt_всё, что ходит в API
Изменён синтаксис GraphQL: нужны явные модификаторы lookup’овотчёты и дашборды на GraphQL
Появилось отдельное разрешение render_configучётки, дёргающие рендер конфигураций
/api/dcim/cable-terminations/ стал доступен только на чтениескрипты инвентаризации кабельной части

Если ходишь в API из pynetbox — обнови его вместе с NetBox: поддержка токенов v2 появилась в 7.6.0.

У меня из всего списка выстрелили только токены. Выстрелили бы молча, если бы я не прочитал release notes заранее — синхронизация с Icinga просто перестала бы находить хосты, а узнал бы я об этом по отсутствию новых хостов в мониторинге. То есть очень нескоро.

Сам апгрейд

Проверяем, что установка из git:

ls -d /opt/netbox/.git

Если каталог на месте — дальше всё коротко:

cd /opt/netbox
sudo git fetch --tags
sudo git checkout v4.5.10
sudo ./upgrade.sh

Если системный python3 не 3.12 и новее, интерпретатор указывается явно:

PYTHON=/usr/bin/python3.12 sudo ./upgrade.sh

Скрипт пересоберёт venv, поставит зависимости, прогонит миграции и соберёт статику. Отдельно проверь local_requirements.txt: внешние пакеты вроде django-auth-ldap должны быть прописаны именно там, иначе после пересборки окружения они просто исчезнут, а LDAP-авторизация отвалится вместе с ними.

sudo systemctl restart netbox netbox-rq
sudo systemctl status netbox netbox-rq --no-pager

Что проверить сразу после

Порядок проверки — от внутреннего к внешнему:

# версия и миграции
cd /opt/netbox && source venv/bin/activate
python netbox/manage.py showmigrations | grep -c '\[ \]'   # должен быть 0

# API отвечает и токен принимается
curl -s -H "Authorization: Token <TOKEN>" \
  https://netbox.example.com/api/status/ | python3 -m json.tool

Дальше — то, что вокруг: скрипты синхронизации, задания в cron, вебхуки, экспорт в системы мониторинга. Их логи в первые сутки после апгрейда стоит просмотреть глазами, а не полагаться на отсутствие алертов. Отсутствие алертов на скрипт, который перестал запускаться, — это тоже тишина.

Вторая история: поле «выбор» → «множественный выбор»

Есть кастомное поле monitoring_template типа «выбор» — им размечаются виртуалки, чтобы автоматика понимала, какие проверки вешать. Понадобилось разрешить несколько значений сразу.

В веб-интерфейсе тип поля после создания не редактируется вообще. Логика разработчиков понятна: за типом стоят уже записанные данные.

Через API — тоже мимо:

curl -s -X PATCH \
  -H "Authorization: Token <TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"type": "multiselect"}' \
  https://netbox.example.com/api/extras/custom-fields/<ID>/

Остаётся база. И здесь ключевой момент, из-за которого статья вообще написана: сменить тип мало.

Значения кастомных полей хранятся не в отдельных колонках, а в поле custom_field_data типа jsonb у каждого объекта. Для типа select там лежит строка "linux-servers", а multiselect ожидает массив ["linux-servers"]. Поменяешь только тип — получишь поле, которое NetBox не сможет ни отрисовать, ни провалидировать, а API начнёт отдавать ошибки на ровном месте.

То есть смена типа — это миграция данных. Просто её никто за тебя не написал.

Шаг 1. Найти поле

curl -s -H "Authorization: Token <TOKEN>" \
  https://netbox.example.com/api/extras/custom-fields/?name=monitoring_template \
  | python3 -m json.tool

Шаг 2. Сменить тип

sudo -u postgres psql netbox
SELECT id, name, type FROM extras_customfield WHERE name = 'monitoring_template';
UPDATE extras_customfield SET type = 'multiselect' WHERE name = 'monitoring_template';

Если есть сомнения в формате значения — посмотри, что вообще встречается в этой колонке:

SELECT DISTINCT type FROM extras_customfield;

Шаг 3. Найти все таблицы с данными

Поле может быть привязано к нескольким типам объектов, и у каждого своя таблица. Список даёт такой запрос:

SELECT cf.name, ct.app_label, ct.model
FROM extras_customfield cf
JOIN extras_customfield_object_types cfot ON cf.id = cfot.customfield_id
JOIN django_content_type ct ON cfot.contenttype_id = ct.id
WHERE cf.name = 'monitoring_template';

Читается прямолинейно: dcim + device → таблица dcim_device, virtualization + virtualmachinevirtualization_virtualmachine. Следующий шаг выполняется для каждой строки из этого вывода. Пропустишь одну — получишь половину объектов со строками вместо массивов, и вылезет это не сразу.

Шаг 4. Конвертировать строки в массивы

Сначала смотрим, что получится, ничего не меняя. Запрос намеренно повторяет условия будущего UPDATE — так видно ровно те строки, которые попадут под изменение:

SELECT id,
  custom_field_data->>'monitoring_template' AS old_value,
  to_jsonb(ARRAY[custom_field_data->>'monitoring_template']) AS new_value
FROM virtualization_virtualmachine
WHERE custom_field_data ? 'monitoring_template'
  AND custom_field_data->>'monitoring_template' IS NOT NULL
  AND jsonb_typeof(custom_field_data->'monitoring_template') != 'array'
LIMIT 5;

В new_value должно быть ["linux-servers"]. Убедился — применяем:

BEGIN;

UPDATE virtualization_virtualmachine
SET custom_field_data = jsonb_set(
      custom_field_data,
      '{monitoring_template}',
      to_jsonb(ARRAY[custom_field_data->>'monitoring_template'])
    )
WHERE custom_field_data ? 'monitoring_template'
  AND custom_field_data->>'monitoring_template' IS NOT NULL
  AND jsonb_typeof(custom_field_data->'monitoring_template') != 'array';

COMMIT;

Условие jsonb_typeof(...) != 'array' делает запрос идемпотентным: повторный прогон не завернёт массив в массив. Это важнее, чем кажется, когда таблиц пять и ты уже сбился со счёта, какие прошёл.

Проверка результата:

SELECT id, custom_field_data->'monitoring_template'
FROM virtualization_virtualmachine
WHERE custom_field_data ? 'monitoring_template'
LIMIT 5;

И перезапуск, чтобы NetBox перечитал определение поля:

sudo systemctl restart netbox netbox-rq

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

Чек-лист

Короткий вариант, чтобы не перечитывать всю статью:

  • python3 --version — 3.12 и новее
  • версия PostgreSQL — 15 и новее (или задача на обновление СУБД поставлена)
  • дамп базы и архив /opt/netbox сделаны
  • внешние пакеты прописаны в local_requirements.txt
  • release notes прочитаны в части «что убрали», а не «что добавили»
  • после апгрейда: миграции применены, /api/status/ отвечает, токен принимается
  • скрипты, cron и вебхуки проверены по логам, а не по отсутствию алертов
  • при смене типа поля: тип изменён, все таблицы из django_content_type обработаны, данные сконвертированы, сервис перезапущен

Мораль

Первое. В NetBox тип кастомного поля — не метаданные, а контракт на формат уже записанных данных. Поэтому интерфейс и не даёт его менять: он не знает, что делать с тем, что вы уже нащёлкали. Меняя тип руками, вы берёте миграцию данных на себя целиком.

Второе. BEGIN перед UPDATE в чужой схеме — не паранойя, а норма. Предпросмотр тем же условием, что и будущий UPDATE, стоит тридцати секунд и снимает половину риска.

Третье. Обновление NetBox ломает не веб-интерфейс, а то, что ходит в API. Читать release notes надо начиная с раздела «что удалено» — там живут все неприятные сюрпризы, и все они проявляются молча.

#netbox #postgresql #python #автоматизация

evgen@spb: ~/contacts
evgen@spbping -c1 evgen.info
64 bytes from evgen.info: отвечу в течение рабочего дня
evgen@spbcat contacts.txt
mail      ya@evgen.info
telegram  @EvgenOne
github    github.com/onegin
blog      evgen.info/blog
evgen@spbecho $ЗАЧЕМ_ПИСАТЬ
интересная инфраструктурная задача · вопрос по Proxmox или Icinga ·
желание обсудить, почему memtest молчал, а память была битая
evgen@spb
© 2026 Евгений Подолинский · Санкт-Петербург
синий здесь – не тот, о котором вы подумали.