Merge pull request #8562 from netbox-community/8405-plugins-graphql

Closes #8405: GraphQL support for plugins
This commit is contained in:
Jeremy Stretch 2022-02-07 13:08:32 -05:00 committed by GitHub
commit 0e827b6ae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 201 additions and 87 deletions

View File

@ -0,0 +1,59 @@
# GraphQL API
## Defining the Schema Class
A plugin can extend NetBox's GraphQL API by registering its own schema class. By default, NetBox will attempt to import `graphql.schema` from the plugin, if it exists. This path can be overridden by defining `graphql_schema` on the PluginConfig instance as the dotted path to the desired Python class. This class must be a subclass of `graphene.ObjectType`.
### Example
```python
# graphql.py
import graphene
from netbox.graphql.fields import ObjectField, ObjectListField
from . import filtersets, models
class MyModelType(graphene.ObjectType):
class Meta:
model = models.MyModel
fields = '__all__'
filterset_class = filtersets.MyModelFilterSet
class MyQuery(graphene.ObjectType):
mymodel = ObjectField(MyModelType)
mymodel_list = ObjectListField(MyModelType)
schema = MyQuery
```
## GraphQL Objects
NetBox provides two object type classes for use by plugins.
::: netbox.graphql.types.BaseObjectType
selection:
members: false
rendering:
show_source: false
::: netbox.graphql.types.NetBoxObjectType
selection:
members: false
rendering:
show_source: false
## GraphQL Fields
NetBox provides two field classes for use by plugins.
::: netbox.graphql.fields.ObjectField
selection:
members: false
rendering:
show_source: false
::: netbox.graphql.fields.ObjectListField
selection:
members: false
rendering:
show_source: false

View File

