The REST API enables powerful integration with other systems which exchange data with NetBox. It is powered by the Django REST Framework (DRF), which is not a component of Django itself. In this tutorial, we'll see how we can extend NetBox's REST API to serve our plugin.
🟦 Note: If you skipped the previous step, run git checkout step08-filter-sets
.
Our API code will live in the api/
directory under netbox_access_lists/
. Let's go ahead and create that as well as an __init__.py
file now:
$ cd netbox_access_lists/
$ mkdir api
$ touch api/__init__.py
Serializers are somewhat analogous to forms: They control the translation of client data to and from Python objects, while Django itself handles the database abstraction. We need to create a serializer for each of our models. Begin by creating serializers.py
in the api/
directory.
$ edit api/serializers.py
At the top of this file, we need to import the serializers
module from the rest_framework
library, as well as NetBox's NetBoxModelSerializer
class and our plugin's own models:
from rest_framework import serializers
from netbox.api.serializers import NetBoxModelSerializer
from ..models import AccessList, AccessListRule
First, we'll create a serializer for AccessList
, subclassing NetBoxModelSerializer
. Much like when creating a model form, we'll create a child Meta
class under the serializer specifying the associated model
and the fields
to be included.
class AccessListSerializer(NetBoxModelSerializer):
class Meta:
model = AccessList
fields = (
'id', 'display', 'name', 'default_action', 'comments', 'tags', 'custom_fields', 'created',
'last_updated',
)
It's worth discussing each of the fields we've named above. id
is the model's primary key; it should always be included with every serializer, as it provides a guaranteed method of uniquely identifying objects. The display
field is built into NetBoxModelSerializer
: It is a read-only field which returns a string representation of the object. This is useful for populating form field dropdowns, for instance.
The name
, default_action
, and comments
fields are declared on the AccessList
model. tags
provides access to the object's tag manager, and custom_fields
includes its custom field data; both of these are provided by NetBoxModelSerializer
. Finally, the created
and last_updated
are read-only fields built into NetBoxModel
.
Our serializer will inspect the model to generate the necessary fields automatically, however there's one field that we need to add manually. Every serializer should include a read-only url
field which contains the URL where the object can be reached; think of it as similar to a model's get_absolute_url()
method. To add this, we'll use DRF's HyperlinkedIdentityField
. Add it above the Meta
child class:
class AccessListSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
)
When invoking the field class, we need to specify the appropriate view name. Note that this view doesn't actually exist yet; we'll create it a bit later.
Remember back in step three when we added a table column showing the number of rules assigned to each access list? That was handy. Let's add a serializer field for it too! Add this directly below the url
field:
rule_count = serializers.IntegerField(read_only=True)
Just as with the table column, we'll rely on our view (to be defined next) to annotate the rule count for each access list on the underlying queryset.
Finally, we need to add both url
and rule_count
to Meta.fields
:
class Meta:
model = AccessList
fields = (
'id', 'url', 'display', 'name', 'default_action', 'comments', 'tags', 'custom_fields', 'created',
'last_updated', 'rule_count',
)
🟢 Tip: The order in which fields are listed determines the order in which they appear in the object's API representation.
We also need to create a serializer for AccessListRule
. Add it to serializers.py
below AccessListSerializer
. As with the first serializer, we'll add a Meta
class to define the model and fields, and a url
field.
class AccessListRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
)
class Meta:
model = AccessListRule
fields = (
'id', 'url', 'display', 'access_list', 'index', 'protocol', 'source_prefix', 'source_ports',
'destination_prefix', 'destination_ports', 'action', 'tags', 'custom_fields', 'created',
'last_updated',
)
There's an additional consideration when referencing related objects in a serializer. By default, the serializer will return only the primary key of the related object; its numeric ID. This requires the client to make additional API requests in order to determine any other information about the related object. It is convenient to include on the serializer some information about the related object, such as its name and URL, automatically. We can do this by using a nested serializer.
For instance, the source_prefix
and destination_prefix
fields both reference NetBox's core ipam.Prefix
model. We can extend AccessListRuleSerializer
to use NetBox's nested serializer for this model:
from ipam.api.serializers import NestedPrefixSerializer
# ...
class AccessListRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail')
source_prefix = NestedPrefixSerializer()
destination_prefix = NestedPrefixSerializer()
Now, our serializer will include an abridged representation of the source and/or destination prefixes for the object. We should do this with the access_list
field as well, however we'll first need to create a nested serializer for the AccessList
model.
Begin by importing NetBox's WritableNestedSerializer
class. This will serve as the base class for our nested serializers.
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
Then, create two nested serializer classes, one for each of our plugin's models. Each of these will have a url
field and Meta
child class like the regular serializers, however the Meta.fields
attribute for each is limited to a bare minimum of fields: id
, url
, display
, and a supplementary human-friendly identifier. Add these in serializers.py
above the regular serializers (because we need to define NestedAccessListSerializer
before we can reference it).
class NestedAccessListSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslist-detail'
)
class Meta:
model = AccessList
fields = ('id', 'url', 'display', 'name')
class NestedAccessListRuleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
)
class Meta:
model = AccessListRule
fields = ('id', 'url', 'display', 'index')
Now we can override the access_list
field on AccessListRuleSerializer
to use the nested serializer:
class AccessListRuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name='plugins-api:netbox_access_lists-api:accesslistrule-detail'
)
access_list = NestedAccessListSerializer()
source_prefix = NestedPrefixSerializer()
destination_prefix = NestedPrefixSerializer()
Next, we need to create views to handle the API logic. Just as serializers are roughly analogous to forms, API views work similarly to the UI views that we created in step five. However, because API functionality is highly standardized, view creation is substantially simpler: We generally need only to create a single view set for each model. A view set is a single class that can handle the view, add, change, and delete operations which each require dedicated views under the UI.
Start by creating api/views.py
and importing NetBox's NetBoxModelViewSet
class, as well as our plugin's models
and filtersets
modules, and our serializers.
from netbox.api.viewsets import NetBoxModelViewSet
from .. import filtersets, models
from .serializers import AccessListSerializer, AccessListRuleSerializer
First we'll create a view set for access lists, by inheriting from NetBoxModelViewSet
and defining its queryset
and serializer_class
attributes. (Note that we're prefetching assigned tags for the queryset.)
class AccessListViewSet(NetBoxModelViewSet):
queryset = models.AccessList.objects.prefetch_related('tags')
serializer_class = AccessListSerializer
Recall that we added a rule_count
field to AccessListSerializer
; let's annotate the queryset appropriately to ensure that field gets populated (just as we did for the table column in step five). Remember to import Django's Count
utility class.
from django.db.models import Count
# ...
class AccessListViewSet(NetBoxModelViewSet):
queryset = models.AccessList.objects.prefetch_related('tags').annotate(
rule_count=Count('rules')
)
serializer_class = AccessListSerializer
Next, we'll add a view set for rules. In addition to queryset
and serializer_class
, we'll attach the filter set for this model as filterset_class
. Note that we're also prefetching all related object fields in addition to tags to improve performance when listing many objects.
class AccessListRuleViewSet(NetBoxModelViewSet):
queryset = models.AccessListRule.objects.prefetch_related(
'access_list', 'source_prefix', 'destination_prefix', 'tags'
)
serializer_class = AccessListRuleSerializer
filterset_class = filtersets.AccessListRuleFilterSet
Finally, we'll create our API endpoint URLs. This works a bit differently from UI views: Instead of defining a series of paths, we instantiate a router and register each view set to it.
Create api/urls.py
and import NetBox's NetBoxRouter
and our API views:
from netbox.api.routers import NetBoxRouter
from . import views
Next, we'll define an app_name
. This will be used to resolve API view names for our plugin.
app_name = 'netbox_access_list'
Then, we create a NetBoxRouter
instance and register each view with it using our desired URL. These are the endpoints that will be available under /api/plugins/access-lists/
.
router = NetBoxRouter()
router.register('access-lists', views.AccessListViewSet)
router.register('access-list-rules', views.AccessListRuleViewSet)
Finally, we expose the router's urls
attribute as urlpatterns
so that it will be detected by the plugins framework.
urlpatterns = router.urls
🟢 Tip: The base URL for our plugin's REST API endpoints is determined by the base_url
attribute of the plugin config class that we created in step one.
With all of our REST API components now in place, we should be able to make API requests. (Note that you may first need to provision a token for authentication.) You can quickly verify that our endpoints are working properly by navigating to http://localhost:8000/api/plugins/access-lists/ in your browser while logged into NetBox. You should see the two available endpoints; clicking on either will return a list of objects.
🟦 Note: If the REST API endpoints do not load, try restarting the development server (manage.py runserver
).