NetBox: Custom Field обязателен только для Active
АйТи Фреш
Linux, Docker и DevOps

Как сделать Custom Field в NetBox обязательным только для активных устройств

Автор: , директор ООО «АйТи-Фреш» · · ~14 мин чтения
Карточка устройства NetBox с условным шлюзом обязательности: активные устройства требуют данные, планируемые — нет
Обязательность поля включается не навсегда, а по условию — как шлюз, который открывается только для активных устройств.

Штатная галочка Required у Custom Field в NetBox не умеет условий — поле либо обязательно всегда, либо не обязательно никогда. Для карточки планируемого оборудования, где часть данных появится только на монтаже, это неудобно. Разбираю, как через CustomValidator и настройку CUSTOM_VALIDATORS сделать поле обязательным только для устройств в статусе Active, не трогая штатную логику NetBox.

Симптом: планируемое оборудование не сохраняется без данных, которых ещё нет

Креативное агентство «Идея-Форма» (24 рабочих места) обратилось с типичной для растущей инфраструктуры проблемой: администратор завёл в NetBox custom field «Ответственный инженер» на модели Device — чтобы для каждой единицы оборудования было видно, кто отвечает за неё при инциденте. Поле пометили Required, потому что для рабочего оборудования отвечающий должен быть указан всегда. Но как только в базу начали заводить карточки планируемых закупок — устройства, которые ещё не приехали и тем более не закреплены ни за кем, — система начала требовать заполнить «Ответственного инженера» и для них, хотя это физически невозможно: сохранить черновик карточки будущего свитча без выдуманного значения не получалось. Мы регулярно донастраиваем внедрение NetBox клиентам именно на этом стыке — между удобством учёта и жёсткостью штатных проверок, — и обязательность по условию статуса встречается почти в каждом проекте.

Первая реакция администратора была снять Required совсем, но тогда терялся смысл поля — рабочее оборудование стало сохраняться без ответственного, и через месяц оказалось, что у трети активных устройств поле просто пустое, потому что никто не следил за его заполнением вручную. Нужен был третий вариант: обязательность, которая включается в зависимости от статуса устройства — Active требует значение, Planned и Staged — нет.

Сравнение штатного Required и условной валидации через CustomValidator в NetBox по сценариям статуса устройства
Штатный Required не различает статус устройства — CustomValidator добавляет именно это условие.

Почему штатный Required не решает эту задачу

Параметр Required у Custom Field в NetBox — это простой булев переключатель на уровне определения поля, а не условие. В официальной документации это описано однозначно: «Marking a field as required will force the user to provide a value for the field when creating a new object or when saving an existing object» — отметка поля как обязательного заставит пользователя указать значение при создании нового объекта или при сохранении существующего. Никакой привязки к другим полям объекта (статусу, роли, типу устройства) в самом определении Custom Field нет — требование либо действует всегда для всех объектов данной модели, либо не действует вовсе.

Это осознанное ограничение модели, а не недоработка: Custom Field — про то, какое поле вообще существует у объекта и какого оно типа, а не про бизнес-правила, при каких условиях оно нужно. Условная логика в NetBox вынесена в отдельный, специально предназначенный для этого механизм — Custom Validation, который выполняется поверх любых полей объекта, включая custom fields, и умеет обращаться к значениям сразу нескольких полей одновременно, в том числе штатных вроде status. Тот же принцип «правило зависит от контекста объекта, а не задаётся раз и навсегда» я закладываю и в других системах — например, когда строю матрицу доступа по ролям в 1С: там тоже нельзя выразить условие вроде «поле видно кладовщику, только если документ не проведён» одной галочкой в настройках роли, нужна отдельная логика поверх штатных прав.

CustomValidator: как устроен и как подключается

