This document serves as a handbook for maintainers of plugins that were written prior to the release of NetBox v4.0. It serves to capture all the changes recommended to ensure a plugin is compatible with NetBox v4.0 and later releases.
NetBox v4.0 drops support for Python 3.8 and 3.9, and introduces support for Python 3.12. You may need to update your CI/CD processes and/or packaging to reflect this.
All plugin Python resources were moved from extras.plugins
to netbox.plugins
in NetBox v3.7 (see #14036), and support for importing these resources from their old locations has been removed.
from extras.plugins import PluginConfig
from netbox.plugins import PluginConfig
NetBox's proxy model for Django's ContentType model has been renamed to ObjectType for clarity. In general, plugins should use the ObjectType proxy when referencing content types, as it includes several custom manager methods. The one exception to this is when defining generic foreign keys: The ForeignKey field used for a GFK should point to Django's native ContentType.
Additionally, plugin maintainers are strongly encouraged to adopt the "object type" terminology for field and filter names wherever feasible to be consistent with NetBox core (however this is not required for compatibility).
content_types = models.ManyToManyField(
to='contenttypes.ContentType',
related_name='event_rules'
)
object_types = models.ManyToManyField(
to='core.ObjectType',
related_name='event_rules'
)
The format for declaring view actions & permissions was updated in NetBox v3.7 (see #13550), and NetBox v4.0 drops support for the old format. Views which inherit ActionsMixin
must declare a single actions
map.
actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete')
action_perms = defaultdict(set, **{
'add': {'add'},
'import': {'add'},
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
})
actions = {
'add': {'add'},
'import': {'add'},
'export': set(),
'bulk_edit': {'change'},
'bulk_delete': {'delete'},
}
The BootstrapMixin
class is no longer available or needed and can be removed from all forms.
from django import forms
from utilities.forms import BootstrapMixin
class MyForm(BootstrapMixin, forms.Form):
from django import forms
class MyForm(forms.Form):
NetBox v4.0 introduces several new classes for advanced form rendering, including FieldSet. Fieldset definitions on forms should use this new class instead of a tuple or list.
Notably, the name of a fieldset is now optional, and passed as a keyword argument rather than as the first item in the set.
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm
class CircuitForm(NetBoxModelForm):
...
fieldsets = (
(_('Circuit'), ('cid', 'type', 'status', 'description', 'tags')),
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelForm
from utilities.forms.rendering import FieldSet
class CircuitForm(NetBoxModelForm):
...
fieldsets = (
FieldSet('cid', 'type', 'status', 'description', 'tags', name=_('Circuit')),
FieldSet('install_date', 'termination_date', 'commit_rate', name=_('Service Parameters')),
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
)
NetBox no longer applies color to buttons within navigation menu items. Although this functionality is still supported, you might want to remove color from any buttons to ensure consistency with the updated design.
PluginMenuButton(
link='myplugin:foo_add',
title='Add a new Foo',
icon_class='mdi mdi-plus-thick',
color=ButtonColorChoices.GREEN
)
PluginMenuButton(
link='myplugin:foo_add',
title='Add a new Foo',
icon_class='mdi mdi-plus-thick'
)
The following template blocks have been renamed or removed:
Template | Old name | New name |
---|---|---|
generic/object.html | header |
page-header |
generic/object.html | controls |
control-buttons |
base/layout.html | content-wrapper |
Removed (use content ) |
Ditch any legacy "float" controls (e.g. float-end
) in favor of Bootstrap's new flex behaviors for controlling the layout and sizing of elements horizontally. For example, the following will align two items against the left and right sides of the parent element:
<div class="d-flex justify-content-between">
<h3>Title text</h3>
<i class="mdi mdi-close"></i>
</div>
When using offset columns (e.g. class="col-offset-3"
), be sure to also set the column width (e.g. class="col-9 col-offset-3"
) to avoid horizontal scrolling.
Tables inside cards should be embedded directly, not nested inside a card-body
element.
<div class="card">
<div class="card-body">
<table class="table table-hover attr-table">
...
</table>
</div>
</div>
<div class="card">
<table class="table table-hover attr-table">
...
</table>
</div>
The btn-sm
(small) class is no longer typically needed on general-purpose buttons.
<a href="#" class="btn btn-sm btn-primary">Text</a>
<a href="#" class="btn btn-primary">Text</a>
Foreground (text) color is no longer automatically adjusted by bg-$color
classes. To ensure sufficient contrast with the background color, use the text-bg-$color
form of the class instead, or set the text color separately with text-$color
.
<span class="badge bg-primary">Text</span>
<span class="badge text-bg-primary">Text</span>
The following custom CSS classes have been removed:
object-subtitle
(usetext-secondary
instead)
NetBox now uses a single API serializer for both normal and "brief" modes (i.e. GET /api/dcim/sites/?brief=true
); nested serializer classes are no longer required. Two changes to API serializers are necessary to support brief mode:
- Define
brief_fields
under itsMeta
class. These are the fields which will be included when brief mode is used. - For any nested objects, switch to using the primary serializer and pass
nested=True
.
Any nested serializers which are no longer needed can be removed.
class SiteSerializer(NetBoxModelSerializer):
region = NestedRegionSerializer(required=False, allow_null=True)
class Meta:
model = Site
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
class SiteSerializer(NetBoxModelSerializer):
region = RegionSerializer(nested=True, required=False, allow_null=True)
class Meta:
model = Site
fields = ('id', 'url', 'display', 'name', 'slug', 'status', 'region', 'time_zone', ...)
brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
NetBox now includes the description
field in "brief" mode for all models which have one. This is not required for plugins, but you may opt to do the same for consistency.
NetBox has replaced Graphene-Django with Strawberry which requires any GraphQL code to be updated.
Strawberry uses Python typing and generally only requires a small refactoring of the schema definition to update:
import graphene
from netbox.graphql.fields import ObjectField, ObjectListField
from utilities.graphql_optimizer import gql_query_optimizer
class CircuitsQuery(graphene.ObjectType):
circuit = ObjectField(CircuitType)
circuit_list = ObjectListField(CircuitType)
def resolve_circuit_list(root, info, **kwargs):
return gql_query_optimizer(models.Circuit.objects.all(), info)
import strawberry
import strawberry_django
@strawberry.type
class CircuitsQuery:
@strawberry.field
def circuit(self, id: int) -> CircuitType:
return models.Circuit.objects.get(pk=id)
circuit_list: list[CircuitType] = strawberry_django.field()
Type conversion is also fairly straight-forward, but Strawberry requires FK and M2M references to be explicitly defined to pick up the right typing.
- The
class Meta
options need to be moved up to the Strawberry decorator - Add
@strawberry_django.field
definitions for any FK and M2M references in the model
import graphene
class CircuitType(NetBoxObjectType, ContactsMixin):
class Meta:
model = models.Circuit
fields = '__all__'
filterset_class = filtersets.CircuitFilterSet
from typing import Annotated
import strawberry
import strawberry_django
@strawberry_django.type(
models.CircuitType,
fields='__all__',
filters=CircuitTypeFilter
)
class CircuitTypeType(OrganizationalObjectType):
color: str
@strawberry_django.field
def circuits(self) -> list[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]]:
return self.circuits.all()
Strawberry currently doesn't directly support django-filter, so an explicit filters.py file will need to be created. NetBox includes a new autotype_decorator
used to automatically wrap FilterSets to reduce the required code to a minimum.
import strawberry
import strawberry_django
from circuits import filtersets, models
from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin
__all__ = (
'CircuitFilter',
)
@strawberry_django.filter(models.Circuit, lookups=True)
@autotype_decorator(filtersets.CircuitFilterSet)
class CircuitFilter(BaseFilterMixin):
pass