jeremystretch 4 лет назад
Родитель
Сommit
6a07f66cfc

+ 8 - 0
base_requirements.txt

@@ -18,6 +18,10 @@ django-debug-toolbar
 # https://github.com/carltongibson/django-filter
 django-filter
 
+# Django debug toolbar extension with support for GraphiQL
+# https://github.com/flavors/django-graphiql-debug-toolbar/
+django-graphiql-debug-toolbar
+
 # Modified Preorder Tree Traversal (recursive nesting of objects)
 # https://github.com/django-mptt/django-mptt
 django-mptt
@@ -54,6 +58,10 @@ djangorestframework
 # https://github.com/axnsan12/drf-yasg
 drf-yasg[validation]
 
+# Django wrapper for Graphene (GraphQL support)
+# https://github.com/graphql-python/graphene-django
+graphene_django
+
 # WSGI HTTP server
 # https://gunicorn.org/
 gunicorn

+ 21 - 0
netbox/circuits/graphql/schema.py

@@ -0,0 +1,21 @@
+import graphene
+
+from netbox.graphql.fields import ObjectField, ObjectListField
+from .types import *
+
+
+class CircuitsQuery(graphene.ObjectType):
+    circuit = ObjectField(CircuitType)
+    circuits = ObjectListField(CircuitType)
+
+    circuit_termination = ObjectField(CircuitTerminationType)
+    circuit_terminations = ObjectListField(CircuitTerminationType)
+
+    circuit_type = ObjectField(CircuitTypeType)
+    circuit_types = ObjectListField(CircuitTypeType)
+
+    provider = ObjectField(ProviderType)
+    providers = ObjectListField(ProviderType)
+
+    provider_network = ObjectField(ProviderNetworkType)
+    provider_networks = ObjectListField(ProviderNetworkType)

+ 54 - 0
netbox/circuits/graphql/types.py

@@ -0,0 +1,54 @@
+from circuits import filtersets, models
+from netbox.graphql.types import *
+
+__all__ = (
+    'CircuitType',
+    'CircuitTerminationType',
+    'CircuitTypeType',
+    'ProviderType',
+    'ProviderNetworkType',
+)
+
+
+#
+# Object types
+#
+
+class ProviderType(TaggedObjectType):
+
+    class Meta:
+        model = models.Provider
+        fields = '__all__'
+        filterset_class = filtersets.ProviderFilterSet
+
+
+class ProviderNetworkType(TaggedObjectType):
+
+    class Meta:
+        model = models.ProviderNetwork
+        fields = '__all__'
+        filterset_class = filtersets.ProviderNetworkFilterSet
+
+
+class CircuitType(TaggedObjectType):
+
+    class Meta:
+        model = models.Circuit
+        fields = '__all__'
+        filterset_class = filtersets.CircuitFilterSet
+
+
+class CircuitTypeType(ObjectType):
+
+    class Meta:
+        model = models.CircuitType
+        fields = '__all__'
+        filterset_class = filtersets.CircuitTypeFilterSet
+
+
+class CircuitTerminationType(BaseObjectType):
+
+    class Meta:
+        model = models.CircuitTermination
+        fields = '__all__'
+        filterset_class = filtersets.CircuitTerminationFilterSet

+ 11 - 0
netbox/netbox/graphql/__init__.py

@@ -0,0 +1,11 @@
+import graphene
+from graphene_django.converter import convert_django_field
+from taggit.managers import TaggableManager
+
+
+@convert_django_field.register(TaggableManager)
+def convert_field_to_tags_list(field, registry=None):
+    """
+    Register conversion handler for django-taggit's TaggableManager
+    """
+    return graphene.List(graphene.String)

+ 65 - 0
netbox/netbox/graphql/fields.py

@@ -0,0 +1,65 @@
+from functools import partial
+
+import graphene
+from graphene_django import DjangoListField
+
+from .utils import get_graphene_type
+
+__all__ = (
+    'ObjectField',
+    'ObjectListField',
+)
+
+
+class ObjectField(graphene.Field):
+    """
+    Retrieve a single object, identified by its numeric ID.
+    """
+    def __init__(self, *args, **kwargs):
+
+        if 'id' not in kwargs:
+            kwargs['id'] = graphene.Int(required=True)
+
+        super().__init__(*args, **kwargs)
+
+    @staticmethod
+    def object_resolver(django_object_type, root, info, **args):
+        """
+        Return an object given its numeric ID.
+        """
+        manager = django_object_type._meta.model._default_manager
+        queryset = django_object_type.get_queryset(manager, info)
+
+        return queryset.get(**args)
+
+    def get_resolver(self, parent_resolver):
+        return partial(self.object_resolver, self._type)
+
+
+class ObjectListField(DjangoListField):
+    """
+    Retrieve a list of objects, optionally filtered by one or more FilterSet filters.
+    """
+    def __init__(self, _type, *args, **kwargs):
+
+        assert hasattr(_type._meta, 'filterset_class'), "DjangoFilterListField must define filterset_class under Meta"
+        filterset_class = _type._meta.filterset_class
+
+        # Get FilterSet kwargs
+        filter_kwargs = {}
+        for filter_name, filter_field in filterset_class.get_filters().items():
+            field_type = get_graphene_type(type(filter_field))
+            filter_kwargs[filter_name] = graphene.Argument(field_type)
+
+        super().__init__(_type, args=filter_kwargs, *args, **kwargs)
+
+    @staticmethod
+    def list_resolver(django_object_type, resolver, default_manager, root, info, **args):
+        # Get the QuerySet from the object type
+        queryset = django_object_type.get_queryset(default_manager, info)
+
+        # Instantiate and apply the FilterSet
+        filterset_class = django_object_type._meta.filterset_class
+        filterset = filterset_class(data=args, queryset=queryset, request=info.context)
+
+        return filterset.qs

+ 13 - 0
netbox/netbox/graphql/schema.py

@@ -0,0 +1,13 @@
+import graphene
+
+from circuits.graphql.schema import CircuitsQuery
+
+
+class Query(
+    CircuitsQuery,
+    graphene.ObjectType
+):
+    pass
+
+
+schema = graphene.Schema(query=Query, auto_camelcase=False)

+ 41 - 0
netbox/netbox/graphql/types.py

@@ -0,0 +1,41 @@
+import graphene
+from graphene_django import DjangoObjectType
+
+__all__ = (
+    'BaseObjectType',
+    'ObjectType',
+    'TaggedObjectType',
+)
+
+
+class BaseObjectType(DjangoObjectType):
+    """
+    Base GraphQL object type for all NetBox objects
+    """
+    class Meta:
+        abstract = True
+
+    @classmethod
+    def get_queryset(cls, queryset, info):
+        # Enforce object permissions on the queryset
+        return queryset.restrict(info.context.user, 'view')
+
+
+class ObjectType(BaseObjectType):
+    # TODO: Custom fields support
+
+    class Meta:
+        abstract = True
+
+
+class TaggedObjectType(ObjectType):
+    """
+    Extends ObjectType with support for Tags
+    """
+    tags = graphene.List(graphene.String)
+
+    class Meta:
+        abstract = True
+
+    def resolve_tags(self, info):
+        return self.tags.all()

+ 25 - 0
netbox/netbox/graphql/utils.py

@@ -0,0 +1,25 @@
+import graphene
+from django_filters import filters
+
+
+def get_graphene_type(filter_cls):
+    """
+    Return the appropriate Graphene scalar type for a django_filters Filter
+    """
+    if issubclass(filter_cls, filters.BooleanFilter):
+        field_type = graphene.Boolean
+    elif issubclass(filter_cls, filters.NumberFilter):
+        # TODO: Floats? BigInts?
+        field_type = graphene.Int
+    elif issubclass(filter_cls, filters.DateFilter):
+        field_type = graphene.Date
+    elif issubclass(filter_cls, filters.DateTimeFilter):
+        field_type = graphene.DateTime
+    else:
+        field_type = graphene.String
+
+    # Multi-value filters should be handled as lists
+    if issubclass(filter_cls, filters.MultipleChoiceFilter):
+        return graphene.List(field_type)
+
+    return field_type

+ 3 - 1
netbox/netbox/settings.py

@@ -282,9 +282,11 @@ INSTALLED_APPS = [
     'cacheops',
     'corsheaders',
     'debug_toolbar',
+    'graphiql_debug_toolbar',
     'django_filters',
     'django_tables2',
     'django_prometheus',
+    'graphene_django',
     'mptt',
     'rest_framework',
     'taggit',
@@ -303,7 +305,7 @@ INSTALLED_APPS = [
 
 # Middleware
 MIDDLEWARE = [
-    'debug_toolbar.middleware.DebugToolbarMiddleware',
+    'graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
     'django_prometheus.middleware.PrometheusBeforeMiddleware',
     'corsheaders.middleware.CorsMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',

+ 5 - 0
netbox/netbox/urls.py

@@ -4,9 +4,11 @@ from django.urls import path, re_path
 from django.views.static import serve
 from drf_yasg import openapi
 from drf_yasg.views import get_schema_view
+from graphene_django.views import GraphQLView
 
 from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
 from netbox.api.views import APIRootView, StatusView
+from netbox.graphql.schema import schema
 from netbox.views import HomeView, StaticMediaFailureView, SearchView
 from users.views import LoginView, LogoutView
 from .admin import admin_site
@@ -60,6 +62,9 @@ _patterns = [
     path('api/redoc/', schema_view.with_ui('redoc'), name='api_redocs'),
     re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(), name='schema_swagger'),
 
+    # GraphQL
+    path('graphql/', GraphQLView.as_view(graphiql=True, schema=schema)),
+
     # Serving static media in Django to pipe it through LoginRequiredMiddleware
     path('media/<path:path>', serve, {'document_root': settings.MEDIA_ROOT}),
     path('media-failure/', StaticMediaFailureView.as_view(), name='media_failure'),

+ 1 - 0
netbox/project-static/volt

@@ -0,0 +1 @@
+Subproject commit 942aa8c7bd506fb88b7c669cab173bc319eca309

+ 2 - 0
requirements.txt

@@ -3,6 +3,7 @@ django-cacheops==6.0
 django-cors-headers==3.7.0
 django-debug-toolbar==3.2.1
 django-filter==2.4.0
+django-graphiql-debug-toolbar==0.1.4
 django-mptt==0.12.0
 django-pglocks==1.0.4
 django-prometheus==2.1.0
@@ -12,6 +13,7 @@ django-taggit==1.4.0
 django-timezone-field==4.1.2
 djangorestframework==3.12.4
 drf-yasg[validation]==1.20.0
+graphene_django==2.15.0
 gunicorn==20.1.0
 Jinja2==3.0.1
 Markdown==3.3.4