Класс CustomValidator живёт в extras.validators и переопределяется одним методом — validate(self, instance, request), куда NetBox передаёт сохраняемый объект (instance) и текущий HTTP-запрос (request). Внутри метода пишется обычная Python-логика с доступом ко всем полям объекта, а при нарушении условия вызывается self.fail(сообщение, field=...) — сообщение попадёт в форму как ошибка валидации, а необязательный параметр field привязывает её к конкретному полю, а не к объекту целиком. Минимальный пример именно под нашу задачу — обязательность custom field только для активных устройств:

Подключается валидатор через параметр CUSTOM_VALIDATORS в configuration.py — это словарь, где ключ — путь к модели в формате app_label.model (например dcim.device), а значение — список или кортеж валидаторов для неё, даже если валидатор всего один: CUSTOM_VALIDATORS = {'dcim.device': ('path.to.RequiredIfActiveValidator',)} — валидатор указывается либо строкой с точечным путём до класса (NetBox импортирует его сам, путь считается относительно рабочего каталога NetBox — при стандартной установке это /opt/netbox/netbox/), либо экземпляром класса, импортированного прямо в configuration.py (RequiredIfActiveValidator()). Править исходники NetBox для этого не нужно: достаточно, чтобы модуль с классом импортировался процессом NetBox.

Важный момент про порядок выполнения, который часто понимают неправильно: пользовательская валидация выполняется **после** того, как отработала встроенная валидация NetBox, и лишь дополняет её. Официальная документация прямо предупреждает: «These validators merely supplement NetBox's own validation: They will not override it» — они не отменяют штатные проверки. Если поле помечено обязательным через саму модель на уровне ядра NetBox (не через custom field, а как часть базовой схемы объекта), выставить для него {'prohibited': True} в CUSTOM_VALIDATORS и тем самым снять обязательность — не получится: штатная проверка сработает раньше и заблокирует сохранение до того, как дело дойдёт до кастомного валидатора.

На практике это означает, что CustomValidator стоит проектировать как дополнительный слой поверх штатных правил, а не как способ их обойти. Если задача — сделать поле не обязательным для части объектов, но обязательным для другой части, правильная последовательность такая: сначала на самом custom field выставляется Required = False (то есть штатно поле необязательно вообще ни для кого), а затем именно через CustomValidator добавляется условие, при котором отсутствие значения становится ошибкой. Обратный порядок — Required = True на поле и попытка «отключить» требование валидатором для части объектов — работать не будет, потому что штатная проверка отработает раньше и заблокирует сохранение независимо от того, что написано в кастомном классе.

Схема порядка валидации в NetBox: встроенная проверка выполняется раньше CustomValidator и не может быть отменена им
CustomValidator включается уже после того, как отработали штатные проверки NetBox, — и не может их отменить.

Как обратиться к custom field внутри валидатора

