Skip to content

nautobot.apps.models

Data model classes and utilities for app implementation.

nautobot.apps.models.BaseModel

Bases: models.Model

Base model class that all models should inherit from.

This abstract base provides globally common fields and functionality.

Here we define the primary key to be a UUID field and set its default to automatically generate a random UUID value. Note however, this does not operate in the same way as a traditional auto incrementing field for which the value is issued by the database upon initial insert. In the case of the UUID field, Django creates the value upon object instantiation. This means the canonical pattern in Django of checking self.pk is None to tell if an object has been created in the actual database does not work because the object will always have the value populated prior to being saved to the database for the first time. An alternate pattern of checking not self.present_in_database can be used for the same purpose in most cases.

Source code in nautobot/core/models/__init__.py
class BaseModel(models.Model):
    """
    Base model class that all models should inherit from.

    This abstract base provides globally common fields and functionality.

    Here we define the primary key to be a UUID field and set its default to
    automatically generate a random UUID value. Note however, this does not
    operate in the same way as a traditional auto incrementing field for which
    the value is issued by the database upon initial insert. In the case of
    the UUID field, Django creates the value upon object instantiation. This
    means the canonical pattern in Django of checking `self.pk is None` to tell
    if an object has been created in the actual database does not work because
    the object will always have the value populated prior to being saved to the
    database for the first time. An alternate pattern of checking `not self.present_in_database`
    can be used for the same purpose in most cases.
    """

    id = models.UUIDField(primary_key=True, default=uuid.uuid4, unique=True, editable=False)

    objects = RestrictedQuerySet.as_manager()

    @property
    def present_in_database(self):
        """
        True if the record exists in the database, False if it does not.
        """
        return not self._state.adding

    class Meta:
        abstract = True

    def validated_save(self):
        """
        Perform model validation during instance save.

        This is a convenience method that first calls `self.full_clean()` and then `self.save()`
        which in effect enforces model validation prior to saving the instance, without having
        to manually make these calls seperately. This is a slight departure from Django norms,
        but is intended to offer an optional, simplified interface for performing this common
        workflow. The indended use is for user defined Jobs and scripts run via the `nbshell`
        command.
        """
        self.full_clean()
        self.save()

present_in_database property

True if the record exists in the database, False if it does not.

validated_save()

Perform model validation during instance save.

This is a convenience method that first calls self.full_clean() and then self.save() which in effect enforces model validation prior to saving the instance, without having to manually make these calls seperately. This is a slight departure from Django norms, but is intended to offer an optional, simplified interface for performing this common workflow. The indended use is for user defined Jobs and scripts run via the nbshell command.

Source code in nautobot/core/models/__init__.py
def validated_save(self):
    """
    Perform model validation during instance save.

    This is a convenience method that first calls `self.full_clean()` and then `self.save()`
    which in effect enforces model validation prior to saving the instance, without having
    to manually make these calls seperately. This is a slight departure from Django norms,
    but is intended to offer an optional, simplified interface for performing this common
    workflow. The indended use is for user defined Jobs and scripts run via the `nbshell`
    command.
    """
    self.full_clean()
    self.save()

nautobot.apps.models.CustomValidator

This class is used to register plugin custom model validators which act on specified models. It contains the clean method which is overridden by plugin authors to execute custom validation logic. Plugin authors must raise ValidationError within this method to trigger validation error messages which are propagated to the user. A convenience method validation_error(<message>) may be used for this purpose.

The model attribute on the class defines the model to which this validator is registered. It should be set as a string in the form <app_label>.<model_name>.

Source code in nautobot/extras/plugins/__init__.py
class CustomValidator:
    """
    This class is used to register plugin custom model validators which act on specified models. It contains the clean
    method which is overridden by plugin authors to execute custom validation logic. Plugin authors must raise
    ValidationError within this method to trigger validation error messages which are propagated to the user.
    A convenience method `validation_error(<message>)` may be used for this purpose.

    The `model` attribute on the class defines the model to which this validator is registered. It
    should be set as a string in the form `<app_label>.<model_name>`.
    """

    model = None

    def __init__(self, obj):
        self.context = {"object": obj}

    def validation_error(self, message):
        """
        Convenience method for raising `django.core.exceptions.ValidationError` which is required in order to
        trigger validation error messages which are propagated to the user.
        """
        raise ValidationError(message)

    def clean(self):
        """
        Implement custom model validation in the standard Django clean method pattern. The model instance is accessed
        with the `object` key within `self.context`, e.g. `self.context['object']`. ValidationError must be raised to
        prevent saving model instance changes, and propagate messages to the user. For convenience,
        `self.validation_error(<message>)` may be called to raise a ValidationError.
        """
        raise NotImplementedError

