From e97adcb614e1ee8df671cd31c47e6936d153136b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 16 Mar 2021 13:08:07 -0400 Subject: [PATCH] Move ConfigContext classes out of models.py --- netbox/extras/models/__init__.py | 6 +- netbox/extras/models/configcontexts.py | 161 +++++++++++++++++++++++++ netbox/extras/models/models.py | 154 +---------------------- 3 files changed, 165 insertions(+), 156 deletions(-) create mode 100644 netbox/extras/models/configcontexts.py diff --git a/netbox/extras/models/__init__.py b/netbox/extras/models/__init__.py index 95132e43a..2d6feb298 100644 --- a/netbox/extras/models/__init__.py +++ b/netbox/extras/models/__init__.py @@ -1,9 +1,7 @@ from .change_logging import ObjectChange +from .configcontexts import ConfigContext, ConfigContextModel from .customfields import CustomField -from .models import ( - ConfigContext, ConfigContextModel, CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script, - Webhook, -) +from .models import CustomLink, ExportTemplate, ImageAttachment, JobResult, Report, Script, Webhook from .tags import Tag, TaggedItem __all__ = ( diff --git a/netbox/extras/models/configcontexts.py b/netbox/extras/models/configcontexts.py new file mode 100644 index 000000000..b7da6852c --- /dev/null +++ b/netbox/extras/models/configcontexts.py @@ -0,0 +1,161 @@ +from collections import OrderedDict + +from django.core.validators import ValidationError +from django.db import models +from django.urls import reverse + +from extras.querysets import ConfigContextQuerySet +from extras.utils import extras_features +from netbox.models import ChangeLoggedModel +from utilities.utils import deepmerge + + +__all__ = ( + 'ConfigContext', + 'ConfigContextModel', +) + + +# +# Config contexts +# + +@extras_features('webhooks') +class ConfigContext(ChangeLoggedModel): + """ + A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned + qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B + will be available to a Device in site A assigned to tenant B. Data is stored in JSON format. + """ + name = models.CharField( + max_length=100, + unique=True + ) + weight = models.PositiveSmallIntegerField( + default=1000 + ) + description = models.CharField( + max_length=200, + blank=True + ) + is_active = models.BooleanField( + default=True, + ) + regions = models.ManyToManyField( + to='dcim.Region', + related_name='+', + blank=True + ) + site_groups = models.ManyToManyField( + to='dcim.SiteGroup', + related_name='+', + blank=True + ) + sites = models.ManyToManyField( + to='dcim.Site', + related_name='+', + blank=True + ) + roles = models.ManyToManyField( + to='dcim.DeviceRole', + related_name='+', + blank=True + ) + platforms = models.ManyToManyField( + to='dcim.Platform', + related_name='+', + blank=True + ) + cluster_groups = models.ManyToManyField( + to='virtualization.ClusterGroup', + related_name='+', + blank=True + ) + clusters = models.ManyToManyField( + to='virtualization.Cluster', + related_name='+', + blank=True + ) + tenant_groups = models.ManyToManyField( + to='tenancy.TenantGroup', + related_name='+', + blank=True + ) + tenants = models.ManyToManyField( + to='tenancy.Tenant', + related_name='+', + blank=True + ) + tags = models.ManyToManyField( + to='extras.Tag', + related_name='+', + blank=True + ) + data = models.JSONField() + + objects = ConfigContextQuerySet.as_manager() + + class Meta: + ordering = ['weight', 'name'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('extras:configcontext', kwargs={'pk': self.pk}) + + def clean(self): + super().clean() + + # Verify that JSON data is provided as an object + if type(self.data) is not dict: + raise ValidationError( + {'data': 'JSON data must be in object form. Example: {"foo": 123}'} + ) + + +class ConfigContextModel(models.Model): + """ + A model which includes local configuration context data. This local data will override any inherited data from + ConfigContexts. + """ + local_context_data = models.JSONField( + blank=True, + null=True, + ) + + class Meta: + abstract = True + + def get_config_context(self): + """ + Return the rendered configuration context for a device or VM. + """ + + # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs + data = OrderedDict() + + if not hasattr(self, 'config_context_data'): + # The annotation is not available, so we fall back to manually querying for the config context objects + config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) + else: + # The attribute may exist, but the annotated value could be None if there is no config context data + config_context_data = self.config_context_data or [] + + for context in config_context_data: + data = deepmerge(data, context) + + # If the object has local config context data defined, merge it last + if self.local_context_data: + data = deepmerge(data, self.local_context_data) + + return data + + def clean(self): + super().clean() + + # Verify that JSON data is provided as an object + if self.local_context_data and type(self.local_context_data) is not dict: + raise ValidationError( + {'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'} + ) diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 49dd49a42..60dc4d861 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -1,6 +1,5 @@ import json import uuid -from collections import OrderedDict from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericForeignKey @@ -8,22 +7,18 @@ from django.contrib.contenttypes.models import ContentType from django.core.validators import ValidationError from django.db import models from django.http import HttpResponse -from django.urls import reverse from django.utils import timezone from rest_framework.utils.encoders import JSONEncoder from extras.choices import * from extras.constants import * -from extras.querysets import ConfigContextQuerySet from extras.utils import extras_features, FeatureQuery, image_upload -from netbox.models import BigIDModel, ChangeLoggedModel +from netbox.models import BigIDModel from utilities.querysets import RestrictedQuerySet -from utilities.utils import deepmerge, render_jinja2 +from utilities.utils import render_jinja2 __all__ = ( - 'ConfigContext', - 'ConfigContextModel', 'CustomLink', 'ExportTemplate', 'ImageAttachment', @@ -375,151 +370,6 @@ class ImageAttachment(BigIDModel): return None -# -# Config contexts -# - -@extras_features('webhooks') -class ConfigContext(ChangeLoggedModel): - """ - A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned - qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B - will be available to a Device in site A assigned to tenant B. Data is stored in JSON format. - """ - name = models.CharField( - max_length=100, - unique=True - ) - weight = models.PositiveSmallIntegerField( - default=1000 - ) - description = models.CharField( - max_length=200, - blank=True - ) - is_active = models.BooleanField( - default=True, - ) - regions = models.ManyToManyField( - to='dcim.Region', - related_name='+', - blank=True - ) - site_groups = models.ManyToManyField( - to='dcim.SiteGroup', - related_name='+', - blank=True - ) - sites = models.ManyToManyField( - to='dcim.Site', - related_name='+', - blank=True - ) - roles = models.ManyToManyField( - to='dcim.DeviceRole', - related_name='+', - blank=True - ) - platforms = models.ManyToManyField( - to='dcim.Platform', - related_name='+', - blank=True - ) - cluster_groups = models.ManyToManyField( - to='virtualization.ClusterGroup', - related_name='+', - blank=True - ) - clusters = models.ManyToManyField( - to='virtualization.Cluster', - related_name='+', - blank=True - ) - tenant_groups = models.ManyToManyField( - to='tenancy.TenantGroup', - related_name='+', - blank=True - ) - tenants = models.ManyToManyField( - to='tenancy.Tenant', - related_name='+', - blank=True - ) - tags = models.ManyToManyField( - to='extras.Tag', - related_name='+', - blank=True - ) - data = models.JSONField() - - objects = ConfigContextQuerySet.as_manager() - - class Meta: - ordering = ['weight', 'name'] - - def __str__(self): - return self.name - - def get_absolute_url(self): - return reverse('extras:configcontext', kwargs={'pk': self.pk}) - - def clean(self): - super().clean() - - # Verify that JSON data is provided as an object - if type(self.data) is not dict: - raise ValidationError( - {'data': 'JSON data must be in object form. Example: {"foo": 123}'} - ) - - -class ConfigContextModel(models.Model): - """ - A model which includes local configuration context data. This local data will override any inherited data from - ConfigContexts. - """ - local_context_data = models.JSONField( - blank=True, - null=True, - ) - - class Meta: - abstract = True - - def get_config_context(self): - """ - Return the rendered configuration context for a device or VM. - """ - - # Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs - data = OrderedDict() - - if not hasattr(self, 'config_context_data'): - # The annotation is not available, so we fall back to manually querying for the config context objects - config_context_data = ConfigContext.objects.get_for_object(self, aggregate_data=True) - else: - # The attribute may exist, but the annotated value could be None if there is no config context data - config_context_data = self.config_context_data or [] - - for context in config_context_data: - data = deepmerge(data, context) - - # If the object has local config context data defined, merge it last - if self.local_context_data: - data = deepmerge(data, self.local_context_data) - - return data - - def clean(self): - super().clean() - - # Verify that JSON data is provided as an object - if self.local_context_data and type(self.local_context_data) is not dict: - raise ValidationError( - {'local_context_data': 'JSON data must be in object form. Example: {"foo": 123}'} - ) - - # # Custom scripts #