Значения custom fields объекта внутри validate() читаются через словарь instance.cf — обращение вида instance.cf["responsible_engineer"] (или .get(...), чтобы не словить KeyError, если поле ещё не заполнено вовсе). Это подтверждено в обсуждении сообщества (discussion #8950 в основном репозитории netbox-community/netbox, март 2022 года): пользователь спрашивал, как сослаться на custom field в правилах валидации, и получил ответ, что простые правила-словари этого не умеют, а в классе CustomValidator что синтаксис instance.cf["customfield"] работает для доступа к значению внутри кастомного валидатора.

Для привязки ошибки конкретно к custom field, а не к объекту в целом, в параметр field метода fail() передаётся не голое имя поля, а с префиксом cf_ — то есть field='cf_responsible_engineer', а не field='responsible_engineer'. Причина простая: в формах NetBox custom fields называются именно cf_<имя>, а fail(message, field=...) в исходниках — это просто raise ValidationError({field: message}). С префиксом cf_ ошибка подсветится у того поля, рядом с которым пользователь должен ввести значение; с голым именем форма не найдёт такого поля, и ошибка либо уйдёт не туда, либо превратится в невнятный сбой — проверять такой вариант на проде я не советую.

В том же обсуждении участники приводили более сложные примеры — например, проверку уникальности значения custom field среди уже существующих устройств через Device.objects.filter(custom_field_data__...), то есть CustomValidator не ограничен простыми условиями «если статус — то обязательно», а вполне подходит для произвольной бизнес-логики, которую нельзя выразить штатными валидаторами min/max/regex на самом определении custom field.

Несколько условий и несколько моделей одновременно

Для «Идея-Формы» одного условия оказалось мало: помимо «Ответственный инженер» обязателен для Active, у клиента ещё была своя нумерация инвентаря (custom field inventory_tag), которая должна быть заполнена для всех статусов, кроме Planned — то есть для Active, Staged, Offline и Decommissioning. Оба условия я собрал в одном классе CustomValidator, но не через два вызова self.fail(): в исходниках fail() сразу выбрасывает ValidationError, и второе условие после первого срабатывания уже не проверится. Чтобы пользователь увидел все нарушения за одно сохранение, я собираю ошибки в словарь вида {'cf_responsible_engineer': '…', 'cf_inventory_tag': '…'} и в конце, если он не пуст, выбрасываю один ValidationError(errors) из django.core.exceptions — Django разнесёт сообщения по полям формы.

Если правила для разных моделей независимы (например, у dcim.device — свои условия обязательности, у virtualization.virtualmachine — свои), их не обязательно смешивать в одном классе: CUSTOM_VALIDATORS принимает отдельный список валидаторов на каждый ключ-модель, и в один момент времени может быть подключено сколько угодно валидаторов на сколько угодно моделей одновременно — документация прямо формулирует правило как словарь {модель: [валидатор, ...]}, без ограничения на число моделей или число валидаторов на модель.

Значения по умолчанию у поля status модели Device стоит держать в голове при написании условий: базовый набор — active (устройство включено и доступно), planned (запланировано к вводу), staged (на месте, готово к переводу в active), offline (в стойке, но выключено или недоступно), failed (ожидает ремонта или замены), inventory (снято с площадки, но пригодно для повторного использования) и decommissioning (в процессе вывода из эксплуатации). Список расширяем через FIELD_CHOICES в конфигурации, если внутренние регламенты требуют дополнительных статусов, — и это стоит учитывать: если кто-то добавит собственный статус вроде reserved, а условие в валидаторе жёстко проверяет только == 'active', новый статус останется вне правила, и его придётся добавлять в валидатор вручную.

Ещё один практический момент, который легко упустить при первом написании такого правила: validate() вызывается NetBox и при создании объекта через веб-форму, и при массовом редактировании (bulk edit), и при импорте, и при изменении через REST API — то есть один класс валидатора закрывает сразу все точки входа, а не только форму создания устройства вручную. Это удобно, но требует аккуратности с сообщениями об ошибках: текст, который хорошо читается как всплывающая подсказка рядом с полем формы, при bulk-редактировании сотни объектов вернётся как часть общего отчёта об ошибках по каждому объекту — стоит формулировать сообщение так, чтобы оно было понятно и вне контекста конкретной карточки, например включать в текст название самого объекта или его идентификатор, если это уместно. Правило, которое молча ведёт себя иначе при массовой обработке, чем при ручном вводе одной карточки, — известная категория граблей: я разбирал похожий эффект, когда Zabbix не удаляет потерянные ресурсы при LLD — там правило тоже применяется не так, как ожидает администратор, если процесс идёт не через штатный ручной путь.

Чек-лист внедрения условной обязательности custom field в NetBox на кейсе агентства Идея-Форма, 24 рабочих места
Прежде чем включать правило на постоянной основе, семь существующих карточек пришлось донастроить вручную.

Кейс «Идея-Форма»: что получилось в итоге

Работа заняла один день. Первым делом мы описали в явном виде оба условия обязательности вместе с ответственным сотрудником клиента — «Ответственный инженер» обязателен строго для Active, «Инвентарный номер» обязателен для всех статусов кроме Planned — чтобы не гадать по ходу написания кода, а зафиксировать правило один раз. Дальше — написание класса RequiredIfActiveValidator в отдельном модуле /opt/netbox/netbox/custom_validators/validators.py (рабочий каталог NetBox, откуда разрешается точечный путь) и подключили в CUSTOM_VALIDATORS для dcim.device строкой 'custom_validators.validators.RequiredIfActiveValidator'. Модуль положили под git и добавили в список файлов, которые переносятся при обновлении NetBox, — иначе новая версия в соседнем каталоге молча запустится без правила.

Проверили на трёх сценариях: создание нового Planned-устройства без обоих полей — сохраняется штатно, никаких ошибок; перевод существующего устройства из Planned в Active без заполненного «Ответственного инженера» — форма блокируется с понятной ошибкой у нужного поля; попытка изменить статус на Offline у устройства без инвентарного номера — тоже блокируется. На 41 уже заведённом устройстве клиента при первом включении правила обнаружилось семь объектов в статусе Active без указанного ответственного — их пришлось донастроить руками до того, как включать проверку на постоянной основе, иначе администраторы получили бы лавину ошибок при любом следующем сохранении этих карточек.

Итог: правило работает молча для планируемого оборудования (не мешает вносить неполные данные заранее) и жёстко требует полноты данных для всего, что реально введено в эксплуатацию — то есть именно то разделение, которого штатный Required дать не мог. Отдельно я предупредил клиента: если через полгода появится новый custom field с похожей логикой, проще расширить существующий класс валидатора новым условием, чем плодить отдельные файлы под каждое правило — иначе разобраться, какое правило за что отвечает, станет так же сложно, как разбирать, почему custom controller в Strapi возвращает лишние приватные поля, когда логика санитизации данных расползлась по десятку разных мест вместо одного.

Частые вопросы

Чем условная обязательность через CustomValidator отличается от штатного Required у Custom Field?

Штатный Required действует всегда для всех объектов модели и не умеет проверять другие поля объекта. CustomValidator выполняется как отдельная проверка после встроенной валидации и может обращаться к любым полям объекта, включая status, — то есть требовать значение custom field только при выполнении условия, а не безусловно.

Можно ли через CustomValidator отменить штатное обязательное поле NetBox?

Нет. Документация прямо говорит, что кастомные валидаторы только дополняют встроенную валидацию NetBox и не могут её переопределить — параметр prohibited в CUSTOM_VALIDATORS не снимет обязательность, заданную на уровне ядра платформы.

Как обратиться к значению custom field внутри validate()?

Через словарь instance.cf, например instance.cf["responsible_engineer«] или instance.cf.get(»responsible_engineer"), чтобы избежать KeyError на объектах, где поле ещё не заполнено. Это подтверждено в GitHub Discussion #8950 сообщества netbox-community/netbox.

Как привязать ошибку валидации именно к custom field в интерфейсе?

В параметр field метода self.fail() нужно передать имя поля с префиксом cf_, например field='cf_responsible_engineer'. Без префикса ошибка отобразится как общая, не привязанная к конкретному полю формы.

Можно ли задать несколько условий обязательности для одной модели сразу?

Да, но с оговоркой: self.fail() сразу выбрасывает ValidationError, поэтому второй вызов после первого не выполнится. Чтобы показать все нарушения разом, соберите ошибки в словарь {'cf_поле': 'сообщение'} и выбросьте один ValidationError. CUSTOM_VALIDATORS принимает и несколько валидаторов на одну модель, и правила для разных моделей.

Нужно ли редактировать исходный код NetBox, чтобы подключить CustomValidator?

Нет. Класс подключается в CUSTOM_VALIDATORS файла configuration.py либо строкой с точечным путём (относительно рабочего каталога NetBox, обычно /opt/netbox/netbox/), либо экземпляром класса, импортированного в самом configuration.py. Исходники платформы не меняются, но модуль с валидатором нужно переносить при обновлении NetBox.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

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

📞 +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи