Просмотр исходного кода

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

Closes #8405: GraphQL support for plugins
Jeremy Stretch 4 лет назад
Родитель
Сommit
0e827b6ae6

+ 59 - 0
docs/plugins/development/graphql.md

@@ -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

+ 19 - 18
docs/plugins/development/index.md

@@ -22,7 +22,7 @@ However, keep in mind that each piece of functionality is entirely optional. For
 
 ### 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
 project-name/
@@ -102,23 +102,24 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i
 
 #### PluginConfig Attributes
 
-| Name | Description                                                                                                   |
-| ---- |---------------------------------------------------------------------------------------------------------------|
-| `name` | Raw plugin name; same as the plugin's source directory                                                        |
-| `verbose_name` | Human-friendly name for the plugin                                                                            |
-| `version` | Current release ([semantic versioning](https://semver.org/) is encouraged)                                    |
-| `description` | Brief description of the plugin's purpose                                                                     |
-| `author` | Name of plugin's author                                                                                       |
-| `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.             |
-| `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                                             |
-| `min_version` | Minimum 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                                     |
-| `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`)           |
-| `user_preferences` | The dotted path to the dictionary mapping of user preferences defined by the plugin (default: `preferences.preferences`) |
+| Name                  | Description                                                                                                              |
+|-----------------------|--------------------------------------------------------------------------------------------------------------------------|
+| `name`                | Raw plugin name; same as the plugin's source directory                                                                   |
+| `verbose_name`        | Human-friendly name for the plugin                                                                                       |
+| `version`             | Current release ([semantic versioning](https://semver.org/) is encouraged)                                               |
+| `description`         | Brief description of the plugin's purpose                                                                                |
+| `author`              | Name of plugin's author                                                                                                  |
+| `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.                        |
+| `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                                                        |
+| `min_version`         | Minimum 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                                                |
+| `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`)                      |
+| `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.
 

+ 1 - 0
mkdocs.yml

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

+ 4 - 4
netbox/circuits/graphql/types.py

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

+ 11 - 11
netbox/dcim/graphql/types.py

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

+ 32 - 12
netbox/extras/plugins/__init__.py

@@ -12,10 +12,13 @@ from utilities.choices import ButtonColorChoices
 from extras.plugins.utils import import_object
 
 
-# Initialize plugin registry stores
-registry['plugin_template_extensions'] = collections.defaultdict(list)
-registry['plugin_menu_items'] = {}
-registry['plugin_preferences'] = {}
+# Initialize plugin registry
+registry['plugins'] = {
+    'graphql_schemas': [],
+    '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
     # integrated components.
-    template_extensions = 'template_content.template_extensions'
+    graphql_schema = 'graphql.schema'
     menu_items = 'navigation.menu_items'
+    template_extensions = 'template_content.template_extensions'
     user_preferences = 'preferences.preferences'
 
     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}")
         if template_extensions is not None:
             register_template_extensions(template_extensions)
@@ -69,10 +74,14 @@ class PluginConfig(AppConfig):
         if menu_items is not None:
             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}")
         if user_preferences is not None:
-            plugin_name = self.name.rsplit('.', 1)[1]
             register_user_preferences(plugin_name, user_preferences)
 
     @classmethod
@@ -178,13 +187,13 @@ def register_template_extensions(class_list):
     # Validation
     for template_extension in class_list:
         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):
             raise TypeError(f"{template_extension} is not a subclass of extras.plugins.PluginTemplateExtension!")
         if template_extension.model is None:
             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):
                 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.
     """
-    registry['plugin_preferences'][plugin_name] = preferences
+    registry['plugins']['preferences'][plugin_name] = preferences

+ 1 - 1
netbox/extras/templatetags/plugins.py

@@ -23,7 +23,7 @@ def _get_registered_content(obj, method, template_context):
     }
 
     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:
 
         # If the class has not overridden the specified method, we can skip it (because we know it

+ 21 - 0
netbox/extras/tests/dummy_plugin/graphql.py

@@ -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

+ 15 - 5
netbox/extras/tests/test_plugins.py

@@ -7,6 +7,7 @@ from django.urls import reverse
 
 from extras.registry import registry
 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")
@@ -61,8 +62,8 @@ class PluginTest(TestCase):
         """
         Check that plugin MenuItems and MenuButtons are registered.
         """
-        self.assertIn('Dummy plugin', registry['plugin_menu_items'])
-        menu_items = registry['plugin_menu_items']['Dummy plugin']
+        self.assertIn('Dummy plugin', registry['plugins']['menu_items'])
+        menu_items = registry['plugins']['menu_items']['Dummy plugin']
         self.assertEqual(len(menu_items), 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
 
-        self.assertIn(SiteContent, registry['plugin_template_extensions']['dcim.site'])
+        self.assertIn(SiteContent, registry['plugins']['template_extensions']['dcim.site'])
 
     def test_user_preferences(self):
         """
         Check that plugin UserPreferences are registered.
         """
-        self.assertIn('dummy_plugin', registry['plugin_preferences'])
-        user_preferences = registry['plugin_preferences']['dummy_plugin']
+        self.assertIn('dummy_plugin', registry['plugins']['preferences'])
+        user_preferences = registry['plugins']['preferences']['dummy_plugin']
         self.assertEqual(type(user_preferences), dict)
         self.assertEqual(list(user_preferences.keys()), ['pref1', 'pref2'])
 
@@ -143,3 +144,12 @@ class PluginTest(TestCase):
         user_config = {'bar': 456}
         DummyConfigWithDefaultSettings.validate(user_config, settings.VERSION)
         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))

+ 12 - 12
netbox/ipam/graphql/types.py

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

+ 6 - 7
netbox/netbox/graphql/fields.py

@@ -41,15 +41,14 @@ 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
+        filter_kwargs = {}
 
         # 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)
+        filterset_class = getattr(_type._meta, 'filterset_class', None)
+        if filterset_class:
+            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)
 

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

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

+ 5 - 4
netbox/netbox/graphql/types.py

@@ -5,8 +5,9 @@ from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntr
 
 __all__ = (
     'BaseObjectType',
+    'ObjectType',
     'OrganizationalObjectType',
-    'PrimaryObjectType',
+    'NetBoxObjectType',
 )
 
 
@@ -16,7 +17,7 @@ __all__ = (
 
 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:
         abstract = True
@@ -51,7 +52,7 @@ class OrganizationalObjectType(
         abstract = True
 
 
-class PrimaryObjectType(
+class NetBoxObjectType(
     ChangelogMixin,
     CustomFieldsMixin,
     JournalEntriesMixin,
@@ -59,7 +60,7 @@ class PrimaryObjectType(
     BaseObjectType
 ):
     """
-    Base type for primary models
+    GraphQL type for most NetBox models. Includes support for custom fields, change logging, journaling, and tags.
     """
     class Meta:
         abstract = True

+ 2 - 2
netbox/netbox/navigation_menu.py

@@ -390,10 +390,10 @@ MENUS = [
 # Add plugin menus
 #
 
-if registry['plugin_menu_items']:
+if registry['plugins']['menu_items']:
     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(
             MenuGroup(
                 label=plugin_name,

+ 2 - 2
netbox/netbox/preferences.py

@@ -49,10 +49,10 @@ PREFERENCES = {
 }
 
 # Register plugin preferences
-if registry['plugin_preferences']:
+if registry['plugins']['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():
             PREFERENCES[f'plugins.{plugin_name}.{name}'] = userpreference
 

+ 3 - 3
netbox/tenancy/graphql/types.py

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

+ 3 - 3
netbox/virtualization/graphql/types.py

@@ -1,7 +1,7 @@
 from dcim.graphql.types import ComponentObjectType
 from extras.graphql.mixins import ConfigContextMixin
 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
 
 __all__ = (
@@ -13,7 +13,7 @@ __all__ = (
 )
 
 
-class ClusterType(VLANGroupsMixin, PrimaryObjectType):
+class ClusterType(VLANGroupsMixin, NetBoxObjectType):
 
     class Meta:
         model = models.Cluster
@@ -37,7 +37,7 @@ class ClusterTypeType(OrganizationalObjectType):
         filterset_class = filtersets.ClusterTypeFilterSet
 
 
-class VirtualMachineType(ConfigContextMixin, PrimaryObjectType):
+class VirtualMachineType(ConfigContextMixin, NetBoxObjectType):
 
     class Meta:
         model = models.VirtualMachine

+ 3 - 3
netbox/wireless/graphql/types.py

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