1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge branch 'develop' into develop-2.8

This commit is contained in:
John Anderson
2020-03-15 00:18:32 -04:00
36 changed files with 464 additions and 230 deletions

View File

@@ -13,6 +13,7 @@ from extras.constants import *
from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
)
from extras.utils import FeatureQuerySet
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer
@@ -31,7 +32,7 @@ from .nested_serializers import *
class GraphSerializer(ValidatedModelSerializer):
type = ContentTypeField(
queryset=ContentType.objects.filter(GRAPH_MODELS),
queryset=ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()),
)
class Meta:
@@ -67,7 +68,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer):
content_type = ContentTypeField(
queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS),
queryset=ContentType.objects.filter(FeatureQuerySet('export_templates').get_queryset()),
)
template_language = ChoiceField(
choices=TemplateLanguageChoices,

View File

@@ -1,129 +1,3 @@
from django.db.models import Q
# Models which support custom fields
CUSTOMFIELD_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'device',
'devicetype',
'powerfeed',
'rack',
'site',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Custom links
CUSTOMLINK_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'cable',
'device',
'devicetype',
'powerpanel',
'powerfeed',
'rack',
'site',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Models which can have Graphs associated with them
GRAPH_MODELS = Q(
Q(app_label='circuits', model__in=[
'provider',
]) |
Q(app_label='dcim', model__in=[
'device',
'interface',
'site',
])
)
# Models which support export templates
EXPORTTEMPLATE_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'cable',
'consoleport',
'device',
'devicetype',
'interface',
'inventoryitem',
'manufacturer',
'powerpanel',
'powerport',
'powerfeed',
'rack',
'rackgroup',
'region',
'site',
'virtualchassis',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Report logging levels
LOG_DEFAULT = 0
LOG_SUCCESS = 10
@@ -138,51 +12,14 @@ LOG_LEVEL_CODES = {
LOG_FAILURE: 'failure',
}
# Webhook content types
HTTP_CONTENT_TYPE_JSON = 'application/json'
# Models which support registered webhooks
WEBHOOK_MODELS = Q(
Q(app_label='circuits', model__in=[
'circuit',
'provider',
]) |
Q(app_label='dcim', model__in=[
'cable',
'consoleport',
'consoleserverport',
'device',
'devicebay',
'devicetype',
'frontport',
'interface',
'inventoryitem',
'manufacturer',
'poweroutlet',
'powerpanel',
'powerport',
'powerfeed',
'rack',
'rearport',
'region',
'site',
'virtualchassis',
]) |
Q(app_label='ipam', model__in=[
'aggregate',
'ipaddress',
'prefix',
'service',
'vlan',
'vrf',
]) |
Q(app_label='secrets', model__in=[
'secret',
]) |
Q(app_label='tenancy', model__in=[
'tenant',
]) |
Q(app_label='virtualization', model__in=[
'cluster',
'virtualmachine',
])
)
# Registerable extras features
EXTRAS_FEATURES = [
'custom_fields',
'custom_links',
'graphs',
'export_templates',
'webhooks'
]

View File

@@ -0,0 +1,40 @@
# Generated by Django 2.2.11 on 2020-03-14 06:50
from django.db import migrations, models
import django.db.models.deletion
import extras.utils
class Migration(migrations.Migration):
dependencies = [
('extras', '0038_webhook_template_support'),
]
operations = [
migrations.AlterField(
model_name='customfield',
name='obj_type',
field=models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuerySet('custom_fields'), related_name='custom_fields', to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='customlink',
name='content_type',
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuerySet('custom_links'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='exporttemplate',
name='content_type',
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuerySet('export_templates'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='graph',
name='type',
field=models.ForeignKey(limit_choices_to=extras.utils.FeatureQuerySet('graphs'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType'),
),
migrations.AlterField(
model_name='webhook',
name='obj_type',
field=models.ManyToManyField(limit_choices_to=extras.utils.FeatureQuerySet('webhooks'), related_name='webhooks', to='contenttypes.ContentType'),
),
]

View File

@@ -22,6 +22,7 @@ from utilities.utils import deepmerge, render_jinja2
from .choices import *
from .constants import *
from .querysets import ConfigContextQuerySet
from .utils import FeatureQuerySet
__all__ = (
@@ -58,7 +59,7 @@ class Webhook(models.Model):
to=ContentType,
related_name='webhooks',
verbose_name='Object types',
limit_choices_to=WEBHOOK_MODELS,
limit_choices_to=FeatureQuerySet('webhooks'),
help_text="The object(s) to which this Webhook applies."
)
name = models.CharField(
@@ -223,7 +224,7 @@ class CustomField(models.Model):
to=ContentType,
related_name='custom_fields',
verbose_name='Object(s)',
limit_choices_to=CUSTOMFIELD_MODELS,
limit_choices_to=FeatureQuerySet('custom_fields'),
help_text='The object(s) to which this field applies.'
)
type = models.CharField(
@@ -470,7 +471,7 @@ class CustomLink(models.Model):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=CUSTOMLINK_MODELS
limit_choices_to=FeatureQuerySet('custom_links')
)
name = models.CharField(
max_length=100,
@@ -518,7 +519,7 @@ class Graph(models.Model):
type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=GRAPH_MODELS
limit_choices_to=FeatureQuerySet('graphs')
)
weight = models.PositiveSmallIntegerField(
default=1000
@@ -579,7 +580,7 @@ class ExportTemplate(models.Model):
content_type = models.ForeignKey(
to=ContentType,
on_delete=models.CASCADE,
limit_choices_to=EXPORTTEMPLATE_MODELS
limit_choices_to=FeatureQuerySet('export_templates')
)
name = models.CharField(
max_length=100

View File

@@ -9,6 +9,7 @@ from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform,
from extras.api.views import ScriptViewSet
from extras.models import ConfigContext, Graph, ExportTemplate, Tag
from extras.scripts import BooleanVar, IntegerVar, Script, StringVar
from extras.utils import FeatureQuerySet
from tenancy.models import Tenant, TenantGroup
from utilities.testing import APITestCase

View File

@@ -3,8 +3,8 @@ from django.test import TestCase
from dcim.models import DeviceRole, Platform, Region, Site
from extras.choices import *
from extras.constants import GRAPH_MODELS
from extras.filters import *
from extras.utils import FeatureQuerySet
from extras.models import ConfigContext, ExportTemplate, Graph
from tenancy.models import Tenant, TenantGroup
from virtualization.models import Cluster, ClusterGroup, ClusterType
@@ -18,7 +18,7 @@ class GraphTestCase(TestCase):
def setUpTestData(cls):
# Get the first three available types
content_types = ContentType.objects.filter(GRAPH_MODELS)[:3]
content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset())[:3]
graphs = (
Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
@@ -32,7 +32,7 @@ class GraphTestCase(TestCase):
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self):
content_type = ContentType.objects.filter(GRAPH_MODELS).first()
content_type = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()).first()
params = {'type': content_type.pk}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)

View File

@@ -1,6 +1,12 @@
import collections
from django.db.models import Q
from django.utils.deconstruct import deconstructible
from taggit.managers import _TaggableManager
from utilities.querysets import DummyQuerySet
from extras.constants import EXTRAS_FEATURES
def is_taggable(obj):
"""
@@ -13,3 +19,65 @@ def is_taggable(obj):
if isinstance(obj.tags, DummyQuerySet):
return True
return False
#
# Dynamic feature registration
#
class Registry:
"""
The registry is a place to hook into for data storage across components
"""
def add_store(self, store_name, initial_value=None):
"""
Given the name of some new data parameter and an optional initial value, setup the registry store
"""
if not hasattr(Registry, store_name):
setattr(Registry, store_name, initial_value)
registry = Registry()
@deconstructible
class FeatureQuerySet:
"""
Helper class that delays evaluation of the registry contents for the functionaility store
until it has been populated.
"""
def __init__(self, feature):
self.feature = feature
def __call__(self):
return self.get_queryset()
def get_queryset(self):
"""
Given an extras feature, return a Q object for content type lookup
"""
query = Q()
for app_label, models in registry.model_feature_store[self.feature].items():
query |= Q(app_label=app_label, model__in=models)
return query
registry.add_store('model_feature_store', {f: collections.defaultdict(list) for f in EXTRAS_FEATURES})
def extras_features(*features):
"""
Decorator used to register extras provided features to a model
"""
def wrapper(model_class):
for feature in features:
if feature in EXTRAS_FEATURES:
app_label, model_name = model_class._meta.label_lower.split('.')
registry.model_feature_store[feature][app_label].append(model_name)
else:
raise ValueError('{} is not a valid extras feature!'.format(feature))
return model_class
return wrapper

View File

@@ -8,6 +8,7 @@ from extras.models import Webhook
from utilities.api import get_serializer_for_model
from .choices import *
from .constants import *
from .utils import FeatureQuerySet
def generate_signature(request_body, secret):
@@ -29,7 +30,7 @@ def enqueue_webhooks(instance, user, request_id, action):
"""
obj_type = ContentType.objects.get_for_model(instance.__class__)
webhook_models = ContentType.objects.filter(WEBHOOK_MODELS)
webhook_models = ContentType.objects.filter(FeatureQuerySet('webhooks').get_queryset())
if obj_type not in webhook_models:
return