mcpField
N
netbox-plugin-development

netbox-plugin-development

github.com/netboxlabs开发2147 次安装2 周前更新
代码仓库

关于 netbox-plugin-development

为 NetBox 插件开发提供参考资料与实践指南,面向构建自定义模型、视图、API 和界面的开发者。内容覆盖 NetBox 4.5–4.7,并强调按版本核对不断变化的 API。

主要功能

  1. 01插件结构与注册配置
  2. 02模型、视图与 REST API 模式
  3. 03表单、表格与筛选器指南
  4. 04导航与模板扩展方法
  5. 05测试和打包建议

适用场景

  1. 01为 NetBox 插件搭建初始结构
  2. 02按 NetBox 版本更新插件实现
  3. 03为插件添加自定义 API 与界面

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/netboxlabs/skills --skill netbox-plugin-development

安装后在 Claude Code 中输入/netbox-plugin-development确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
forms-tables-filtersets.md
model-patterns.md
netbox-4.7-migration.md
packaging.md
testing-guide.md
views-and-api.md

详细介绍

NetBox 插件开发

你对 NetBox 插件 API 的了解可能已经过时。 基类、混入类、模板标签和注册模式会随 NetBox 版本变化。请优先检索,而非依赖预训练知识。

检索来源

来源URL / 方法用途
插件开发文档https://netboxlabs.com/docs/netbox/plugins/development/插件框架参考
NetBox 仓库https://github.com/netbox-community/netbox基类、混入类、当前 API
NetBox 发行说明https://netboxlabs.com/docs/netbox/release-notes/破坏性变更、新插件功能
示例插件https://github.com/netbox-community社区插件模式

构建插件,为 NetBox 扩展自定义模型、视图、API 和 UI 元素。 目标版本:NetBox 4.5–4.7 / Python 3.12–3.14。请注意 Django 版本的变化:NetBox 4.5 运行于 Django 5.2,NetBox 4.6 运行于 Django 6.0,NetBox 4.7 运行于 Django 6.1——面向 4.6+ 时,代码、迁移和第三方依赖必须兼容 Django 6.0;面向 4.7+ 时,则必须兼容 Django 6.1。

REST API 客户端模式(分页、筛选、令牌)请参见 netbox-api-integration。


快速参考 — 关键导入

# Models
from netbox.models import NetBoxModel          # base for all plugin models
from netbox.models import PrimaryModel         # adds description + comments (4.5+)
from netbox.models import OrganizationalModel  # name/slug/description/comments (4.5+)

# Views
from netbox.views.generic import (ObjectListView, ObjectView, ObjectEditView,
    ObjectDeleteView, BulkImportView, BulkEditView, BulkDeleteView)
from utilities.views import register_model_view, ViewTab

# Forms
from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelImportForm

# Tables
from netbox.tables import NetBoxTable, columns

# FilterSets
from netbox.filtersets import NetBoxModelFilterSet
from utilities.filters import register_filterset  # 4.5+: UI lookup modifiers

# REST API
from netbox.api.serializers import NetBoxModelSerializer
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.routers import NetBoxRouter

# Navigation
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton

1. 插件结构与设置

包布局
netbox_myplugin/
├── __init__.py              # PluginConfig + config variable
├── version.py               # __version__ = '1.0.0'
├── models.py                # Django models (NetBoxModel subclasses)
├── views.py                 # UI views
├── tables.py                # django-tables2 table classes
├── forms.py                 # Model, bulk edit, filter, import forms
├── filtersets.py            # FilterSet classes
├── navigation.py            # Menu items (auto-discovered)
├── search.py                # Search indexes (auto-discovered)
├── graphql/                 # Strawberry GraphQL schema
│   ├── schema.py            # Query class (auto-discovered)
│   ├── types.py
│   └── filters.py
├── api/
│   ├── urls.py              # NetBoxRouter config
│   ├── views.py             # API ViewSets
│   └── serializers.py
├── templates/netbox_myplugin/
│   └── myplugin_model.html  # Detail view templates
├── template_content.py      # Template extensions (auto-discovered)
├── event_rules.py           # EventRuleAction subclasses (4.7+, auto-discovered)
├── jinja_env.py             # Config-template Jinja filters (4.7+, auto-discovered)
├── graphql_extensions.py    # Fields/filters on core GraphQL types (4.7+, auto-discovered)
└── tests/
    ├── test_models.py
    ├── test_views.py
    ├── test_api.py
    └── test_forms.py