clean()

Implement custom model validation in the standard Django clean method pattern. The model instance is accessed with the object key within self.context, e.g. self.context['object']. ValidationError must be raised to prevent saving model instance changes, and propagate messages to the user. For convenience, self.validation_error(<message>) may be called to raise a ValidationError.

Source code in nautobot/extras/plugins/__init__.py
def clean(self):
    """
    Implement custom model validation in the standard Django clean method pattern. The model instance is accessed
    with the `object` key within `self.context`, e.g. `self.context['object']`. ValidationError must be raised to
    prevent saving model instance changes, and propagate messages to the user. For convenience,
    `self.validation_error(<message>)` may be called to raise a ValidationError.
    """
    raise NotImplementedError

validation_error(message)

Convenience method for raising django.core.exceptions.ValidationError which is required in order to trigger validation error messages which are propagated to the user.

Source code in nautobot/extras/plugins/__init__.py
def validation_error(self, message):
    """
    Convenience method for raising `django.core.exceptions.ValidationError` which is required in order to
    trigger validation error messages which are propagated to the user.
    """
    raise ValidationError(message)

nautobot.apps.models.OrganizationalModel

Bases: BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin

Base abstract model for all organizational models.

Organizational models aid the primary models by building structured relationships and logical groups, or categorizations. Organizational models do not typically represent concrete networking resources or assets, but rather they enable user specific use cases and metadata about network resources. Examples include Device Role, Rack Group, Status, Manufacturer, and Platform.

Source code in nautobot/core/models/generics.py
class OrganizationalModel(
    BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin
):
    """
    Base abstract model for all organizational models.

    Organizational models aid the primary models by building structured relationships
    and logical groups, or categorizations. Organizational models do not typically
    represent concrete networking resources or assets, but rather they enable user
    specific use cases and metadata about network resources. Examples include
    Device Role, Rack Group, Status, Manufacturer, and Platform.
    """

    class Meta:
        abstract = True

nautobot.apps.models.PrimaryModel

Bases: BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin

Base abstract model for all primary models.

A primary model is one which is materialistically relevant to the network datamodel. Such models form the basis of major elements of the data model, like Device, IP Address, Site, VLAN, Virtual Machine, etc. Primary models usually represent tangible or logical resources on the network, or within the organization.

Source code in nautobot/core/models/generics.py
class PrimaryModel(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel, DynamicGroupMixin, NotesMixin):
    """
    Base abstract model for all primary models.

    A primary model is one which is materialistically relevant to the network datamodel.
    Such models form the basis of major elements of the data model, like Device,
    IP Address, Site, VLAN, Virtual Machine, etc. Primary models usually represent
    tangible or logical resources on the network, or within the organization.
    """

    tags = TaggableManager(through=TaggedItem, manager=_NautobotTaggableManager, ordering=["name"])

    class Meta:
        abstract = True

nautobot.apps.models.StatusField

Bases: models.ForeignKey

Model database field that automatically limits custom choices.

The limit_choices_to for the field are automatically derived from
  • the content-type to which the field is attached (e.g. dcim.device)