@ -22,7 +22,7 @@ However, keep in mind that each piece of functionality is entirely optional. For
### Plugin Structure ### Plugin Structure
Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin looks something like this: Although the specific structure of a plugin is largely left to the discretion of its authors, a typical NetBox plugin might look something like this:
```no-highlight ```no-highlight
project-name/ project-name/
@ -102,23 +102,24 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
#### PluginConfig Attributes #### PluginConfig Attributes
| Name | Description | | Name | Description |
| ---- |---------------------------------------------------------------------------------------------------------------| |-----------------------|--------------------------------------------------------------------------------------------------------------------------|
| `name` | Raw plugin name; same as the plugin's source directory | | `name` | Raw plugin name; same as the plugin's source directory |
| `verbose_name` | Human-friendly name for the plugin | | `verbose_name` | Human-friendly name for the plugin |
| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) | | `version` | Current release ([semantic versioning](https://semver.org/) is encouraged) |
| `description` | Brief description of the plugin's purpose | | `description` | Brief description of the plugin's purpose |
| `author` | Name of plugin's author | | `author` | Name of plugin's author |
| `author_email` | Author's public email address | | `author_email` | Author's public email address |
| `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. | | `base_url` | Base path to use for plugin URLs (optional). If not specified, the project's `name` will be used. |
| `required_settings` | A list of any configuration parameters that **must** be defined by the user | | `required_settings` | A list of any configuration parameters that **must** be defined by the user |
| `default_settings` | A dictionary of configuration parameters and their default values | | `default_settings` | A dictionary of configuration parameters and their default values |
| `min_version` | Minimum version of NetBox with which the plugin is compatible | | `min_version` | Minimum version of NetBox with which the plugin is compatible |
| `max_version` | Maximum version of NetBox with which the plugin is compatible | | `max_version` | Maximum version of NetBox with which the plugin is compatible |
| `middleware` | A list of middleware classes to append after NetBox's build-in middleware | | `middleware` | A list of middleware classes to append after NetBox's build-in middleware |
| `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | | `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) |
| `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | | `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) | | `graphql_schema` | The dotted path to the plugin's GraphQL schema class, if any (default: `graphql.schema`) |
| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored.

View File

@ -108,6 +108,7 @@ nav:
- Forms: 'plugins/development/forms.md' - Forms: 'plugins/development/forms.md'
- Filter Sets: 'plugins/development/filtersets.md' - Filter Sets: 'plugins/development/filtersets.md'
- REST API: 'plugins/development/rest-api.md' - REST API: 'plugins/development/rest-api.md'
- GraphQL API: 'plugins/development/graphql.md'
- Background Tasks: 'plugins/development/background-tasks.md' - Background Tasks: 'plugins/development/background-tasks.md'
- Administration: - Administration:
- Authentication: 'administration/authentication.md' - Authentication: 'administration/authentication.md'

View File

@ -1,5 +1,5 @@
from circuits import filtersets, models from circuits import filtersets, models
from netbox.graphql.types import ObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'CircuitTerminationType', 'CircuitTerminationType',
@ -18,7 +18,7 @@ class CircuitTerminationType(ObjectType):
filterset_class = filtersets.CircuitTerminationFilterSet filterset_class = filtersets.CircuitTerminationFilterSet
class CircuitType(PrimaryObjectType): class CircuitType(NetBoxObjectType):
class Meta: class Meta:
model = models.Circuit model = models.Circuit
@ -34,7 +34,7 @@ class CircuitTypeType(OrganizationalObjectType):
filterset_class = filtersets.CircuitTypeFilterSet filterset_class = filtersets.CircuitTypeFilterSet
class ProviderType(PrimaryObjectType): class ProviderType(NetBoxObjectType):
class Meta: class Meta:
model = models.Provider model = models.Provider
@ -42,7 +42,7 @@ class ProviderType(PrimaryObjectType):
filterset_class = filtersets.ProviderFilterSet filterset_class = filtersets.ProviderFilterSet
class ProviderNetworkType(PrimaryObjectType): class ProviderNetworkType(NetBoxObjectType):
class Meta: class Meta:
model = models.ProviderNetwork model = models.ProviderNetwork

View File

@ -6,7 +6,7 @@ from extras.graphql.mixins import (
) )
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'CableType', 'CableType',
@ -85,7 +85,7 @@ class ComponentTemplateObjectType(
# Model types # Model types
# #
class CableType(PrimaryObjectType): class CableType(NetBoxObjectType):
class Meta: class Meta:
model = models.Cable model = models.Cable
@ -143,7 +143,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, PrimaryObjectType): class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Device model = models.Device
@ -189,7 +189,7 @@ class DeviceRoleType(OrganizationalObjectType):
filterset_class = filtersets.DeviceRoleFilterSet filterset_class = filtersets.DeviceRoleFilterSet
class DeviceTypeType(PrimaryObjectType): class DeviceTypeType(NetBoxObjectType):
class Meta: class Meta:
model = models.DeviceType model = models.DeviceType
@ -300,7 +300,7 @@ class ModuleBayTemplateType(ComponentTemplateObjectType):
filterset_class = filtersets.ModuleBayTemplateFilterSet filterset_class = filtersets.ModuleBayTemplateFilterSet
class ModuleTypeType(PrimaryObjectType): class ModuleTypeType(NetBoxObjectType):
class Meta: class Meta:
model = models.ModuleType model = models.ModuleType
@ -316,7 +316,7 @@ class PlatformType(OrganizationalObjectType):
filterset_class = filtersets.PlatformFilterSet filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(PrimaryObjectType): class PowerFeedType(NetBoxObjectType):
class Meta: class Meta:
model = models.PowerFeed model = models.PowerFeed
@ -352,7 +352,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class PowerPanelType(PrimaryObjectType): class PowerPanelType(NetBoxObjectType):
class Meta: class Meta:
model = models.PowerPanel model = models.PowerPanel
@ -382,7 +382,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
return self.type or None return self.type or None
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Rack model = models.Rack
@ -396,7 +396,7 @@ class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
return self.outer_unit or None return self.outer_unit or None
class RackReservationType(PrimaryObjectType): class RackReservationType(NetBoxObjectType):
class Meta: class Meta:
model = models.RackReservation model = models.RackReservation
@ -436,7 +436,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
filterset_class = filtersets.RegionFilterSet filterset_class = filtersets.RegionFilterSet
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType): class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
asn = graphene.Field(BigInt) asn = graphene.Field(BigInt)
class Meta: class Meta:
@ -453,7 +453,7 @@ class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
filterset_class = filtersets.SiteGroupFilterSet filterset_class = filtersets.SiteGroupFilterSet
class VirtualChassisType(PrimaryObjectType): class VirtualChassisType(NetBoxObjectType):
class Meta: class Meta:
model = models.VirtualChassis model = models.VirtualChassis

View File

@ -12,10 +12,13 @@ from utilities.choices import ButtonColorChoices
from extras.plugins.utils import import_object from extras.plugins.utils import import_object
# Initialize plugin registry stores # Initialize plugin registry
registry['plugin_template_extensions'] = collections.defaultdict(list) registry['plugins'] = {
registry['plugin_menu_items'] = {} 'graphql_schemas': [],
registry['plugin_preferences'] = {} 'menu_items': {},
'preferences': {},
'template_extensions': collections.defaultdict(list),
}
# #
@ -53,13 +56,15 @@ class PluginConfig(AppConfig):
# Default integration paths. Plugin authors can override these to customize the paths to # Default integration paths. Plugin authors can override these to customize the paths to
# integrated components. # integrated components.
template_extensions = 'template_content.template_extensions' graphql_schema = 'graphql.schema'
menu_items = 'navigation.menu_items' menu_items = 'navigation.menu_items'
template_extensions = 'template_content.template_extensions'
user_preferences = 'preferences.preferences' user_preferences = 'preferences.preferences'
def ready(self): def ready(self):
plugin_name = self.name.rsplit('.', 1)[1]
# Register template content # Register template content (if defined)
template_extensions = import_object(f"{self.__module__}.{self.template_extensions}") template_extensions = import_object(f"{self.__module__}.{self.template_extensions}")
if template_extensions is not None: if template_extensions is not None:
register_template_extensions(template_extensions) register_template_extensions(template_extensions)
@ -69,10 +74,14 @@ class PluginConfig(AppConfig):
if menu_items is not None: if menu_items is not None:
register_menu_items(self.verbose_name, menu_items) register_menu_items(self.verbose_name, menu_items)
# Register user preferences # Register GraphQL schema (if defined)
graphql_schema = import_object(f"{self.__module__}.{self.graphql_schema}")
if graphql_schema is not None:
register_graphql_schema(graphql_schema)
# Register user preferences (if defined)
user_preferences = import_object(f"{self.__module__}.{self.user_preferences}") user_preferences = import_object(f"{self.__module__}.{self.user_preferences}")
if user_preferences is not None: if user_preferences is not None:
plugin_name = self.name.rsplit('.', 1)[1]
register_user_preferences(plugin_name, user_preferences) register_user_preferences(plugin_name, user_preferences)
@classmethod @classmethod
@ -178,13 +187,13 @@ def register_template_extensions(class_list):
# Validation # Validation
for template_extension in class_list: for template_extension in class_list:
if not inspect.isclass(template_extension): if not inspect.isclass(template_extension):
raise TypeError(f"PluginTemplateExtension class {template_extension} was passes as an instance!") raise TypeError(f"PluginTemplateExtension class {template_extension} was passed as an instance!")
if not issubclass(template_extension, PluginTemplateExtension): if not issubclass(template_extension, PluginTemplateExtension):
raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!") raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
if template_extension.model is None: if template_extension.model is None:
raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!") raise TypeError(f"PluginTemplateExtension class {template_extension} does not define a valid model!")
registry['plugin_template_extensions'][template_extension.model].append(template_extension) registry['plugins']['template_extensions'][template_extension.model].append(template_extension)
# #
@ -249,7 +258,18 @@ def register_menu_items(section_name, class_list):
if not isinstance(button, PluginMenuButton): if not isinstance(button, PluginMenuButton):
raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton") raise TypeError(f"{button} must be an instance of extras.plugins.PluginMenuButton")
registry['plugin_menu_items'][section_name] = class_list registry['plugins']['menu_items'][section_name] = class_list
#
# GraphQL schemas
#
def register_graphql_schema(graphql_schema):
"""
Register a GraphQL schema class for inclusion in NetBox's GraphQL API.
"""
registry['plugins']['graphql_schemas'].append(graphql_schema)
# #
@ -260,4 +280,4 @@ def register_user_preferences(plugin_name, preferences):
""" """
Register a list of user preferences defined by a plugin. Register a list of user preferences defined by a plugin.
""" """
registry['plugin_preferences'][plugin_name] = preferences registry['plugins']['preferences'][plugin_name] = preferences

View File

@ -23,7 +23,7 @@ def _get_registered_content(obj, method, template_context):
} }
model_name = obj._meta.label_lower model_name = obj._meta.label_lower
template_extensions = registry['plugin_template_extensions'].get(model_name, []) template_extensions = registry['plugins']['template_extensions'].get(model_name, [])
for template_extension in template_extensions: for template_extension in template_extensions:
# If the class has not overridden the specified method, we can skip it (because we know it # If the class has not overridden the specified method, we can skip it (because we know it

View File

@ -0,0 +1,21 @@
import graphene
from graphene_django import DjangoObjectType
from netbox.graphql.fields import ObjectField, ObjectListField
from . import models
class DummyModelType(DjangoObjectType):
class Meta:
model = models.DummyModel
fields = '__all__'
class DummyQuery(graphene.ObjectType):
dummymodel = ObjectField(DummyModelType)
dummymodel_list = ObjectListField(DummyModelType)
schema = DummyQuery

View File

@ -7,6 +7,7 @@ from django.urls import reverse
from extras.registry import registry from extras.registry import registry
from extras.tests.dummy_plugin import config as dummy_config from extras.tests.dummy_plugin import config as dummy_config
from netbox.graphql.schema import Query
@skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS") @skipIf('extras.tests.dummy_plugin' not in settings.PLUGINS, "dummy_plugin not in settings.PLUGINS")
@ -61,8 +62,8 @@ class PluginTest(TestCase):
""" """
Check that plugin MenuItems and MenuButtons are registered. Check that plugin MenuItems and MenuButtons are registered.
""" """
self.assertIn('Dummy plugin', registry['plugin_menu_items']) self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
menu_items = registry['plugin_menu_items']['Dummy plugin'] menu_items = registry['plugins']['menu_items']['Dummy plugin']
self.assertEqual(len(menu_items), 2) self.assertEqual(len(menu_items), 2)
self.assertEqual(len(menu_items[0].buttons), 2) self.assertEqual(len(menu_items[0].buttons), 2)
@ -72,14 +73,14 @@ class PluginTest(TestCase):
""" """
from extras.tests.dummy_plugin.template_content import SiteContent from extras.tests.dummy_plugin.template_content import SiteContent
self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site']) self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
def test_user_preferences(self): def test_user_preferences(self):
""" """
Check that plugin UserPreferences are registered. Check that plugin UserPreferences are registered.
""" """
self.assertIn('dummy_plugin', registry['plugin_preferences']) self.assertIn('dummy_plugin', registry['plugins']['preferences'])
user_preferences = registry['plugin_preferences']['dummy_plugin'] user_preferences = registry['plugins']['preferences']['dummy_plugin']
self.assertEqual(type(user_preferences), dict) self.assertEqual(type(user_preferences), dict)
self.assertEqual(list(user_preferences.keys()), ['pref1', 'pref2']) self.assertEqual(list(user_preferences.keys()), ['pref1', 'pref2'])
@ -143,3 +144,12 @@ class PluginTest(TestCase):
user_config = {'bar': 456} user_config = {'bar': 456}
DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION) DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
self.assertEqual(user_config['bar'], 456) self.assertEqual(user_config['bar'], 456)
def test_graphql(self):
"""
Validate the registration and operation of plugin-provided GraphQL schemas.
"""
from extras.tests.dummy_plugin.graphql import DummyQuery
self.assertIn(DummyQuery, registry['plugins']['graphql_schemas'])
self.assertTrue(issubclass(Query, DummyQuery))

View File

@ -2,7 +2,7 @@ import graphene
from ipam import filtersets, models from ipam import filtersets, models
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'ASNType', 'ASNType',
@ -23,7 +23,7 @@ __all__ = (
) )
class ASNType(PrimaryObjectType): class ASNType(NetBoxObjectType):
asn = graphene.Field(BigInt) asn = graphene.Field(BigInt)
class Meta: class Meta:
@ -32,7 +32,7 @@ class ASNType(PrimaryObjectType):
filterset_class = filtersets.ASNFilterSet filterset_class = filtersets.ASNFilterSet
class AggregateType(PrimaryObjectType): class AggregateType(NetBoxObjectType):
class Meta: class Meta:
model = models.Aggregate model = models.Aggregate
@ -40,7 +40,7 @@ class AggregateType(PrimaryObjectType):
filterset_class = filtersets.AggregateFilterSet filterset_class = filtersets.AggregateFilterSet
class FHRPGroupType(PrimaryObjectType): class FHRPGroupType(NetBoxObjectType):
class Meta: class Meta:
model = models.FHRPGroup model = models.FHRPGroup
@ -59,7 +59,7 @@ class FHRPGroupAssignmentType(BaseObjectType):
filterset_class = filtersets.FHRPGroupAssignmentFilterSet filterset_class = filtersets.FHRPGroupAssignmentFilterSet
class IPAddressType(PrimaryObjectType): class IPAddressType(NetBoxObjectType):
class Meta: class Meta:
model = models.IPAddress model = models.IPAddress
@ -70,7 +70,7 @@ class IPAddressType(PrimaryObjectType):
return self.role or None return self.role or None
class IPRangeType(PrimaryObjectType): class IPRangeType(NetBoxObjectType):
class Meta: class Meta:
model = models.IPRange model = models.IPRange
@ -81,7 +81,7 @@ class IPRangeType(PrimaryObjectType):
return self.role or None return self.role or None
class PrefixType(PrimaryObjectType): class PrefixType(NetBoxObjectType):
class Meta: class Meta:
model = models.Prefix model = models.Prefix
@ -105,7 +105,7 @@ class RoleType(OrganizationalObjectType):
filterset_class = filtersets.RoleFilterSet filterset_class = filtersets.RoleFilterSet
class RouteTargetType(PrimaryObjectType): class RouteTargetType(NetBoxObjectType):
class Meta: class Meta:
model = models.RouteTarget model = models.RouteTarget
@ -113,7 +113,7 @@ class RouteTargetType(PrimaryObjectType):
filterset_class = filtersets.RouteTargetFilterSet filterset_class = filtersets.RouteTargetFilterSet
class ServiceType(PrimaryObjectType): class ServiceType(NetBoxObjectType):
class Meta: class Meta:
model = models.Service model = models.Service
@ -121,7 +121,7 @@ class ServiceType(PrimaryObjectType):
filterset_class = filtersets.ServiceFilterSet filterset_class = filtersets.ServiceFilterSet
class ServiceTemplateType(PrimaryObjectType): class ServiceTemplateType(NetBoxObjectType):
class Meta: class Meta:
model = models.ServiceTemplate model = models.ServiceTemplate
@ -129,7 +129,7 @@ class ServiceTemplateType(PrimaryObjectType):
filterset_class = filtersets.ServiceTemplateFilterSet filterset_class = filtersets.ServiceTemplateFilterSet
class VLANType(PrimaryObjectType): class VLANType(NetBoxObjectType):
class Meta: class Meta:
model = models.VLAN model = models.VLAN
@ -145,7 +145,7 @@ class VLANGroupType(OrganizationalObjectType):
filterset_class = filtersets.VLANGroupFilterSet filterset_class = filtersets.VLANGroupFilterSet
class VRFType(PrimaryObjectType): class VRFType(NetBoxObjectType):
class Meta: class Meta:
model = models.VRF model = models.VRF

View File

@ -41,15 +41,14 @@ class ObjectListField(DjangoListField):
Retrieve a list of objects, optionally filtered by one or more FilterSet filters. Retrieve a list of objects, optionally filtered by one or more FilterSet filters.
""" """
def __init__(self, _type, *args, **kwargs): def __init__(self, _type, *args, **kwargs):
filter_kwargs = {}
assert hasattr(_type._meta, 'filterset_class'), "DjangoFilterListField must define filterset_class under Meta"
filterset_class = _type._meta.filterset_class
# Get FilterSet kwargs # Get FilterSet kwargs
filter_kwargs = {} filterset_class = getattr(_type._meta, 'filterset_class', None)
for filter_name, filter_field in filterset_class.get_filters().items(): if filterset_class:
field_type = get_graphene_type(type(filter_field)) for filter_name, filter_field in filterset_class.get_filters().items():
filter_kwargs[filter_name] = graphene.Argument(field_type) field_type = get_graphene_type(type(filter_field))
filter_kwargs[filter_name] = graphene.Argument(field_type)
super().__init__(_type, args=filter_kwargs, *args, **kwargs) super().__init__(_type, args=filter_kwargs, *args, **kwargs)

View File

@ -3,6 +3,7 @@ import graphene
from circuits.graphql.schema import CircuitsQuery from circuits.graphql.schema import CircuitsQuery
from dcim.graphql.schema import DCIMQuery from dcim.graphql.schema import DCIMQuery
from extras.graphql.schema import ExtrasQuery from extras.graphql.schema import ExtrasQuery
from extras.registry import registry
from ipam.graphql.schema import IPAMQuery from ipam.graphql.schema import IPAMQuery
from tenancy.graphql.schema import TenancyQuery from tenancy.graphql.schema import TenancyQuery
from users.graphql.schema import UsersQuery from users.graphql.schema import UsersQuery
@ -19,6 +20,7 @@ class Query(
UsersQuery, UsersQuery,
VirtualizationQuery, VirtualizationQuery,
WirelessQuery, WirelessQuery,
*registry['plugins']['graphql_schemas'], # Append plugin schemas
graphene.ObjectType graphene.ObjectType
): ):
pass pass

View File

@ -5,8 +5,9 @@ from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntr
__all__ = ( __all__ = (
'BaseObjectType', 'BaseObjectType',
'ObjectType',
'OrganizationalObjectType', 'OrganizationalObjectType',
'PrimaryObjectType', 'NetBoxObjectType',
) )
@ -16,7 +17,7 @@ __all__ = (
class BaseObjectType(DjangoObjectType): class BaseObjectType(DjangoObjectType):
""" """
Base GraphQL object type for all NetBox objects Base GraphQL object type for all NetBox objects. Restricts the model queryset to enforce object permissions.
""" """
class Meta: class Meta:
abstract = True abstract = True
@ -51,7 +52,7 @@ class OrganizationalObjectType(
abstract = True abstract = True
class PrimaryObjectType( class NetBoxObjectType(
ChangelogMixin, ChangelogMixin,
CustomFieldsMixin, CustomFieldsMixin,
JournalEntriesMixin, JournalEntriesMixin,
@ -59,7 +60,7 @@ class PrimaryObjectType(
BaseObjectType BaseObjectType
): ):
""" """
Base type for primary models GraphQL type for most NetBox models. Includes support for custom fields, change logging, journaling, and tags.
""" """
class Meta: class Meta:
abstract = True abstract = True

View File

@ -390,10 +390,10 @@ MENUS = [
# Add plugin menus # Add plugin menus
# #
if registry['plugin_menu_items']: if registry['plugins']['menu_items']:
plugin_menu_groups = [] plugin_menu_groups = []
for plugin_name, items in registry['plugin_menu_items'].items(): for plugin_name, items in registry['plugins']['menu_items'].items():
plugin_menu_groups.append( plugin_menu_groups.append(
MenuGroup( MenuGroup(
label=plugin_name, label=plugin_name,

View File

@ -49,10 +49,10 @@ PREFERENCES = {
} }
# Register plugin preferences # Register plugin preferences
if registry['plugin_preferences']: if registry['plugins']['preferences']:
plugin_preferences = {} plugin_preferences = {}
for plugin_name, preferences in registry['plugin_preferences'].items(): for plugin_name, preferences in registry['plugins']['preferences'].items():
for name, userpreference in preferences.items(): for name, userpreference in preferences.items():
PREFERENCES[f'plugins.{plugin_name}.{name}'] = userpreference PREFERENCES[f'plugins.{plugin_name}.{name}'] = userpreference

View File

@ -1,7 +1,7 @@
import graphene import graphene
from tenancy import filtersets, models from tenancy import filtersets, models
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'ContactAssignmentType', 'ContactAssignmentType',
@ -24,7 +24,7 @@ class ContactAssignmentsMixin:
# Tenants # Tenants
# #
class TenantType(PrimaryObjectType): class TenantType(NetBoxObjectType):
class Meta: class Meta:
model = models.Tenant model = models.Tenant
@ -44,7 +44,7 @@ class TenantGroupType(OrganizationalObjectType):
# Contacts # Contacts
# #
class ContactType(ContactAssignmentsMixin, PrimaryObjectType): class ContactType(ContactAssignmentsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Contact model = models.Contact

View File

@ -1,7 +1,7 @@
from dcim.graphql.types import ComponentObjectType from dcim.graphql.types import ComponentObjectType
from extras.graphql.mixins import ConfigContextMixin from extras.graphql.mixins import ConfigContextMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType
from virtualization import filtersets, models from virtualization import filtersets, models
__all__ = ( __all__ = (
@ -13,7 +13,7 @@ __all__ = (
) )
class ClusterType(VLANGroupsMixin, PrimaryObjectType): class ClusterType(VLANGroupsMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.Cluster model = models.Cluster
@ -37,7 +37,7 @@ class ClusterTypeType(OrganizationalObjectType):
filterset_class = filtersets.ClusterTypeFilterSet filterset_class = filtersets.ClusterTypeFilterSet
class VirtualMachineType(ConfigContextMixin, PrimaryObjectType): class VirtualMachineType(ConfigContextMixin, NetBoxObjectType):
class Meta: class Meta:
model = models.VirtualMachine model = models.VirtualMachine

View File

@ -1,5 +1,5 @@
from wireless import filtersets, models from wireless import filtersets, models
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType
__all__ = ( __all__ = (
'WirelessLANType', 'WirelessLANType',
@ -16,7 +16,7 @@ class WirelessLANGroupType(OrganizationalObjectType):
filterset_class = filtersets.WirelessLANGroupFilterSet filterset_class = filtersets.WirelessLANGroupFilterSet
class WirelessLANType(PrimaryObjectType): class WirelessLANType(NetBoxObjectType):
class Meta: class Meta:
model = models.WirelessLAN model = models.WirelessLAN
@ -30,7 +30,7 @@ class WirelessLANType(PrimaryObjectType):
return self.auth_cipher or None return self.auth_cipher or None
class WirelessLinkType(PrimaryObjectType): class WirelessLinkType(NetBoxObjectType):
class Meta: class Meta:
model = models.WirelessLink model = models.WirelessLink