PluginConfig(__init__.py)
from netbox.plugins import PluginConfig
from .version import __version__

class MyPluginConfig(PluginConfig):
    name = 'netbox_myplugin'
    verbose_name = 'My Plugin'
    description = 'Adds custom functionality to NetBox'
    version = __version__
    author = 'Your Name'
    author_email = '[email protected]'
    base_url = 'myplugin'               # URL prefix under /plugins/
    min_version = '4.5.0'
    max_version = '4.7.99'              # span 4.5–4.7; use .99 to allow patch releases
    default_settings = {'feature_x': True}
    required_settings = []

    def ready(self):
        super().ready()
        from . import signals  # deferred imports to avoid circular deps

config = MyPluginConfig  # MUST be module-level variable named 'config'

自动发现: NetBox 会自动发现 navigation.menu、navigation.menu_items、 template_content.template_extensions、search.indexes、graphql.schema,以及 (4.7+) event_rules.event_rule_actions、jinja_env.filters、graphql_extensions.type_extensions/.filter_extensions。


2. 模型

继承 NetBoxModel 以获得完整功能支持(标签、自定义字段、变更日志、 书签、日志记录、导出模板、事件规则、通知)。

from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel

class AccessList(NetBoxModel):
    name = models.CharField(max_length=100)
    device = models.ForeignKey(
        to='dcim.Device',
        on_delete=models.CASCADE,
        related_name='%(app_label)s_access_lists',  # avoid FK collisions
    )
    type = models.CharField(max_length=50, choices=AccessListTypeChoices)

    class Meta:
        ordering = ['name']
        unique_together = ('device', 'name')

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('plugins:netbox_myplugin:accesslist', args=[self.pk])

4.5+:PrimaryModel(描述 + 评论)和 OrganizationalModel (名称/slug/描述/评论,并强制名称和 slug 唯一)现已纳入插件 API。模型符合这些模式时,请使用它们。

注意事项:

  • 在 related_name 上使用 %(app_label)s_ 前缀,以避免插件之间的 FK 冲突
  • 在 ForeignKey 字段中以字符串形式引用核心模型('dcim.Device')
  • 始终定义 get_absolute_url()——遵循 plugins:<plugin_name>:<model_name> 模式
  • 权限格式:netbox_myplugin.view_accesslist、.add_accesslist 等

有关基类层次结构、混入类、选项模式和迁移提示,请参见 references/model-patterns.md。


3. 视图

使用 @register_model_view 装饰器创建标准 CRUD 视图:

from netbox.views.generic import (
    ObjectListView, ObjectView, ObjectEditView, ObjectDeleteView,
    BulkImportView, BulkEditView, BulkDeleteView
)
from utilities.views import register_model_view, ViewTab
from .models import AccessList
from .tables import AccessListTable
from .forms import AccessListForm, AccessListFilterForm, AccessListBulkEditForm
from .filtersets import AccessListFilterSet

@register_model_view(AccessList, 'list')
class AccessListListView(ObjectListView):
    queryset = AccessList.objects.all()
    table = AccessListTable
    filterset = AccessListFilterSet
    filterset_form = AccessListFilterForm

@register_model_view(AccessList)
class AccessListView(ObjectView):
    queryset = AccessList.objects.all()

@register_model_view(AccessList, 'edit')
class AccessListEditView(ObjectEditView):
    queryset = AccessList.objects.all()
    form = AccessListForm

