AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified Apache-2.0 Self-run

Netbox Plugin Development

skill-netboxlabs-skills-netbox-plugin-development · by netboxlabs

>

No reviews yet
0 installs
21 views
0.0% view→install

Install

$ agentstack add skill-netboxlabs-skills-netbox-plugin-development

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-netboxlabs-skills-netbox-plugin-development)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Netbox Plugin Development? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

NetBox Plugin Development

> Your knowledge of NetBox plugin APIs may be outdated. Base classes, mixins, template tags, and registration patterns change between NetBox releases. Prefer retrieval over pre-trained knowledge.

Retrieval Sources

| Source | URL / Method | Use for | |--------|-------------|---------| | Plugin development docs | https://netboxlabs.com/docs/netbox/plugins/development/ | Plugin framework reference | | NetBox repo | https://github.com/netbox-community/netbox | Base classes, mixins, current APIs | | NetBox release notes | https://netboxlabs.com/docs/netbox/release-notes/ | Breaking changes, new plugin features | | Example plugins | https://github.com/netbox-community | Community plugin patterns |

Build plugins that extend NetBox with custom models, views, APIs, and UI elements. Target: NetBox 4.5–4.6 / Python 3.12–3.14. Note the Django split: NetBox 4.5 runs on Django 5.2, NetBox 4.6 on Django 6.0 — code, migrations, and third-party deps must be Django 6.0-compatible when targeting 4.6+.

> For REST API client patterns (pagination, filtering, tokens), see > [netbox-api-integration](../netbox-api-integration/SKILL.md).


Quick Reference — Key Imports

# 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 django_filters import FilterSet  # for @register_filterset

# 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. Plugin Structure & Setup

Package Layout

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)
└── 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 = 'you@example.com'
    base_url = 'myplugin'               # URL prefix under /plugins/
    min_version = '4.5.0'
    max_version = '4.6.99'              # span 4.5–4.6; 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'

Auto-discovery: NetBox automatically discovers modules at navigation.menu, navigation.menu_items, template_content.template_extensions, search.indexes, graphql.schema, and more. No manual registration needed for these.


2. Models

Subclass NetBoxModel for full feature support (tags, custom fields, change logging, bookmarks, journaling, export templates, event rules, notifications).

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 (description + comments) and OrganizationalModel > (name/slug/description/comments, enforces unique name+slug) are now part of the > plugins API. Use them when your model fits those patterns.

Key gotchas:

  • Use %(app_label)s_ prefix on related_name to avoid FK collisions across plugins
  • Reference core models as strings ('dcim.Device') in ForeignKey fields
  • Always define get_absolute_url() — follows plugins:: pattern
  • Permissions format: netbox_myplugin.view_accesslist, .add_accesslist, etc.

See [references/model-patterns.md](references/model-patterns.md) for base class hierarchy, mixins, choices patterns, and migration tips.


3. Views

Use the @register_model_view decorator for standard CRUD views:

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 Configuration (urls.py)

from netbox.views.generic import get_model_urls
from . import models

urlpatterns = get_model_urls('netbox_myplugin', models)

Extending Core Model Views with Tabs

@register_model_view(Device, 'access_lists', path='access-lists')
class DeviceAccessListsView(ObjectChildrenView):
    queryset = Device.objects.all()
    child_model = AccessList
    table = AccessListTable
    tab = ViewTab(label='Access Lists', badge=lambda obj: obj.accesslists.count())

See [references/views-and-api.md](references/views-and-api.md) for all view classes and URL patterns.


4. REST API

Serializer

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

