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`

    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    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

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type(cls):  # pylint: disable=no-self-argument
            """
            Return the ContentType of the object, never cached.
            """
            return ContentType.objects.get_for_model(cls)

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type_cache_key(cls):  # pylint: disable=no-self-argument
            """
            Return the cache key for the ContentType of the object.

            Necessary for use with _content_type_cached and management commands.
            """
            return f"{cls._meta.label_lower}._content_type"

        @classproperty  # https://github.com/PyCQA/pylint-django/issues/240
        def _content_type_cached(cls):  # pylint: disable=no-self-argument
            """
            Return the ContentType of the object, cached.
            """

            return cache.get_or_set(cls._content_type_cache_key, cls._content_type, settings.CONTENT_TYPE_CACHE_TIMEOUT)

        class Meta:
            abstract = True

        def validated_save(self, *args, **kwargs):
            """
            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 intended use is for user defined Jobs and scripts run via the `nbshell`
            command.
            """
            self.full_clean()
            self.save(*args, **kwargs)

### `present_in_database` `property`

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

### `validated_save(*args, **kwargs)`

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 intended use is for user defined Jobs and scripts run via the `nbshell` command.

Source code in `nautobot/core/models/__init__.py`

    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    def validated_save(self, *args, **kwargs):
        """
        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 intended use is for user defined Jobs and scripts run via the `nbshell`
        command.
        """
        self.full_clean()
        self.save(*args, **kwargs)

## `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`

    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660
    661
    662
    663
    664
    665
    666
    667
    668
    669
    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`

    662
    663
    664
    665
    666
    667
    668
    669
    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`

    655
    656
    657
    658
    659
    660
    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`

    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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`

    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    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`

     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    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`

    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    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`

    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    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`

     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    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 get_default(self):
            value = super().get_default()
            # Prevent None or "" values from being represented as b''
            if value in self.empty_values:
                return None
            return value

        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`

    16
    17
    18
    19
    20
    21
    22
    23
    24
    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`

    62
    63
    64
    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`

    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    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`

    66
    67
    68
    69
    70
    71
    72
    73
    74
    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`

    33
    34
    35
    36
    37
    38
    39
    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`

    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    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