Nautobot Apps and Data Model Relationships

When developing a Nautobot App, there are multiple ways to integrate any new data models belonging to that App with the core data models provided by Nautobot itself. I’m writing to share a few quick tips about which approaches to choose.

Classes of Data Relationships

There are four basic classes of data relationships you might wish to implement in your App:

  1. One to One: Each record of type A relates to at most one record of type B and vice versa. For example, a VirtualChassis has at most one Device serving as the primary for that chassis, and a Device is the primary for at most one VirtualChassis.
  2. One to Many: Each record of type A relates to any number of records of type B, but each record of type B relates to at most one record of type A. For example, a Location may have many Racks, but each Rack has only one Location.
  3. Many to One: The reverse of the previous class. I’m calling it out as a separate item, because in some cases it needs to be handled differently when developing an App.
  4. Many to Many: Any number of records of type A relate to any number of records of type B. For example, a VRF might have many associated RouteTarget records as its import and export targets, and a RouteTarget might be reused across many VRF records.

Options for Implementing Data Relationships in Nautobot

The first, and seemingly easiest, approach to implement would be something like a CharField on your App’s model (or a String-type CustomField added to a core model) that identifies a related record(s) by its nameslug, or similar natural key. I’m including this only for completeness, as really you should never do this. It has many drawbacks, notably in terms of data validation and consistency. For example, there’s no inherent guarantee that the related record exists in the first place, or that it will continue to exist so long as you have a reference to it. Nautobot is built atop a relational database and as such has first-class support for representing and tracking object relationships at the database level. You should take advantage of these features instead!

The next, and most traditional, approach is to represent data relationships using native database features such as foreign keys. This has a lot of advantages, including database validation, data consistency, and optimal performance. In most cases, this will be your preferred approach when developing new data models in your App, but there are a few cases where it isn’t possible.

The final approach, which is specific to Nautobot, is to make use of Nautobot’s Relationship feature, which allows a user or developer to define arbitrary data relationships between any two models. This is an extremely powerful and flexible feature, and is especially useful to a Nautobot user who wishes to associate existing models in a new way, but from an App developer standpoint, it should often be your fallback choice rather than your first choice, because it lacks some of the performance advantages of native database constructs.

Implementing One-to-One Data Relationships

A one-to-one relationship between App data models, or between an App model and a core Nautobot model, should generally be implemented as a Django OneToOneField on the appropriate App data model. This is a special case of a ForeignKey and provides all of the same inherent performance and data consistency benefits. You can use Django features such as on_delete=models.PROTECT or on_delete=models.CASCADE to control how your data model will automatically respond when the other related model is deleted.

An example from the nautobot-firewall-models App:

class CapircaPolicy(PrimaryModel):
    """CapircaPolicy model."""

    device = models.OneToOneField(
        to="dcim.Device",
        blank=True,
        null=True,
        on_delete=models.CASCADE,
        related_name="capirca_policy",
    )

In this example, each CapircaPolicy maps to at most one Device, and vice versa. Deleting a Device will result in its associated CapircaPolicy being automatically deleted as well.

If, and only if, your App needs to define a new relationship between two core Nautobot models, you cannot use a OneToOneField because an App cannot directly modify a core model. In this case, your fallback option would be to create a one-to-one Relationship record as the way of adding this data relationship. This is a pretty rare case, so I don’t have a real-world example to point to, but it would conceptually be implemented using the nautobot_database_ready signal:

def handle_nautobot_database_ready(sender, *, apps, **kwargs):
    Relationship.objects.get_or_create(
        slug="originating_device_to_vrf",
        defaults={
            "name": "Originating Device to VRF",
            "type": RelationshipTypeChoices.TYPE_ONE_TO_ONE,
            "source_type": ContentType.objects.get_for_model(Device),
            "destination_type": ContentType.objects.get_for_model(VRF),
        },
    )

Implementing One-to-Many and Many-to-One Data Relationships

A one-to-many or many-to-one data relationship between two App models should be implemented as a standard Django ForeignKey field from the “many” model to the “one” model. The same approach works for a many-to-one relationship from an App model to a core Nautobot model.

An example from the nautobot-device-lifecycle-mgmt App:

class SoftwareLCM(PrimaryModel):
    """Software Life-Cycle Management model."""

    device_platform = models.ForeignKey(
        to="dcim.Platform",
        on_delete=models.CASCADE,
        verbose_name="Device Platform"
    )

In this example, many SoftwareLCM may all map to a single Platform, and deleting a Platform will automatically delete all such SoftwareLCM records.

Because, again, an App cannot directly modify a core model, this approach cannot be used for a one-to-many relation from an App model to a core model, or between two core models, because it would require adding a ForeignKey on the core model itself. In this case, you’ll need to create a Relationship, as in this example from the nautobot-ssot App’s Infoblox integration:

def nautobot_database_ready_callback(sender, *, apps, **kwargs):
    # ...

    # add Prefix -> VLAN Relationship
    relationship_dict = {
        "name": "Prefix -> VLAN",
        "slug": "prefix_to_vlan",
        "type": RelationshipTypeChoices.TYPE_ONE_TO_MANY,
        "source_type": ContentType.objects.get_for_model(Prefix),
        "source_label": "Prefix",
        "destination_type": ContentType.objects.get_for_model(VLAN),
        "destination_label": "VLAN",
    }
    Relationship.objects.get_or_create(name=relationship_dict["name"], defaults=relationship_dict)

Implementing Many-to-Many Data Relationships

A many-to-many data relationship involving App models should be implemented via a Django ManyToManyField. An example from the nautobot-circuit-maintenance App:

class NotificationSource(OrganizationalModel):
    # ...

    providers = models.ManyToManyField(
        Provider,
        help_text="The Provider(s) that this Notification Source applies to.",
        blank=True,
    )

One NotificationSource can provide notifications for many different Providers, and any given Provider may have multiple distinct NotificationSources.

Once again, the only exception is when a relationship between two core Nautobot models is desired, in which case use of a Relationship would be required. This is another fairly rare case and so I don’t have a real-world example to point to here, but it would follow the similar pattern to the other Relationship examples above.

Conclusion and Summary

Here’s a handy table summarizing which approach to take for various data relationships:

Model AModel BCardinalityRecommended Approach
App modelApp modelOne-to-OneOneToOneField on either model
App modelApp modelOne-to-ManyForeignKey on model B
App modelApp modelMany-to-OneForeignKey on model A
App modelApp modelMany-to-ManyManyToManyField on either model
App modelCore modelOne-to-OneOneToOneField on model A
App modelCore modelOne-to-ManyRelationship definition
App modelCore modelMany-to-OneForeignKey on model A
App modelCore modelMany-to-ManyManyToManyField on model A
Core modelCore modelOne-to-OneRelationship definition
Core modelCore modelOne-to-ManyRelationship definition
Core modelCore modelMany-to-OneRelationship definition
Core modelCore modelMany-to-ManyRelationship definition

Conclusion

I hope you’ve found this post useful. Go forth and model some data!

-Glenn



ntc img
ntc img

Contact Us to Learn More

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

Author