@register_model_view(AccessList, 'delete')
class AccessListDeleteView(ObjectDeleteView):
    queryset = AccessList.objects.all()
URL 配置(urls.py)
from netbox.views.generic import get_model_urls
from . import models

urlpatterns = get_model_urls('netbox_myplugin', models)

有关所有视图类、URL 模式、ViewTab(向核心对象视图添加标签页)、声明式布局 + 面包屑导航(4.7)、GraphQL 核心类型扩展(4.7)和自定义事件规则操作(4.7),请参见 references/views-and-api.md。


4. REST API

序列化器
from netbox.api.serializers import NetBoxModelSerializer
from ..models import AccessList

class AccessListSerializer(NetBoxModelSerializer):
    class Meta:
        model = AccessList
        fields = ('id', 'url', 'display', 'name', 'device', 'type',
                  'tags', 'custom_fields', 'created', 'last_updated')
        brief_fields = ('id', 'url', 'display', 'name')  # for nested representation
ViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from ..models import AccessList
from ..filtersets import AccessListFilterSet
from .serializers import AccessListSerializer

class AccessListViewSet(NetBoxModelViewSet):
    queryset = AccessList.objects.all()
    serializer_class = AccessListSerializer
    filterset_class = AccessListFilterSet
路由器(api/urls.py)
from netbox.api.routers import NetBoxRouter
from . import views

router = NetBoxRouter()
router.register('access-lists', views.AccessListViewSet)
urlpatterns = router.urls

API URL 命名: plugins-api:netbox_myplugin-api:accesslist-detail (这会影响序列化器中的 HyperlinkedIdentityField)。

有关 GraphQL、嵌套序列化器和高级 API 模式,请参见 references/views-and-api.md。


5. 表单、表格与 FilterSet

模型表单
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from dcim.models import Device
from .models import AccessList

class AccessListForm(NetBoxModelForm):
    device = DynamicModelChoiceField(queryset=Device.objects.all())

    fieldsets = (
        FieldSet('name', 'device', 'type'),
        FieldSet('tags', name='Tags'),
    )

    class Meta:
        model = AccessList
        fields = ('name', 'device', 'type', 'tags')
表格
import django_tables2 as tables
from netbox.tables import NetBoxTable, columns

class AccessListTable(NetBoxTable):
    name = tables.Column(linkify=True)    # no LinkColumn in netbox.tables.columns
    device = tables.Column(linkify=True)
    type = columns.ChoiceFieldColumn()
    tags = columns.TagColumn()

    class Meta(NetBoxTable.Meta):
        model = AccessList
        fields = ('pk', 'name', 'device', 'type', 'tags')
        default_columns = ('name', 'device', 'type')
FilterSet
from netbox.filtersets import NetBoxModelFilterSet
from utilities.filters import register_filterset
from .models import AccessList

@register_filterset  # 4.5+: enables lookup modifiers in UI
class AccessListFilterSet(NetBoxModelFilterSet):
    class Meta:
        model = AccessList
        fields = ('name', 'device', 'type')

    def search(self, queryset, name, value):
        """Required for ?q= parameter to work."""
        return queryset.filter(name__icontains=value)

4.5+:要让查找修饰符(例如 name__ic、device_id__n)显示在 UI 筛选表单中,必须使用 @register_filterset 装饰器。

有关批量编辑/导入/筛选表单、GenericObjectChoiceField(4.7)和 django-tables2 v3 注意事项(4.7),请参见 references/forms-tables-filtersets.md。


6. 导航

# navigation.py — auto-discovered
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton

menu = PluginMenu(
    label='My Plugin',
    icon_class='mdi mdi-shield-lock',
    groups=(
        ('Access Control', (
            PluginMenuItem(
                link='plugins:netbox_myplugin:accesslist_list',
                link_text='Access Lists',
                permissions=['netbox_myplugin.view_accesslist'],
                buttons=(
                    PluginMenuButton(
                        link='plugins:netbox_myplugin:accesslist_add',
                        title='Add',
                        icon_class='mdi mdi-plus-thick',
                        permissions=['netbox_myplugin.add_accesslist'],
                    ),
                ),
            ),
        )),
    ),
)

