Netbox Entity Relationship Diagrams Deep Dive

When you’re working with a relational database, you’ll need to create some kind of Entity Relationship Diagram (ERD) at some point. ERDs are helpful in visualizing the relationships between database tables and can be a helpful tool for designing or reverse-engineering databases.

This project creates a new tab on all netbox model detail views.

When selected, this produces the Entity Relationship Diagram for that model and its related models.

Heres how I did it.

Dependencies

Using Netbox 3.4.4, I added these dependencies using the poetry tool. https://python-poetry.org/docs/cli/#add

python = "^3.8
graphviz = "^0.20.1"
django-extensions = "^3.2.1"
pydot = "^1.4.2"
pygraphviz = "^1.10""

__init__

The required django_extensions package can now be configured in the plugin’s __init__ file.

...
django_apps = ["django_extensions"]
...

Template

Nothing special here but note the word safe. This is required to render the xml file. If omitted, only the string is shown.

{% extends 'base/layout.html' %


{% block header %}
    

Entity Relationship Diagram

{% endblock header %} {% block content %}
{{ xml_str | safe }}
{% endblock %} }

View

Here’s the imports.

import django.app
import pygraphviz


from django.apps import apps
from django.contrib.auth.mixins import PermissionRequiredMixin
from django_extensions.management.modelviz import ModelGraph, generate_dot, os, loader
from django.shortcuts import render
from django.views.generic import View


from re import search
from urllib.parse import urlparse
from utilities.views import ViewTab


from netbox.registry import registrys

The ERD view is created for numerous (most) netbox models. Each need to be registered with netbox. I looped models and further looked for the User facing ones. I then made the registration.

for model in django.apps.apps.get_models()
    """Register Models."""


    if model._meta.app_label in ["circuits", "wireless", "ipam", "tenancy", "dcim", "virtualization"]:
        app_label = model._meta.app_label
        model_name = model._meta.model_name


        if model_name not in registry["views"][app_label]:
            registry["views"][app_label][model_name] = []


        registry["views"][app_label][model_name].append(
            {
                "name": "erd",
                "view": "netbox_erd.views.EntityRelationshipDiagramView",
                "path": "../../../plugins/erd",
                "kwargs": {},
            }
        ):

Here’s the class for the view. The tab is defined here, with the permissions, as per this:
https://docs.netbox.dev/en/stable/plugins/development/views/

class EntityRelationshipDiagramView(PermissionRequiredMixin, View)
    """View for Entity Relationship Diagram."""


    tab = ViewTab(label="Entity Relationship Diagram", permission="netbox_erd.erd")


    permission_required = "netbox_erd.erd"
    queryset = None
    template_name = "netbox_erd/erd.html"


    def get(self, request):
        """Display Entity Relationship Diagram.""":

First the Model that the User was previously viewing is found. The app_label and model_name are recovered from the http referer in the request.

# Get the Model that the User was previously viewin
        referer = request.META.get("HTTP_REFERER")
        app_label = urlparse(referer).path.split("/")[-4]
        model_name = urlparse(referer).path.split("/")[-3].replace("-", "").capitalize()
        if search("ses$", model_name):
            model_name = urlparse(referer).path.split("/")[-3][:-2].replace("-", "").capitalize()
        else:
            model_name = urlparse(referer).path.split("/")[-3][:-1].replace("-", "").capitalize()g

Next a list of related Models to include in the graph is created. This gives the users last viewed model and its related models, that is the relationships from the models Foreign Keys.

# Create a list of related Models to include in the graph
        include_models = []
        include_models.append(model_name)
        model = apps.get_model(app_label=app_label, model_name=model_name)
        for relation in model._meta.related_objects:
            include_models.append(relation.related_model.__name__.capitalize())h

The modelviz ModelGraph is created. This is the minimal required with default values. Here’s the link:
https://github.com/django-extensions/django-extensions/blob/9cc676b98596fc7f716be752bb89f943ea81f234/django_extensions/management/modelviz.py#L73

 # Create the Model Graph
        args = []
        cli_options = {}
        options = {
            "pygraphviz": True,
            "all_applications": True,
            "arrow_shape": "normal",
            "include_models": include_models,
        }
        graph_models = ModelGraph(args, cli_options=cli_options, **options)h

dot file creation

  graph_models.generate_graph_data(
        graph_data = graph_models.get_graph_data(as_json=False)
        theme = "django2018"
        template_name = os.path.join("django_extensions", "graph_models", theme, "digraph.dot")
        template = loader.get_template(template_name)
        dotdata = generate_dot(graph_data, template=template))

Finally, the xml containing SVG is created for use in the template.

        # Create the xml for display
        graph = pygraphviz.AGraph(dotdata)
        graph.layout(prog="dot")
        xml_bytes = graph.draw(format="svg")
        xml_str = xml_bytes.decode()
        return render(
            request,
            self.template_name,
            {
                "xml_str": xml_str,
            },
        )

That’s it.