Router (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 naming: plugins-api:netbox_myplugin-api:accesslist-detail (this matters for HyperlinkedIdentityField on serializers).

See [references/views-and-api.md](references/views-and-api.md) for GraphQL, nested serializers, and advanced API patterns.


5. Forms, Tables & FilterSets

Model Form

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')

Table

from netbox.tables import NetBoxTable, columns

class AccessListTable(NetBoxTable):
    name = columns.LinkColumn()
    device = columns.LinkColumn()
    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+: The @register_filterset decorator is required for lookup modifiers > (e.g., name__ic, device_id__n) to appear in the UI filter forms.

See [references/forms-tables-filtersets.md](references/forms-tables-filtersets.md) for bulk edit forms, import forms, filter form widgets, and all field types.


6. Navigation

# 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. Template Extensions

Two approaches — use ViewTab (modern) for tab-based extensions, or PluginTemplateExtension for injecting content into existing pages:

# 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]

Available methods: head(), navbar(), list_buttons(), buttons(), alerts(), left_page(), right_page(), full_width_page().


8. Testing

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)

See [references/testing-guide.md](references/testing-guide.md) for fixtures, view tests, form tests, and CI setup.


9. Packaging

Use pyproject.toml (modern) or setup.py:

[project]
name = "netbox-myplugin"
version = "1.0.0"
dependencies = ["netbox>=4.5.0, **Note:** The entry point key must match `PluginConfig.name` and point to the
> module-level `config` variable.

See [references/packaging.md](references/packaging.md) for versioning strategy,
publishing to PyPI, and version compatibility matrix.

---

## 10. Common Gotchas

1. **`config` must be module-level** in `__init__.py` — not inside a function
2. **FK `related_name` collisions** — prefix with `%(app_label)s_` when multiple plugins target the same core model
3. **URL namespace** — views: `plugins::`, API: `plugins-api:-api:-detail`
4. **Missing `search()` on filterset** — `?q=` won't work without it
5. **`models` on TemplateExtension** — must be list of `'app.model'` strings, not classes; `None` = global
6. **Heavy imports in `__init__.py`** — use `ready()` for signals and deferred imports (fixed in 4.5.2 but still best practice)
7. **`max_version` too strict** — use `'4.6.99'` (or your top minor's `.99`) not `'4.5.0'` to allow patches
8. **GraphQL uses Strawberry** since 4.0 — Graphene patterns will not work
9. **`@register_filterset`** (4.5+) — without it, lookup modifiers won't appear in UI
10. **Permissions** — format is `.view_`, `.add_`, etc. **Custom actions (4.6.0+):** declare extra actions via your model's `Meta.permissions`; NetBox auto-registers them as actions selectable in the ObjectPermission form (preferred over ad-hoc permission checks).

---

## Version Notes

### NetBox 4.6 (2026)
- **Django 6.0** (was 5.2 in 4.5) — ensure code, migrations, and dependencies are Django 6.0-compatible; set `min_version='4.6.0'` for any plugin using 4.6-only APIs below
- **Declarative view layouts / reusable UI components** — `from netbox.ui import layout` (`Layout`/`Row`/`Column`); the modern alternative to hand-written detail templates (requires NetBox ≥4.6.0)
- **Custom model actions / permissions via `Meta.permissions`** — declare them on your model's `Meta`; NetBox auto-registers them as actions selectable in the ObjectPermission form
- **Custom serializer resolvers** for `get_serializer_for_model()` (4.6.2) — register a resolver to control serializer lookup
- **Security:** ExportTemplate/ConfigTemplate `environment_params` RCE (CVE-2026-29514) fixed in 4.6.1 — the allowlist blocks `extensions`/`finalize`/`loader`/`bytecode_cache`
- **Deprecations to avoid:** `DEFAULT_ACTION_PERMISSIONS`, legacy view actions, the internal registry `models` key, the custom `querystring` template tag, and `OptionalLimitOffsetPagination`

### NetBox 4.5 (2026-01-06)
- **Python 3.12+ required** (dropped 3.10/3.11); Django 5.2
- `Primar

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [netboxlabs](https://github.com/netboxlabs)
- **Source:** [netboxlabs/skills](https://github.com/netboxlabs/skills)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.