Blog Detail
This is part 5 of the tutorial series on writing Nautobot plugins. Nautobot plugins are a way to extend the base functionality of Nautobot. Plugins can extend the database schema, add custom pages, and even update existing pages within Nautobot; the possibilities are nearly endless. In this blog series, we are developing a plugin for modeling and managing DNS zone data within Nautobot. In the previous posts, we covered setting up a development environment (part 1) and creating models (part 2), views (part 3), and filters (part 4).
In this post, we will create REST API endpoints for our plugin models, and we’ll also integrate them with GraphQL.
Source code for this part of the tutorial can be found here.
Adding REST API endpoints
To implement REST API for models used by our plugin, we need to extend the file structure of our plugin by adding api
directory under our plugin files.
$ tree api
.
├── __init__.py
├── nested_serializers.py
├── serializers.py
├── urls.py
└── views.py
We start from the bottom by defining serializers and nested serializers in their respective files, and then at the top we will chain them all together in urls
and views
.
Serializers
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML, or other content types.
We need one API endpoint for each of our models. Some of our models are related, and for related models we will use nested serializers to populate related model fields (attributes). Let’s get started with DnsZoneModelSerializer
.
# api/serializers.py
from rest_framework import serializers
from nautobot.extras.api.serializers import NautobotModelSerializer
from ..models import DnsZoneModel, ARecordModel, CNameRecordModel
class DnsZoneModelSerializer(NautobotModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_example_dns_manager-api:dnszonemodel-detail"
)
class Meta:
model = DnsZoneModel
fields = "__all__"
Our serializer class inherits from NautobotModelSerializer
. Under Meta
class, we define model and fields that we want to include in our API endpoint. You may use fields
attribute to limit model fields, but in this tutorial we expose them all. We add a new attribute, url
, which is a hyperlink to the record that points to a detailed view of the model.
Next, we define a serializer for ARecordModel
.
# api/serializers.py
from rest_framework import serializers
from nautobot.extras.api.serializers import NautobotModelSerializer
from nautobot.ipam.api.nested_serializers import NestedIPAddressSerializer
from ..models import DnsZoneModel, ARecordModel, CNameRecordModel
from .nested_serializers import NestedDnsZoneModelSerializer
class ARecordModelSerializer(NautobotModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_example_dns_manager-api:arecordmodel-detail"
)
zone = NestedDnsZoneModelSerializer()
address = NestedIPAddressSerializer()
class Meta:
model = ARecordModel
fields = "__all__"
Meta
class is almost identical to the DnsZoneModelSerializer
. There is also a url
attribute, and fields for the related models, DnsZoneModel
and IPAddress
. We want to include the data fields of related records in our API endpoint, so we need nested serializers that will serialize related models. If we don’t create nested serializers for zone
and address
fields, it will display only related object UUID
. NestedIPAddressSerializer
is already defined by Nautobot, so we can import it and attach to address
attribute. NestedDnsZoneModelSerializer
we need to implement ourselves.
# api/nested_serializers.py
from rest_framework import serializers
from nautobot.core.api import WritableNestedSerializer
from ..models import DnsZoneModel
class NestedDnsZoneModelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_example_dns_manager-api:dnszonemodel-detail"
)
class Meta:
"""Meta attributes."""
model = DnsZoneModel
fields = "__all__"
Implementation is almost exactly the same as regular serializers, but nested serializers inherit from a different parent class. Now we can attach our nested serializer for DnsZoneModel
to the zone attribute in the model serializer class ARecordModelSerializer
.
And the last one is CNameRecordModelSerializer
# api/serializers.py
class CNameRecordModelSerializer(NautobotModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="plugins-api:nautobot_example_dns_manager-api:cnamerecordmodel-detail"
)
zone = NestedDnsZoneModelSerializer()
class Meta:
model = CNameRecordModel
fields = "__all__"
It is also related to DnsZoneModel
, so we use the previously defined nested serializer for the zone attribute.
We are done with serializers, let’s move to views
and urls
.
Views and URLs
We already covered views and URLs in this series in (part 2). REST API endpoints use the same concept. The only difference is that we import the parent classes from their respective api
modules.
# api/views.py
from nautobot.extras.api.views import NautobotModelViewSet
from nautobot_example_dns_manager.models import DnsZoneModel, ARecordModel, CNameRecordModel
from .. import filters
from . import serializers
class DnsZoneModelViewSet(NautobotModelViewSet):
queryset = DnsZoneModel.objects.all()
serializer_class = serializers.DnsZoneModelSerializer
filterset_class = filters.DnsZoneModelFilterSet
class ARecordModelViewSet(NautobotModelViewSet):
queryset = ARecordModel.objects.prefetch_related("zone", "address")
serializer_class = serializers.ARecordModelSerializer
filterset_class = filters.ARecordModelFilterSet
class CNameRecordModelViewSet(NautobotModelViewSet):
queryset = CNameRecordModel.objects.prefetch_related("zone")
serializer_class = serializers.CNameRecordModelSerializer
filterset_class = filters.CNameRecordModelFilterSet
We have three models, and we defined three serializers: one for each model; we also need three views. Each view has three attributes: queryset
to fetch records from the database; serializer_class
defined in previous steps above; and filterset_class
, which was defined and covered in this series in (part 4). We may use these filtersets on our API endpoints to allow the same filtering as shown in part 4.
For DnsZoneModelViewSet
there is basic query to fetch all records. But for ARecordModelViewSet
and CNameRecordModelViewSet
, which have related models, we use .prefetch_related(<related fields>)
to include data attributes of the related models.
The last piece is URLs for REST API endpoints.
# api/urls.py
from nautobot.core.api import OrderedDefaultRouter
from . import views
router = OrderedDefaultRouter()
router.register("dnszonemodel", views.DnsZoneModelViewSet)
router.register("arecordmodel", views.ARecordModelViewSet)
router.register("cnamerecordmodel", views.CNameRecordModelViewSet)
urlpatterns = router.urls
We define three URLs (endpoints), and we bind them to our views implemented above. Now we are ready to see how our newly created REST API endpoints work. Let’s have a look at API documentation interface available under api/docs
on your development Nautobot instance.
In plugins section, we may see our newly added endpoints. They allow programmatic CRUD (Create, Read, Update, and Delete) operations.
Let’s take a look at the arecordmodel
endpoint by fetching it with curl (GET).
curl -X 'GET' \
'http://localhost:8080/api/plugins/example-dns-manager/arecordmodel/' \
-H 'accept: application/json'
In response, we get:
{
"count": 1,
"next": null,
"previous": null,
"results": [
{
"id": "f59dc014-1d3c-4ecc-b0a4-faa505e3061d",
"display": "app.example.com",
"custom_fields": {},
"notes_url": "http://localhost:8080/api/plugins/example-dns-manager/arecordmodel/f59dc014-1d3c-4ecc-b0a4-faa505e3061d/notes/",
"url": "http://localhost:8080/api/plugins/example-dns-manager/arecordmodel/f59dc014-1d3c-4ecc-b0a4-faa505e3061d/",
"zone": {
"id": "2ceb7857-f5fe-412a-8e6a-5cbcfcfd48af",
"display": "example.com",
"url": "http://localhost:8080/api/plugins/example-dns-manager/dnszonemodel/2ceb7857-f5fe-412a-8e6a-5cbcfcfd48af/",
"created": "2022-10-03",
"last_updated": "2022-10-03T11:08:24.884393Z",
"_custom_field_data": {},
"name": "example.com",
"slug": "example-com",
"mname": "dns1.example.com",
"rname": "admin@example.com",
"refresh": 86400,
"retry": 7200,
"expire": 3600000,
"ttl": 3600
},
"address": {
"display": "1.1.1.1/32",
"id": "7fedc46e-f79d-4671-b4e9-fd149f322250",
"url": "http://localhost:8080/api/ipam/ip-addresses/7fedc46e-f79d-4671-b4e9-fd149f322250/",
"family": 4,
"address": "1.1.1.1/32"
},
"created": "2022-10-03",
"last_updated": "2022-10-03T11:10:12.792646Z",
"_custom_field_data": {},
"slug": "app-example-com",
"name": "app.example.com",
"ttl": 14400
}
]
}
We see all our data fields (attributes) serialized to JSON. We also have related fields nicely serialized and nested in the data structure.
GraphQL Integration
Plugins can optionally expose their models via the GraphQL API. This allows models and their relationships to be represented as a graph and queried more easily. There are two mutually exclusive ways to expose a model to the GraphQL interface.
- By using the
@extras_features
decorator - By creating your own GraphQL type definition and registering it within graphql/types.py of your plugin (the decorator should not be used in this case)
The first option is quick and easy, as you just need to decorate our model classes in models.py
with @extras_features("graphql")
. But if you want to be able to use filtersets on GQL queries, you need option 2 (where you can attach filtersets to our plugin models).
We extend our plugin files with types.py
under graphql
directory.
$ tree graphql
├── graphql
│ └── types.py
In types.py
, we create GQL type classes for each plugin model. Nautobot uses a library called graphene-django-optimizer to decrease the time queries take to process. By inheriting from graphene_django_optimizer, type classes are automatically optimized.
# graphql/types.py
import graphene_django_optimizer as gql_optimizer
from .. import models, filters
class DnsZoneModelType(gql_optimizer.OptimizedDjangoObjectType):
"""GraphQL Type for DnsZoneModel."""
class Meta:
model = models.DnsZoneModel
filterset_class = filters.DnsZoneModelFilterSet
class ARecordModelType(gql_optimizer.OptimizedDjangoObjectType):
"""GraphQL Type for ARecordModel."""
class Meta:
model = models.ARecordModel
filterset_class = filters.ARecordModelFilterSet
class CNameRecordModelType(gql_optimizer.OptimizedDjangoObjectType):
"""GraphQL Type for CNameRecordModel."""
class Meta:
model = models.CNameRecordModel
filterset_class = filters.CNameRecordModelFilterSet
graphql_types = [DnsZoneModelType, ARecordModelType, CNameRecordModelType]
Under each class we define model
and filterset_class
attributes where we attach our plugin models and filtersets. By default, Nautobot looks for custom GraphQL types in an iterable named graphql_types within a graphql/types.py file. We add our classes to the list and expose them under graphql_types
special variable.
Now we can execute a query to retrieve our records. As opposed to REST API, where we need three separate queries to get our records, in GraphQL we can get what we want in a single query. Remember, GQL is only for read operations, so each interface has its pros and cons. Let’s get all records data.
{
dns_zone_models {
name
}
a_record_models {
name
zone{
name
}
address {
address
}
}
c_name_record_models {
name
zone{
name
}
}
}
Response:
{
"data": {
"dns_zone_models": [
{
"name": "example.com"
}
],
"a_record_models": [
{
"name": "app.example.com",
"zone": {
"name": "example.com"
},
"address": {
"address": "1.1.1.1/32"
}
}
],
"c_name_record_models": [
{
"name": "www.app.example.com",
"zone": {
"name": "example.com"
}
}
]
}
}
References
Conclusion
In this blog post, we learned how to build API endpoints for models defined in our plugin and how to integrate them with GraphQL.
-Patryk
Tags :
Contact Us to Learn More
Share details about yourself & someone from our team will reach out to you ASAP!