Skip to content

Commit

Permalink
Merge pull request #8562 from netbox-community/8405-plugins-graphql
Browse files Browse the repository at this point in the history
Closes #8405: GraphQL support for plugins
  • Loading branch information
jeremystretch committed Feb 7, 2022
2 parents 049acde + dae5c94 commit 0e827b6
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 87 deletions.
59 changes: 59 additions & 0 deletions docs/plugins/development/graphql.md
Original file line number Diff line number Diff line change
@@ -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
37 changes: 19 additions & 18 deletions docs/plugins/development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
8 changes: 4 additions & 4 deletions netbox/circuits/graphql/types.py
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -18,7 +18,7 @@ class Meta:
filterset_class = filtersets.CircuitTerminationFilterSet


class CircuitType(PrimaryObjectType):
class CircuitType(NetBoxObjectType):

class Meta:
model = models.Circuit
Expand All @@ -34,15 +34,15 @@ class Meta:
filterset_class = filtersets.CircuitTypeFilterSet


class ProviderType(PrimaryObjectType):
class ProviderType(NetBoxObjectType):

class Meta:
model = models.Provider
fields = '__all__'
filterset_class = filtersets.ProviderFilterSet


class ProviderNetworkType(PrimaryObjectType):
class ProviderNetworkType(NetBoxObjectType):

class Meta:
model = models.ProviderNetwork
Expand Down
22 changes: 11 additions & 11 deletions netbox/dcim/graphql/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
)
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',
Expand Down Expand Up @@ -85,7 +85,7 @@ class Meta:
# Model types
#

class CableType(PrimaryObjectType):
class CableType(NetBoxObjectType):

class Meta:
model = models.Cable
Expand Down Expand Up @@ -143,7 +143,7 @@ def resolve_type(self, info):
return self.type or None


class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, PrimaryObjectType):
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):

class Meta:
model = models.Device
Expand Down Expand Up @@ -189,7 +189,7 @@ class Meta:
filterset_class = filtersets.DeviceRoleFilterSet


class DeviceTypeType(PrimaryObjectType):
class DeviceTypeType(NetBoxObjectType):

class Meta:
model = models.DeviceType
Expand Down Expand Up @@ -300,7 +300,7 @@ class Meta:
filterset_class = filtersets.ModuleBayTemplateFilterSet


class ModuleTypeType(PrimaryObjectType):
class ModuleTypeType(NetBoxObjectType):

class Meta:
model = models.ModuleType
Expand All @@ -316,7 +316,7 @@ class Meta:
filterset_class = filtersets.PlatformFilterSet


class PowerFeedType(PrimaryObjectType):
class PowerFeedType(NetBoxObjectType):

class Meta:
model = models.PowerFeed
Expand Down Expand Up @@ -352,7 +352,7 @@ def resolve_type(self, info):
return self.type or None


class PowerPanelType(PrimaryObjectType):
class PowerPanelType(NetBoxObjectType):

class Meta:
model = models.PowerPanel
Expand Down Expand Up @@ -382,7 +382,7 @@ def resolve_type(self, info):
return self.type or None


class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):

class Meta:
model = models.Rack
Expand All @@ -396,7 +396,7 @@ def resolve_outer_unit(self, info):
return self.outer_unit or None


class RackReservationType(PrimaryObjectType):
class RackReservationType(NetBoxObjectType):

class Meta:
model = models.RackReservation
Expand Down Expand Up @@ -436,7 +436,7 @@ class Meta:
filterset_class = filtersets.RegionFilterSet


class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
asn = graphene.Field(BigInt)

class Meta:
Expand All @@ -453,7 +453,7 @@ class Meta:
filterset_class = filtersets.SiteGroupFilterSet


class VirtualChassisType(PrimaryObjectType):
class VirtualChassisType(NetBoxObjectType):

class Meta:
model = models.VirtualChassis
Expand Down
44 changes: 32 additions & 12 deletions netbox/extras/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
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),
}


#
Expand Down Expand Up @@ -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)
Expand All @@ -69,10 +74,14 @@ def ready(self):
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
Expand Down Expand Up @@ -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)


#
Expand Down Expand Up @@ -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)


#
Expand All @@ -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
2 changes: 1 addition & 1 deletion netbox/extras/templatetags/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions netbox/extras/tests/dummy_plugin/graphql.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 0e827b6

Please sign in to comment.