7. 模板扩展

有两种方式——对于基于标签页的扩展,使用 ViewTab(现代方式);对于向现有页面注入内容,使用 PluginTemplateExtension:

# template_content.py
from netbox.plugins import PluginTemplateExtension

class DeviceAccessInfo(PluginTemplateExtension):
    models = ['dcim.device']  # list of strings, not model classes; None = global

    def right_page(self):
        return self.render('netbox_myplugin/inc/device_access.html', extra_context={
            'access_lists': self.context['object'].accesslists.all()
        })

template_extensions = [DeviceAccessInfo]

可用的方法:head()、navbar()、list_buttons()、buttons()、alerts()、 left_page()、right_page()、full_width_page()。


8. 测试

from django.test import TestCase
from utilities.testing import APITestCase
from .models import AccessList

class AccessListAPITest(APITestCase):
    model = AccessList

    def setUp(self):
        self.device = Device.objects.create(name='test-device', ...)
        self.access_list = AccessList.objects.create(
            name='test-acl', device=self.device, type='standard'
        )

    def test_list(self):
        url = reverse('plugins-api:netbox_myplugin-api:accesslist-list')
        response = self.client.get(url, **self.header)
        self.assertEqual(response.status_code, 200)

请参阅 references/testing-guide.md,了解夹具、视图测试、 表单测试和 CI 设置。


9. 打包

使用 pyproject.toml(现代方式)或 setup.py:

[project]
name = "netbox-myplugin"
version = "1.0.0"
dependencies = []   # never list "netbox" here — see note below

[project.entry-points."netbox.plugins"]
netbox_myplugin = "netbox_myplugin:config"

**注意:**入口点键必须与 PluginConfig.name 匹配,并指向模块级的 config 变量。切勿将 netbox 列入 dependencies:NetBox 自 4.7 起已发布到 PyPI,因此 pip 会在真实的 NetBox 旁边,将第二个 NetBox 安装到虚拟环境中——应改为在 PluginConfig 上通过 min_version/max_version 声明兼容性。

请参阅 references/packaging.md,了解版本管理策略、 发布到 PyPI 以及版本兼容性矩阵。


10. 常见问题

  1. config 必须定义在模块级别,位于 __init__.py 中,不能放在函数内部
  2. 外键 related_name 冲突——多个插件针对同一个核心模型时,使用 %(app_label)s_ 作为前缀
  3. URL 命名空间——视图:plugins:<plugin>:<view>,API:plugins-api:<plugin>-api:<basename>-detail
  4. Filterset 缺少 search()——没有它,?q= 将无法使用
  5. TemplateExtension 上的 models——必须是 'app.model' 字符串列表,不能是类;None 表示全局
  6. 在 __init__.py 中进行重量级导入——信号和延迟导入请使用 ready()(已在 4.5.2 中修复,但仍是最佳实践)
  7. max_version 过于严格——使用 '4.7.99'(或你的最高次版本号加 .99),而不是 '4.5.0',以允许补丁版本
  8. 自 4.0 起,GraphQL 使用 Strawberry——Graphene 模式将无法使用
  9. @register_filterset(4.5+)——没有它,查找修饰符不会显示在 UI 中
  10. 权限——格式为 <plugin_name>.view_<model>、<plugin_name>.add_<model> 等。**自定义操作(4.6.0+):**通过模型的 Meta.permissions 声明额外操作;NetBox 会自动将它们注册为 ObjectPermission 表单中可选的操作(优先于临时权限检查)。

版本说明