Source code in nautobot/extras/models/statuses.py
class StatusField(models.ForeignKey):
    """
    Model database field that automatically limits custom choices.

    The limit_choices_to for the field are automatically derived from:

        - the content-type to which the field is attached (e.g. `dcim.device`)
    """

    def __init__(self, **kwargs):
        kwargs.setdefault("to", Status)
        kwargs.setdefault("null", True)
        super().__init__(**kwargs)

    def get_limit_choices_to(self):
        return {"content_types": ContentType.objects.get_for_model(self.model)}

    def contribute_to_class(self, cls, name, *args, private_only=False, **kwargs):
        """
        Overload default so that we can assert that `.get_FOO_display` is
        attached to any model that is using a `StatusField`.

        Using `.contribute_to_class()` is how field objects get added to the model
        at during the instance preparation. This is also where any custom model
        methods are hooked in. So in short this method asserts that any time a
        `StatusField` is added to a model, that model also gets a
        `.get_status_display()` and a `.get_status_color()` method without
        having to define it on the model yourself.
        """
        super().contribute_to_class(cls, name, *args, private_only=private_only, **kwargs)

        def _get_FIELD_display(self, field):
            """
            Closure to replace default model method of the same name.

            Cargo-culted from `django.db.models.base.Model._get_FIELD_display`
            """
            choices = field.get_choices()
            value = getattr(self, field.attname)
            choices_dict = dict(make_hashable(choices))
            # force_str() to coerce lazy strings.
            return force_str(choices_dict.get(make_hashable(value), value), strings_only=True)

        # Install `.get_FOO_display()` onto the model using our own version.
        if f"get_{self.name}_display" not in cls.__dict__:
            setattr(
                cls,
                f"get_{self.name}_display",
                partialmethod(_get_FIELD_display, field=self),
            )

        def _get_FIELD_color(self, field):
            """
            Return `self.FOO.color` (where FOO is field name).

            I am added to the model via `StatusField.contribute_to_class()`.
            """
            field_method = getattr(self, field.name)
            return getattr(field_method, "color")

        # Install `.get_FOO_color()` onto the model using our own version.
        if f"get_{self.name}_color" not in cls.__dict__:
            setattr(
                cls,
                f"get_{self.name}_color",
                partialmethod(_get_FIELD_color, field=self),
            )

    def formfield(self, **kwargs):
        """Return a prepped formfield for use in model forms."""
        defaults = {
            "form_class": DynamicModelChoiceField,
            "queryset": Status.objects.all(),
            # label_lower e.g. "dcim.device"
            "query_params": {"content_types": self.model._meta.label_lower},
        }
        defaults.update(**kwargs)
        return super().formfield(**defaults)

contribute_to_class(cls, name, args, private_only=False, kwargs)

Overload default so that we can assert that .get_FOO_display is attached to any model that is using a StatusField.

Using .contribute_to_class() is how field objects get added to the model at during the instance preparation. This is also where any custom model methods are hooked in. So in short this method asserts that any time a StatusField is added to a model, that model also gets a .get_status_display() and a .get_status_color() method without having to define it on the model yourself.

Source code in nautobot/extras/models/statuses.py
def contribute_to_class(self, cls, name, *args, private_only=False, **kwargs):
    """
    Overload default so that we can assert that `.get_FOO_display` is
    attached to any model that is using a `StatusField`.

    Using `.contribute_to_class()` is how field objects get added to the model
    at during the instance preparation. This is also where any custom model
    methods are hooked in. So in short this method asserts that any time a
    `StatusField` is added to a model, that model also gets a
    `.get_status_display()` and a `.get_status_color()` method without
    having to define it on the model yourself.
    """
    super().contribute_to_class(cls, name, *args, private_only=private_only, **kwargs)

    def _get_FIELD_display(self, field):
        """
        Closure to replace default model method of the same name.

        Cargo-culted from `django.db.models.base.Model._get_FIELD_display`
        """
        choices = field.get_choices()
        value = getattr(self, field.attname)
        choices_dict = dict(make_hashable(choices))
        # force_str() to coerce lazy strings.
        return force_str(choices_dict.get(make_hashable(value), value), strings_only=True)

    # Install `.get_FOO_display()` onto the model using our own version.
    if f"get_{self.name}_display" not in cls.__dict__:
        setattr(
            cls,
            f"get_{self.name}_display",
            partialmethod(_get_FIELD_display, field=self),
        )

    def _get_FIELD_color(self, field):
        """
        Return `self.FOO.color` (where FOO is field name).

        I am added to the model via `StatusField.contribute_to_class()`.
        """
        field_method = getattr(self, field.name)
        return getattr(field_method, "color")

    # Install `.get_FOO_color()` onto the model using our own version.
    if f"get_{self.name}_color" not in cls.__dict__:
        setattr(
            cls,
            f"get_{self.name}_color",
            partialmethod(_get_FIELD_color, field=self),
        )

formfield(kwargs)

Return a prepped formfield for use in model forms.

Source code in nautobot/extras/models/statuses.py
def formfield(self, **kwargs):
    """Return a prepped formfield for use in model forms."""
    defaults = {
        "form_class": DynamicModelChoiceField,
        "queryset": Status.objects.all(),
        # label_lower e.g. "dcim.device"
        "query_params": {"content_types": self.model._meta.label_lower},
    }
    defaults.update(**kwargs)
    return super().formfield(**defaults)

nautobot.apps.models.VarbinaryIPField

Bases: models.BinaryField

IP network address

