Skip to content

Modeling

This page describes how to model various kinds of fields on a nautobot_ssot.contrib.NautobotModel subclass.

Quick Reference

The following table describes in brief the different types of model fields and how they are handled.

Type of field Field name Notes Applies to
Normal fields Has to match ORM exactly Make sure that the name matches the name in the ORM model. Fields that are neither custom fields nor relations
Custom fields Field name doesn't matter Use nautobot_ssot.contrib.CustomFieldAnnotation Nautobot custom fields
*-to-one relationships Django lookup syntax See here - your model fields need to use this syntax django.db.models.OneToOneField, django.db.models.ForeignKey, django.contrib.contenttypes.fields.GenericForeignKey
*-to-many relationships Has to match ORM exactly In case of a generic foreign key see here django.db.models.ManyToManyField, django.contrib.contenttypes.fields.GenericRelation, django.db.models.ForeignKey backwards
Custom Relationships Field name doesn't matter Use nautobot_ssot.contrib.CustomRelationshipAnnotation https://docs.nautobot.com/projects/core/en/stable/models/extras/relationship/

Normal Fields

For normal, non-relationship, non-custom fields on a model, all you need to do is to ensure that field name on your SSoT model class matches that of the Nautobot model class. To ensure this is the case, you can check out the model either by reading the corresponding source code on GitHub or through the Nautobot Shell - in there you can use commands like dir(Tenant) to get an overview of available fields for the different models.

Note

If you want to sync data into a model provided by a Nautobot App, you need to navigate to its respective source code repository. Furthermore, you may need to manually import the model if you're using the Nautobot Shell approach.

Custom Fields

For custom fields, you will need to use the nautobot_ssot.contrib.CustomFieldAnnotation class. Given a custom field called "Test Custom Field" on the circuit provider model, this is how you could map that custom field into the diffsync model field test_custom_field:

try:
    from typing import Annotated  # Python>=3.9
except ModuleNotFoundError:
    from typing_extensions import Annotated  # Python<3.9
from nautobot.circuits.models import Provider
from nautobot_ssot.contrib import NautobotModel, CustomFieldAnnotation

class ProviderModel(NautobotModel):
    _model = Provider
    _modelname = "provider"
    _identifiers = ("name",)
    _attributes = ("test_custom_field",)

    name: str
    test_custom_field: Annotated[str, CustomFieldAnnotation(key="test_custom_field")]

Note

Defining a CustomFieldAnnotation variable is necessary since custom field names may include spaces, which are un-representable in Python object field names.

*-to-one Relationships

For many-to-one relationships (i.e. foreign keys or generic foreign keys) a slightly different approach is employed. We need to add a field on our model for each field that we need in order to uniquely identify our related object behind the many-to-one relationship. We can do this using a familiar syntax of double underscore separated paths. Assuming we want to synchronize prefixes and associate them with locations, we may be faced with the problems that locations aren't uniquely identified by name alone, but rather need the location type as well, we can address this as follows.

from nautobot.ipam.models import Prefix
from nautobot_ssot.contrib import NautobotModel

class PrefixModel(NautobotModel):
    _model = Prefix
    _identifiers = ("network", "prefix_length")
    _attributes = ("vlan__vid", "vlan__group__name")

    network: str
    prefix_length: int

    vlan__vid: int
    vlan__group__name: str

Now, on model create or update, the SSoT framework will dynamically pull in the location with the specified name and location type name, uniquely identifying the location and populating the foreign key. In this case, the corresponding query will look something like this:

from nautobot.dcim.models import Location
prefix = PrefixModel(network="192.0.2.0", prefix_length=26, vlan__vid=1000, vlan__group__name="Datacenter")

# A query similar to the following line will be used to automatically populate the foreign key field upon `prefix.create`
VLAN.objects.get(vid=prefix.vlan__vid, group__name=prefix.vlan__group__name)

Special Case: Generic Foreign Key

In the case of a generic foreign key, we are faced with a problem. With normal foreign keys the content type of a relationship field can be inferred from the model class. In the case of generic foreign keys however, this is not the case. In the example of the IP address model in Nautobot, the assigned_object field can point to either a device's or a VM's interface. We address this the following way:

from nautobot.ipam.models import IPAddress
from nautobot_ssot.contrib import NautobotModel

class NautobotIPAddress(NautobotModel):
    _model = IPAddress
    _modelname = "ip_address"
    _identifiers = (
        "host",
        "prefix_length",
    )
    _attributes = (
        "status__name",
        "assigned_object__app_label",
        "assigned_object__model",
        "assigned_object__device__name",
        "assigned_object__name",
    )

    host: str
    prefix_length: int
    status__name: str
    assigned_object__app_label: str
    assigned_object__model: str
    assigned_object__device__name: str
    assigned_object__name: str

Warning

A limitation of this approach is that we are locked into a single kind of content type per model and foreign key, unless the field names for all content types match. In this specific case, the VM interface model does not have a device field which will cause the create and update method of NautobotIPAddress to raise a ValueError. This is a known issue.

*-to-many Relationships

For "-to-many" relationships such as (generic) foreign keys traversed backwards or many-to-many relationships, we need to employ a different mechanism. Again we start by identifying which fields of the related object we are interested in for queries to* the model. In this case, our example will be using an Interface model for which we also want to sync the associated IP addresses. For our scenario, lets assume that our IP addresses can be uniquely identified through the host and prefix_length fields:

try:
    from typing import TypedDict  # Python>=3.9
except ImportError:
    from typing_extensions import TypedDict  # Python<3.9

class IPAddressDict(TypedDict):
    """This typed dict is 100% decoupled from the `NautobotIPAddress` class defined above."""
    host: str
    prefix_length: int

Having defined this, we can now define our diffsync model:

from typing import List  # use the builtin 'list' from Python 3.9 on
from nautobot.dcim.models import Interface
from nautobot_ssot.contrib import NautobotModel

class NautobotInterface(NautobotModel):
    _model = Interface
    _modelname = "interface"
    _identifiers = (
        "name",
        "device__name",
    )
    _attributes = ("ip_addresses",)

    name: str
    device__name: str
    ip_addresses: List[IPAddressDict] = []

Note

In this example we can also see a case where a foreign key (named device, through device__name) constitutes part of the _identfiers field - this is a common pattern as model relationships define their uniqueness across multiple models in Nautobot.

Through us defining the model, Nautobot will now be able to dynamically load IP addresses related to our interfaces from Nautobot, and also set the related objects on the relationship in the create and update methods.

Note

Although Interface.ip_addresses is a generic relation, there is only one content type (i.e. ipam.ipaddress) that may be related through this relation, therefore we don't have to specific this in any way.

Filtering Objects Loaded From Nautobot

If you'd like to filter the objects loaded from the Nautobot, you can do so creating a get_queryset function in your model class and return your own queryset. Here is an example where the adapter would only load Tenant objects whose name starts with an "s".

from nautobot.tenancy.models import Tenant
from nautobot_ssot.contrib import NautobotModel

class TenantModel(NautobotModel):
    _model = Tenant
    _modelname = "tenant"
    _identifiers = ("name",)
    _attributes = ("description",)

    name: str
    description: str

    @classmethod
    def get_queryset(cls):
        return Tenant.objects.filter(name__startswith="s")