Как сделать Custom Field в NetBox обязательным только для активных устройств
Штатная галочка Required у Custom Field в NetBox не умеет условий — поле либо обязательно всегда, либо не обязательно никогда. Для карточки планируемого оборудования, где часть данных появится только на монтаже, это неудобно. Разбираю, как через CustomValidator и настройку CUSTOM_VALIDATORS сделать поле обязательным только для устройств в статусе Active, не трогая штатную логику NetBox.
Симптом: планируемое оборудование не сохраняется без данных, которых ещё нет
Креативное агентство «Идея-Форма» (24 рабочих места) обратилось с типичной для растущей инфраструктуры проблемой: администратор завёл в NetBox custom field «Ответственный инженер» на модели Device — чтобы для каждой единицы оборудования было видно, кто отвечает за неё при инциденте. Поле пометили Required, потому что для рабочего оборудования отвечающий должен быть указан всегда. Но как только в базу начали заводить карточки планируемых закупок — устройства, которые ещё не приехали и тем более не закреплены ни за кем, — система начала требовать заполнить «Ответственного инженера» и для них, хотя это физически невозможно: сохранить черновик карточки будущего свитча без выдуманного значения не получалось. Мы регулярно донастраиваем внедрение NetBox клиентам именно на этом стыке — между удобством учёта и жёсткостью штатных проверок, — и обязательность по условию статуса встречается почти в каждом проекте.
Первая реакция администратора была снять Required совсем, но тогда терялся смысл поля — рабочее оборудование стало сохраняться без ответственного, и через месяц оказалось, что у трети активных устройств поле просто пустое, потому что никто не следил за его заполнением вручную. Нужен был третий вариант: обязательность, которая включается в зависимости от статуса устройства — Active требует значение, Planned и Staged — нет.
Почему штатный 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 на поле и попытка «отключить» требование валидатором для части объектов — работать не будет, потому что штатная проверка отработает раньше и заблокирует сохранение независимо от того, что написано в кастомном классе.
- from extras.validators import CustomValidator class RequiredIfActiveValidator(CustomValidator): def validate(self, instance, request): if instance.status == 'active' and not instance.cf.get('responsible_engineer'): self.fail( «Для устройства в статусе Active укажите ответственного инженера.», field='cf_responsible_engineer' )
Как обратиться к 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 — там правило тоже применяется не так, как ожидает администратор, если процесс идёт не через штатный ручной путь.
Кейс «Идея-Форма»: что получилось в итоге
Работа заняла один день. Первым делом мы описали в явном виде оба условия обязательности вместе с ответственным сотрудником клиента — «Ответственный инженер» обязателен строго для 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.
Источники
- NetBox Labs Docs — Custom Validation — validate(self, instance, request) и fail(); выполнение _после_ встроенной валидации; «These validators merely supplement NetBox's own validation: They will not override it» и пример с prohibited; CUSTOM_VALIDATORS: точечный путь относительно рабочего каталога NetBox или экземпляр класса, «Even if defining only a single validator, it must be passed as an iterable»: https://netboxlabs.com/docs/netbox/customization/custom-validation/
- GitHub Discussion #8950, netbox-community/netbox — Подтверждённый синтаксис instance.cf["customfield"] для доступа к custom field внутри валидатора, привязка ошибки через field='cf_<name>', примеры проверки уникальности и формата значений: https://github.com/netbox-community/netbox/discussions/8950
- NetBox Labs Docs — Custom Fields — «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» — Required безусловен: https://netboxlabs.com/docs/netbox/customization/custom-fields/
- GitHub — netbox-community/netbox, docs/models/dcim/device.md — Поле status модели Device и указание, что дополнительные статусы задаются через FIELD_CHOICES в конфигурации: https://github.com/netbox-community/netbox/blob/main/docs/models/dcim/device.md
- NetBox Labs — Modeling OT Infrastructure in NetBox — Практический контекст использования custom fields (compliance status, policy assignments, end-of-life dates) и подтверждение поддержки validation rules у custom fields: https://netboxlabs.com/blog/modeling-ot-infrastructure-in-netbox-device-types-custom-fields-and-handling-duplicate-ips/
- GitHub — netbox-community/netbox, extras/validators.py и extras/signals.py — fail() сразу выбрасывает ValidationError({field: message}); валидаторы запускаются по сигналу post_clean до save(): https://github.com/netbox-community/netbox/blob/main/netbox/extras/validators.py



