mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-22 20:12:00 -06:00
Merge pull request #8562 from netbox-community/8405-plugins-graphql
Closes #8405: GraphQL support for plugins
This commit is contained in:
commit
0e827b6ae6
59
docs/plugins/development/graphql.md
Normal file
59
docs/plugins/development/graphql.md
Normal 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
|
@ -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.
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
netbox/extras/tests/dummy_plugin/graphql.py
Normal file
21
netbox/extras/tests/dummy_plugin/graphql.py
Normal 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
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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,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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user