2021-03-04 13:06:04 -05:00
|
|
|
import logging
|
2021-02-24 21:01:16 -05:00
|
|
|
from collections import OrderedDict
|
|
|
|
|
2021-03-16 15:00:08 -04:00
|
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
2021-02-24 21:01:16 -05:00
|
|
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
|
|
from django.core.validators import ValidationError
|
|
|
|
from django.db import models
|
|
|
|
from mptt.models import MPTTModel, TreeForeignKey
|
2021-03-10 14:32:50 -05:00
|
|
|
from taggit.managers import TaggableManager
|
2021-02-24 21:01:16 -05:00
|
|
|
|
2021-03-04 13:06:04 -05:00
|
|
|
from extras.choices import ObjectChangeActionChoices
|
2021-02-24 21:01:16 -05:00
|
|
|
from utilities.mptt import TreeManager
|
|
|
|
from utilities.utils import serialize_object
|
|
|
|
|
|
|
|
__all__ = (
|
|
|
|
'BigIDModel',
|
2021-03-10 13:35:13 -05:00
|
|
|
'ChangeLoggedModel',
|
2021-02-24 21:01:16 -05:00
|
|
|
'NestedGroupModel',
|
|
|
|
'OrganizationalModel',
|
|
|
|
'PrimaryModel',
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2021-02-26 16:12:52 -05:00
|
|
|
#
|
|
|
|
# Mixins
|
|
|
|
#
|
2021-02-24 21:01:16 -05:00
|
|
|
|
2021-02-26 16:12:52 -05:00
|
|
|
class ChangeLoggingMixin(models.Model):
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
2021-02-26 16:12:52 -05:00
|
|
|
Provides change logging support.
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
|
|
|
created = models.DateField(
|
|
|
|
auto_now_add=True,
|
|
|
|
blank=True,
|
|
|
|
null=True
|
|
|
|
)
|
|
|
|
last_updated = models.DateTimeField(
|
|
|
|
auto_now=True,
|
|
|
|
blank=True,
|
|
|
|
null=True
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
2021-03-04 13:06:04 -05:00
|
|
|
def snapshot(self):
|
|
|
|
"""
|
|
|
|
Save a snapshot of the object's current state in preparation for modification.
|
|
|
|
"""
|
|
|
|
logger = logging.getLogger('netbox')
|
|
|
|
logger.debug(f"Taking a snapshot of {self}")
|
|
|
|
self._prechange_snapshot = serialize_object(self)
|
|
|
|
|
|
|
|
def to_objectchange(self, action, related_object=None):
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
|
|
|
Return a new ObjectChange representing a change made to this object. This will typically be called automatically
|
|
|
|
by ChangeLoggingMiddleware.
|
|
|
|
"""
|
|
|
|
from extras.models import ObjectChange
|
2021-03-04 13:06:04 -05:00
|
|
|
objectchange = ObjectChange(
|
2021-02-24 21:01:16 -05:00
|
|
|
changed_object=self,
|
2021-03-04 13:06:04 -05:00
|
|
|
related_object=related_object,
|
2021-02-24 21:01:16 -05:00
|
|
|
object_repr=str(self),
|
2021-03-04 13:06:04 -05:00
|
|
|
action=action
|
2021-02-24 21:01:16 -05:00
|
|
|
)
|
2021-03-04 13:06:04 -05:00
|
|
|
if hasattr(self, '_prechange_snapshot'):
|
|
|
|
objectchange.prechange_data = self._prechange_snapshot
|
|
|
|
if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE):
|
|
|
|
objectchange.postchange_data = serialize_object(self)
|
|
|
|
|
|
|
|
return objectchange
|
2021-02-24 21:01:16 -05:00
|
|
|
|
|
|
|
|
2021-02-26 16:12:52 -05:00
|
|
|
class CustomFieldsMixin(models.Model):
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
2021-02-26 16:12:52 -05:00
|
|
|
Provides support for custom fields.
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
|
|
|
custom_field_data = models.JSONField(
|
|
|
|
encoder=DjangoJSONEncoder,
|
|
|
|
blank=True,
|
|
|
|
default=dict
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
@property
|
|
|
|
def cf(self):
|
|
|
|
"""
|
|
|
|
Convenience wrapper for custom field data.
|
|
|
|
"""
|
|
|
|
return self.custom_field_data
|
|
|
|
|
|
|
|
def get_custom_fields(self):
|
|
|
|
"""
|
|
|
|
Return a dictionary of custom fields for a single object in the form {<field>: value}.
|
|
|
|
"""
|
|
|
|
from extras.models import CustomField
|
|
|
|
|
|
|
|
fields = CustomField.objects.get_for_model(self)
|
|
|
|
return OrderedDict([
|
|
|
|
(field, self.custom_field_data.get(field.name)) for field in fields
|
|
|
|
])
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
|
|
|
from extras.models import CustomField
|
|
|
|
|
|
|
|
custom_fields = {cf.name: cf for cf in CustomField.objects.get_for_model(self)}
|
|
|
|
|
|
|
|
# Validate all field values
|
|
|
|
for field_name, value in self.custom_field_data.items():
|
|
|
|
if field_name not in custom_fields:
|
|
|
|
raise ValidationError(f"Unknown field name '{field_name}' in custom field data.")
|
|
|
|
try:
|
|
|
|
custom_fields[field_name].validate(value)
|
|
|
|
except ValidationError as e:
|
|
|
|
raise ValidationError(f"Invalid value for custom field '{field_name}': {e.message}")
|
|
|
|
|
|
|
|
# Check for missing required values
|
|
|
|
for cf in custom_fields.values():
|
|
|
|
if cf.required and cf.name not in self.custom_field_data:
|
|
|
|
raise ValidationError(f"Missing required custom field '{cf.name}'.")
|
|
|
|
|
|
|
|
|
2021-02-26 16:12:52 -05:00
|
|
|
#
|
|
|
|
# Base model classes
|
|
|
|
|
|
|
|
class BigIDModel(models.Model):
|
|
|
|
"""
|
|
|
|
Abstract base model for all data objects. Ensures the use of a 64-bit PK.
|
|
|
|
"""
|
|
|
|
id = models.BigAutoField(
|
|
|
|
primary_key=True
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
|
2021-03-10 13:35:13 -05:00
|
|
|
class ChangeLoggedModel(ChangeLoggingMixin, BigIDModel):
|
|
|
|
"""
|
|
|
|
Base model for all objects which support change logging.
|
|
|
|
"""
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
|
2021-02-26 16:12:52 -05:00
|
|
|
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel):
|
|
|
|
"""
|
|
|
|
Primary models represent real objects within the infrastructure being modeled.
|
|
|
|
"""
|
2021-03-16 15:00:08 -04:00
|
|
|
journal_entries = GenericRelation(
|
|
|
|
to='extras.JournalEntry',
|
|
|
|
object_id_field='assigned_object_id',
|
|
|
|
content_type_field='assigned_object_type'
|
|
|
|
)
|
|
|
|
tags = TaggableManager(
|
|
|
|
through='extras.TaggedItem'
|
|
|
|
)
|
2021-02-26 16:12:52 -05:00
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
|
2021-02-26 16:25:37 -05:00
|
|
|
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel, MPTTModel):
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
|
|
|
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
|
|
|
|
recursively using MPTT. Within each parent, each child instance must have a unique name.
|
|
|
|
"""
|
|
|
|
parent = TreeForeignKey(
|
|
|
|
to='self',
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
related_name='children',
|
|
|
|
blank=True,
|
|
|
|
null=True,
|
|
|
|
db_index=True
|
|
|
|
)
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=100
|
|
|
|
)
|
|
|
|
description = models.CharField(
|
|
|
|
max_length=200,
|
|
|
|
blank=True
|
|
|
|
)
|
|
|
|
|
|
|
|
objects = TreeManager()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
|
|
|
|
class MPTTMeta:
|
|
|
|
order_insertion_by = ('name',)
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
2021-04-11 13:16:00 -04:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
|
|
|
|
|
|
|
# An MPTT model cannot be its own parent
|
|
|
|
if self.pk and self.parent_id == self.pk:
|
|
|
|
raise ValidationError({
|
|
|
|
"parent": "Cannot assign self as parent."
|
|
|
|
})
|
|
|
|
|
2021-02-24 21:01:16 -05:00
|
|
|
|
2021-02-26 16:25:37 -05:00
|
|
|
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, BigIDModel):
|
2021-02-24 21:01:16 -05:00
|
|
|
"""
|
|
|
|
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
|
|
|
|
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
|
|
|
|
models provide the following standard attributes:
|
|
|
|
- Unique name
|
|
|
|
- Unique slug (automatically derived from name)
|
|
|
|
- Optional description
|
|
|
|
"""
|
|
|
|
name = models.CharField(
|
|
|
|
max_length=100,
|
|
|
|
unique=True
|
|
|
|
)
|
|
|
|
slug = models.SlugField(
|
|
|
|
max_length=100,
|
|
|
|
unique=True
|
|
|
|
)
|
|
|
|
description = models.CharField(
|
|
|
|
max_length=200,
|
|
|
|
blank=True
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
abstract = True
|
|
|
|
ordering = ('name',)
|