Source code in nautobot/ipam/fields.py
class VarbinaryIPField(models.BinaryField):
    """
    IP network address
    """

    description = "IP network address"

    def db_type(self, connection):
        """Returns the correct field type for a given database vendor."""

        # Use 'bytea' type for PostgreSQL.
        if connection.vendor == "postgresql":
            return "bytea"

        # Or 'varbinary' for everyone else.
        return "varbinary(16)"

    def value_to_string(self, obj):
        """IPField is serialized as str(IPAddress())"""
        value = self.value_from_object(obj)
        if not value:
            return value

        return str(self._parse_address(value))

    def _parse_address(self, value):
        """
        Parse `str`, `bytes` (varbinary), or `netaddr.IPAddress to `netaddr.IPAddress`.
        """
        try:
            int_value = int.from_bytes(value, "big")
            # Distinguish between
            # \x00\x00\x00\x01 (IPv4 0.0.0.1) and
            # \x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01 (IPv6 ::1), among other cases
            version = 4 if len(value) == 4 else 6
            value = int_value
        except TypeError:
            version = None  # It's a string, IP version should be self-evident

        try:
            return netaddr.IPAddress(value, version=version)
        except netaddr.AddrFormatError:
            raise ValidationError(f"Invalid IP address format: {value}")
        except (TypeError, ValueError) as e:
            raise ValidationError(e)

    def from_db_value(self, value, expression, connection):
        """Converts DB (varbinary) to Python (str)."""
        return self.to_python(value)

    def to_python(self, value):
        """Converts `value` to Python (str)."""
        if isinstance(value, netaddr.IPAddress):
            return str(value)

        if value is None:
            return value

        return str(self._parse_address(value))

    def get_db_prep_value(self, value, connection, prepared=False):
        """Converts Python (str) to DB (varbinary)."""
        if value is None:
            return value

        # Parse the address and then pack it to binary.
        value = self._parse_address(value).packed

        # Use defaults for PostgreSQL
        if connection.vendor == "postgresql":
            return super().get_db_prep_value(value, connection, prepared)

        return value

    def form_class(self):
        return IPNetworkFormField

    def formfield(self, *args, **kwargs):
        defaults = {"form_class": self.form_class()}
        defaults.update(kwargs)
        return super().formfield(*args, **defaults)

db_type(connection)

Returns the correct field type for a given database vendor.

Source code in nautobot/ipam/fields.py
def db_type(self, connection):
    """Returns the correct field type for a given database vendor."""

    # Use 'bytea' type for PostgreSQL.
    if connection.vendor == "postgresql":
        return "bytea"

    # Or 'varbinary' for everyone else.
    return "varbinary(16)"

from_db_value(value, expression, connection)

Converts DB (varbinary) to Python (str).

Source code in nautobot/ipam/fields.py
def from_db_value(self, value, expression, connection):
    """Converts DB (varbinary) to Python (str)."""
    return self.to_python(value)

get_db_prep_value(value, connection, prepared=False)

Converts Python (str) to DB (varbinary).

Source code in nautobot/ipam/fields.py
def get_db_prep_value(self, value, connection, prepared=False):
    """Converts Python (str) to DB (varbinary)."""
    if value is None:
        return value

    # Parse the address and then pack it to binary.
    value = self._parse_address(value).packed

    # Use defaults for PostgreSQL
    if connection.vendor == "postgresql":
        return super().get_db_prep_value(value, connection, prepared)

    return value

to_python(value)

Converts value to Python (str).

Source code in nautobot/ipam/fields.py
def to_python(self, value):
    """Converts `value` to Python (str)."""
    if isinstance(value, netaddr.IPAddress):
        return str(value)

    if value is None:
        return value

    return str(self._parse_address(value))

value_to_string(obj)

IPField is serialized as str(IPAddress())

Source code in nautobot/ipam/fields.py
def value_to_string(self, obj):
    """IPField is serialized as str(IPAddress())"""
    value = self.value_from_object(obj)
    if not value:
        return value

    return str(self._parse_address(value))

nautobot.apps.models.extras_features(features)

Decorator used to register extras provided features to a model

Source code in nautobot/extras/utils.py
def extras_features(*features):
    """
    Decorator used to register extras provided features to a model
    """

    def wrapper(model_class):
        # Initialize the model_features store if not already defined
        if "model_features" not in registry:
            registry["model_features"] = {f: collections.defaultdict(list) for f in EXTRAS_FEATURES}
        for feature in features:
            if feature in EXTRAS_FEATURES:
                app_label, model_name = model_class._meta.label_lower.split(".")
                registry["model_features"][feature][app_label].append(model_name)
            else:
                raise ValueError(f"{feature} is not a valid extras feature!")
        return model_class

    return wrapper