Почему Config Template в NetBox не видит кастомное поле площадки и как вывести его в конфигурацию
Если Config Template в NetBox рендерит конфигурацию устройства, а место кастомного поля площадки в ней пустое — дело почти всегда в синтаксисе Jinja2, а не в самих данных: значение реально хранится в базе, но обращение к нему через `custom_fields` возвращает не то, что вы думаете. Разбираю, как на самом деле устроен контекст рендеринга, чем `cf` отличается от `custom_fields` и `custom_field_data`, и как проверить конфигурацию одним запросом к API, не трогая продовую автоматизацию.
Симптом: конфигурация рендерится, а поле площадки в ней пустое
Типичная ситуация после того, как в компании внедрили NetBox как систему записи для сети и начали генерировать конфигурации через Config Templates: шаблон подключён к роли устройства, рендер запускается без ошибок, но в готовом конфиге на месте идентификатора площадки — либо пустая строка, либо буквально None. При этом в карточке площадки в веб-интерфейсе значение custom field видно прекрасно, оно заполнено, опечаток в названии нет. Инженер лезет в шаблон, видит там что-то вроде {{ device.site.custom_fields.site_id }}, всё выглядит логично — обратились к устройству, через него к площадке, через неё к кастомным полям, взяли нужное. И это как раз тот момент, где логика подводит.
Дело в том, что custom_fields на объекте NetBox — это не словарь значений, а атрибут, через который NetBox отдаёт связанные определения полей (то, что вы видите как список полей в разделе «Custom Fields» админки) для модели этого объекта. Значений там нет — есть структура. Обращение к .site_id на этой структуре либо падает с ошибкой в момент рендера (если шаблон строгий), либо тихо возвращает пусто, если Jinja2 в NetBox настроен прощать отсутствующие атрибуты — а он настроен именно так, поэтому симптом выглядит как «пропавшие данные», а не как ошибка синтаксиса.
Как устроен контекст рендеринга: device, virtualmachine и три способа добраться до custom field
Официальная документация по рендерингу конфигурации прямо описывает, что объект, для которого рендерится конфигурация, попадает в контекст шаблона под именем device — для физических устройств, и virtualmachine — для виртуальных машин. Дальше из этого объекта в Jinja2 доступны все связанные объекты как есть: device.site, device.device_type, device.platform, device.tenant и так далее — обычная навигация по связям, как в Python.
Для custom fields у любого объекта, который их поддерживает, NetBox даёт три разных пути, и только один из них — про значения в удобной форме. Первый — custom_field_data: это словарь сырых значений, как они физически хранятся в базе (для полей типа select там будет сохранённое значение, а не отображаемая метка). Второй — cf — обёртка над тем же самым словарём, которая отдаёт уже десериализованные значения в удобном виде: даты как объекты даты, множественный выбор как список и так далее. Третий путь — тот самый custom_fields, с которым чаще всего путают cf, но это отдельная сущность: связь с определениями полей модели, а не с их значениями на конкретном объекте.
Официальная документация по кастомным полям прямо рекомендует использовать именно cf: «для удобства объекты, которые поддерживают присвоение custom field, предоставляют доступ к данным custom field через свойство cf» — и это чище, чем работать напрямую с custom_field_data. Пример из документации предельно конкретный: кастомное поле с именем foo123 на модели Site доступно на экземпляре как {{ site.cf.foo123 }}. Обратите внимание — это slug поля (техническое имя без пробелов), а не отображаемое название («Label»), которое видно в интерфейсе; если в форме создания поля вы вводили удобочитаемое название, а slug сгенерировался автоматически с подчёркиваниями, обращаться в шаблоне нужно именно по slug.
Правильный путь до значения: обсуждение сообщества и официальный пример сходятся
Этот же вывод независимо подтверждается на практике. В обсуждении #14378 в репозитории NetBox на GitHub разбирается ровно эта ситуация — попытка достать custom field площадки через {{ device.site.custom_fields.site_id }} не даёт результата, а рабочим путём оказывается {{ device.site.cf.site_id }}. То есть цепочка навигации через связи (device → site) остаётся такой же, как и задумывалась, меняется только последнее звено — с custom_fields на cf.
На практике это значит, что при переносе custom field площадки в конфигурацию устройства правильная запись выглядит так: {{ device.site.cf.site_id }}. Если поле определено не на площадке, а прямо на устройстве — короче: {{ device.cf.site_id }}. Если название slug содержит символы, которые Jinja2 не примет как часть атрибута (например, поле было создано со slug через API и содержит нестандартные символы), можно использовать индексный синтаксис {{ device.site.cf['site_id'] }} — он равнозначен точечному обращению, но работает даже с именами, которые нельзя написать через точку.
Отдельно стоит проверить, что кастомное поле вообще назначено на нужный тип объекта. Custom field в NetBox создаётся с привязкой к одной или нескольким моделям (в разделе «Object types» при создании поля) — если поле «site_id» создано только для площадок, а шаблон пытается достать его же имя с устройства, cf на устройстве этого поля просто не будет содержать, и это снова будет выглядеть как «поле не видно», хотя на самом деле опечатки нет — не то место в объектной модели.
Как проверить рендер за один запрос, не трогая продовую автоматизацию
Самая частая ошибка при отладке — сразу лезть в связанную систему автоматизации (Ansible, скрипт разворачивания), которая дёргает конфигурацию из NetBox и применяет её на устройстве. Это удлиняет цикл проверки: правка шаблона — прогон плейбука — ожидание — снова правка. Быстрее проверить рендер напрямую через REST API NetBox, ничего не применяя.
У каждого устройства есть эндпоинт рендеринга конфигурации, и он вызывается отдельным запросом:
curl -X POST \
-H "Authorization: Bearer $NETBOX_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/plain" \
https://netbox.example.com/api/dcim/devices/123/render-config/Здесь 123 — ID устройства. Заголовок Authorization: Bearer — формат для токенов v2 (вида nbt_<ключ>.<токен>), которые появились в NetBox 4.5; старые токены v1 с заголовком Authorization: Token в 4.6–4.7 ещё принимаются, но объявлены устаревшими и уйдут в 5.0, так что новые скрипты я сразу пишу под Bearer. Для вызова нужно право render_config на устройства — его ввели в 4.5, и без него запрос вернёт отказ, даже если у токена есть право на чтение устройств (суперпользователя это ограничение не касается). Заголовок Accept управляет форматом ответа: text/plain вернёт сам текст конфигурации как есть, application/json — обёртку с полем content, удобную для встраивания в другой инструмент. По умолчанию используется Config Template, назначенный устройству (через сам device, либо унаследованный от роли/платформы — подробнее в следующем разделе); без дополнительных параметров эндпоинт рендерит именно эту, штатную цепочку разрешения шаблона. Если нужно прогнать против контекста устройства другой шаблон — например, черновик исправленной версии, — в актуальных релизах в тело запроса можно добавить config_template_id с ID нужного Config Template: назначения на устройстве и роли при этом не меняются, а для такой подмены пользователю дополнительно нужно право view на Config Templates.
В тело POST-запроса можно передать и произвольные дополнительные ключи — они попадают в контекст шаблона как переменные вместе с обычными данными объекта и его config context. Это удобно для отладки: если подозреваете, что дело не в самом custom field, а в чём-то ещё, можно временно передать заведомо корректное значение через тело запроса и посмотреть, появляется ли оно в выводе — так отделяется проблема доступа к custom field от проблемы в остальной части шаблона.
Порядок наследования Config Template: от роли и платформы к самому устройству
Ещё один источник путаницы — какой именно шаблон реально рендерится, если их несколько. Config Template для устройства назначается на трёх уровнях: на самом устройстве, на его роли и на платформе (у типа устройства такого поля нет — частое заблуждение). Порядок разрешения документирован жёстко: сначала шаблон самого устройства, затем шаблон роли, затем шаблон платформы — берётся первый найденный. Если шаблон не назначен ни на одном из трёх объектов, запрос на рендер завершится ошибкой, а не пустым конфигом.
Отсюда практическая ошибка: инженер правит и тестирует один шаблон, а устройство на самом деле рендерит другой — потому что на конкретном устройстве десять месяцев назад кто-то один раз явно назначил Config Template вручную, и с тех пор изменения общего шаблона роли до этого устройства не доходят. Прежде чем разбираться, почему «шаблон не видит custom field», стоит открыть карточку конкретного устройства и явно посмотреть, какой шаблон у него назначен — через API это то же самое поле config_template, которое видно в ответе на GET /api/dcim/devices/{id}/.
Кейс «РемонтГрад»: пустой ID площадки во всех конфигурациях после переноса на новый шаблон
У клиента — сеть ремонтных мастерских «РемонтГрад», 27 рабочих мест, три площадки со своим сетевым оборудованием и общей учётной системой NetBox для DCIM/IPAM. Полгода назад мы переносили конфигурации коммутаторов доступа с ручных шаблонов в текстовом редакторе на Config Templates внутри NetBox — идея была в том, чтобы при вводе нового коммутатора конфигурация генерировалась автоматически, а не копипастилась из прошлого с правкой руками, где легко забыть один параметр.
После переноса рендер стал давать пустое значение там, где должен был подставляться внутренний код площадки — кастомное поле site_code, которое используется в конфигурации как часть hostname коммутатора и SNMP location. Само поле было заполнено на каждой из трёх площадок правильно, никаких опечаток. В шаблоне, унаследованном ещё от черновика, который писали в спешке, обращение выглядело как {{ device.site.custom_fields.site_code }} и встречалось трижды — в hostname, в SNMP location и в баннере входа. Работающий на вид синтаксис на деле обращался не к тому атрибуту.
Правка заняла пару минут — заменили путь на {{ device.site.cf.site_code }} и перепроверили рендер через render-config эндпоинт по всем трём площадкам до того, как выкатывать шаблон на реальные коммутаторы. Заодно проверили конфигурации остальных устройств на предмет того же паттерна .custom_fields. — нашли ещё два места в другом шаблоне (для точек доступа), где та же ошибка тихо оставляла пустым код площадки в имени SSID-профиля и в комментарии конфигурации. Итог: три площадки, пять исправленных мест в двух шаблонах (три плюс два), полный цикл проверки через API — около сорока минут вместо того, чтобы находить проблему по одной на живом коммутаторе методом проб и ошибок.
Чек-лист: если Config Template не видит custom field
Порядок, который я прохожу при разборе такой жалобы. Первое — открыть сам шаблон и найти точное обращение к полю: если там custom_fields.<name> — это почти наверняка причина, меняем на cf.<slug>. Второе — свериться со slug поля в разделе «Custom Fields» админки, а не с его отображаемым названием: Jinja2 обращается именно по техническому имени. Третье — проверить, на какой тип объекта (модель) назначено поле: если оно создано для площадок, а шаблон читает его с устройства (или наоборот), cf на неправильном объекте будет просто пустым, без ошибки.
Четвёртое — убедиться, какой именно Config Template реально назначен конкретному устройству: явное назначение на устройстве перебивает унаследованное от роли и платформы, и правка «общего» шаблона может не долетать до части парка. Пятое — проверять итог не на реальном оборудовании, а запросом к POST /api/dcim/devices/{id}/render-config/ с заголовком Accept: text/plain — это тот же самый рендер, который использует автоматизация, но без риска применить недоделанный конфиг. Такую же дисциплину — сначала проверка в изолированном контуре, потом выкладка — я держу и для регламента обновления конфигураций 1С: разница только в инструменте, а не в логике «сначала сухой прогон».
И последнее — если после исправления синтаксиса поле всё ещё пустое, значит дело не в Jinja2, а в данных: проверьте через API (GET /api/dcim/sites/{id}/ или .../devices/{id}/) поле custom_fields в самом JSON-ответе объекта — там, в отличие от шаблона, это как раз словарь текущих значений, и если там пусто — заполнение не сохранилось на самом объекте, а это уже вопрос не к шаблону, а к тому, где и как поле заполнялось. Одна оговорка для NetBox 4.7: значения полей типа selection и multiple selection в REST API теперь приходят объектом вида {"value": ..., "label": ...}, а не голой строкой. Внешние скрипты, которые читали такие поля из API, после обновления стоит перепроверить — а вот cf внутри Jinja2-шаблона работает с Python-объектами и этой смены формата ответа API не касается.
Частые вопросы
Почему `{{ device.site.custom_fields.site_id }}` не выводит значение поля?
Потому что `custom_fields` на объекте NetBox — это связь с определениями полей (структура: тип, обязательность, варианты выбора), а не со значениями. Для значений в шаблоне нужно свойство `cf`, документация NetBox описывает его именно для этого: `{{ site.cf.foo123 }}` для поля с именем `foo123`.
В чём разница между `cf` и `custom_field_data`?
`custom_field_data` — сырой словарь, как значения физически хранятся в базе. `cf` — обёртка над тем же словарём, которая отдаёт значения уже десериализованными (например, даты как объекты даты). NetBox docs называют `cf` более удобным вариантом именно для шаблонов и рекомендуют его.
Как проверить, что рендерит Config Template, не применяя конфигурацию на устройстве?
Запросом `POST /api/dcim/devices/{id}/render-config/` к REST API NetBox с заголовком `Accept: text/plain` (или `application/json`); токену нужно право `render_config`, а для проверки другого шаблона в тело можно передать `config_template_id`. Это тот же самый рендер, который использует автоматизация, но результат просто возвращается в ответе, без применения на оборудовании.
Что делать, если поле создано, заполнено, но с правильным `cf` всё равно пусто?
Проверить, на какой тип объекта назначено custom field при создании. Если поле привязано только к моделям Site, а шаблон читает его с Device (или наоборот), `cf` на неподходящем объекте не будет содержать это поле — это не баг, а вопрос того, где именно поле определено.
Какой Config Template реально используется, если он назначен и на устройстве, и на его роли?
Шаблон, явно назначенный на самом устройстве, имеет приоритет; дальше по порядку идут роль и платформа. Если шаблона нет ни на одном из трёх уровней, рендер вернёт ошибку. Если правка общего шаблона роли не долетает до конкретного устройства — сначала проверьте поле `config_template` в карточке или ответе API именно этого устройства.
Можно ли обратиться к полю по индексу, а не через точку — `cf['site_id']`?
Да, это равнозначная запись, и она нужна, если slug поля содержит символы, которые Jinja2 не примет как часть атрибута после точки. Для обычных slug из букв, цифр и подчёркиваний оба варианта работают одинаково.
Источники
- NetBox Labs Docs — Configuration Rendering — Проверено: контекст `device`/`virtualmachine`; `POST /api/dcim/devices/{id}/render-config/`; порядок выбора шаблона device → role → platform, без шаблона — ошибка; `Accept: application/json|text/plain`; `config_template_id` в теле для подмены шаблона; право `render_config`; заголовок `Authorization: Bearer`. https://netboxlabs.com/docs/netbox/features/configuration-rendering/
- NetBox Labs Docs — Custom Fields — Проверено: объекты, поддерживающие custom fields, предоставляют доступ к значениям через свойство `cf` — «cleaner than accessing custom field data through the actual field (custom_field_data)»; официальный пример `{{ site.cf.foo123 }}` для поля с именем foo123 на модели Site. https://netboxlabs.com/docs/netbox/customization/custom-fields/
- GitHub netbox-community/netbox — Discussion #14378 — Проверено: реальный кейс сообщества с тем же симптомом (custom field площадки не подставляется в Config Template через `device.site.custom_fields.site_id`); рабочим путём подтверждён `{{ device.site.cf.site_id }}`. https://github.com/netbox-community/netbox/discussions/14378
- GitHub netbox-community/netbox — Release notes v4.5–v4.7 — Проверено: v4.7.1 от 15.09.2026 — актуальный релиз; в 4.5 введены токены v2 (Bearer) и право render_config, v1-токены устаревшие до 5.0; в 4.7 select-поля в REST API отдаются объектом value/label, `custom_fields` у CustomFieldsMixin возвращает список. https://github.com/netbox-community/netbox/tree/main/docs/release-notes
- NetBox Labs Docs — Devices — Проверено: у устройства есть поле `config_template`, которое имеет приоритет над шаблонами роли и платформы. https://netboxlabs.com/docs/netbox/models/dcim/device/