NetBox 4.7(2026-09-02)
  • PostgreSQL 15+(在 14 上系统检查会失败,包括运行 manage.py test 时)和 Django 6.1;NetBox 已发布到 PyPI(不要在 pyproject.toml 中依赖 netbox);建议使用 ≥4.7.3——4.7.0/4.7.1 记录了通过后台批量请求创建的明文 API 令牌,并将其写入作业结果;4.7.3 增加了分支安全的后台写入和配置上下文省略优化
  • 已移除: DEFAULT_ACTION_PERMISSIONS、旧版视图操作、registry['models'](改用 ObjectType.objects.public())和 registry['denormalized_fields']、OptionalLimitOffsetPagination(→ NetBoxPagination)、ExpandableIPAddressField/expand_ipaddress_pattern()(→ *IPNetwork*)、populate_custom_field_defaults()、OwnerMixin 的反向关联(site_set)、Django EMAIL_* 设置,以及 NetBox 的 querystring 标签(改用 Django 的标签,且不传入 request:{% querystring page=1 %})——完整清单见 netbox-4.7-migration
  • 已更改: custom_fields/get_for_model() 返回列表;django-tables2 v3(使用 querystring_replace,不再有 RelatedLinkColumn);筛选集测试混入类更名为 *TestMixin;搜索索引推迟到提交时执行(测试中使用 captureOnCommitCallbacks(execute=True));插件视图集继承 ?background=true 和逐对象批量错误处理
  • 核心树结构从 MPTT 迁移到 ltree(Region、Location、Platform 等):无法筛选或排序 level;get_root()/get_family()/is_leaf_node()/move_to()/insert_at() 均已移除。NestedGroupModel 已弃用 → NestedLtreeGroupModel;在 4.7.0 上安装了 ltree 触发器的插件需要添加 ReinstallLtreeTriggers 迁移——参见 model-patterns
  • 新的插件扩展点(均需设置 min_version='4.7.0'):核心 GraphQL 类型/筛选器扩展(graphql_extensions.py)、自定义事件规则操作(EventRuleAction)、Jinja 筛选器和配置模板上下文(jinja_env.py、get_jinja_context())、布局中的面包屑、GenericObjectChoiceField、选项描述
NetBox 4.6(2026)
  • Django 6.0(4.5 使用的是 5.2)——确保代码、迁移和依赖项兼容 Django 6.0;使用以下任何仅限 4.6 的 API 时,为插件设置 min_version='4.6.0'
  • 声明式视图布局 / 可复用 UI 组件——from netbox.ui import layout(Layout/Row/Column);这是手写详情模板的现代替代方案(需要 NetBox ≥4.6.0)
  • 通过 Meta.permissions 定义自定义模型操作 / 权限——在模型的 Meta 中声明;NetBox 会自动将其注册为可在 ObjectPermission 表单中选择的操作
  • get_serializer_for_model() 的自定义序列化器解析器(4.6.2)——注册解析器以控制序列化器查找
  • **安全性:**4.6.1 修复了 ExportTemplate/ConfigTemplate environment_params 的 RCE 漏洞(CVE-2026-29514)——允许列表会阻止 extensions/finalize/loader/bytecode_cache
  • 应避免使用的弃用项: DEFAULT_ACTION_PERMISSIONS、旧版视图操作、内部注册表的 models 键、自定义 querystring 模板标签,以及 OptionalLimitOffsetPagination
NetBox 4.5(2026-01-06)
  • 需要 Python 3.12+(不再支持 3.10/3.11);Django 5.2
  • PrimaryModel、OrganizationalModel、NestedGroupModel 现已加入插件 API
  • @register_filterset 装饰器可启用 UI 查找修饰符
  • OwnerMixin 可用于对象所有权
  • **重大变更:**GraphQL 筛选语法要求 ID/枚举使用查找修饰符
NetBox 4.4(2025-09-02)
  • 后台作业日志记录:在作业方法中使用 self.logger.info()
  • ObjectAction 用于视图中的自定义单项 / 批量操作
  • register_model_feature() 用于插件注册自定义模型特性
  • 表格支持 ArrayColumn

评论

支持 Markdown