Developing Nautobot Plugins – Part 5

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 UUIDNestedIPAddressSerializer 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



ntc img
ntc img

Contact Us to Learn More

Share details about yourself & someone from our team will reach out to you ASAP!

Author