Merge pull request #6873 from netbox-community/6829-graphql-reverse-relations

Closes #6829: GraphQL reverse generic relations
This commit is contained in:
Jeremy Stretch 2021-08-03 16:22:45 -04:00 committed by GitHub
commit c411d2a9f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 266 additions and 88 deletions

View File

@ -2,6 +2,10 @@
## v3.0-beta2 (FUTURE)
### Enhancements
* [#6829](https://github.com/netbox-community/netbox/issues/6829) - Extend GraphQL API to support reverse generic relationships
### Bug Fixes
* [#6811](https://github.com/netbox-community/netbox/issues/6811) - Fix exception when editing users

View File

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

View File

@ -1,8 +1,11 @@
from dcim import filtersets, models
from netbox.graphql.types import BaseObjectType, ObjectType, TaggedObjectType
from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = (
'CableType',
'ComponentObjectType',
'ConsolePortType',
'ConsolePortTemplateType',
'ConsoleServerPortType',
@ -38,7 +41,40 @@ __all__ = (
)
class CableType(TaggedObjectType):
#
# Base types
#
class ComponentObjectType(
ChangelogMixin,
CustomFieldsMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
class ComponentTemplateObjectType(
ChangelogMixin,
BaseObjectType
):
"""
Base type for device/VM components
"""
class Meta:
abstract = True
#
# Model types
#
class CableType(PrimaryObjectType):
class Meta:
model = models.Cable
@ -52,7 +88,7 @@ class CableType(TaggedObjectType):
return self.length_unit or None
class ConsolePortType(TaggedObjectType):
class ConsolePortType(ComponentObjectType):
class Meta:
model = models.ConsolePort
@ -63,7 +99,7 @@ class ConsolePortType(TaggedObjectType):
return self.type or None
class ConsolePortTemplateType(BaseObjectType):
class ConsolePortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ConsolePortTemplate
@ -74,7 +110,7 @@ class ConsolePortTemplateType(BaseObjectType):
return self.type or None
class ConsoleServerPortType(TaggedObjectType):
class ConsoleServerPortType(ComponentObjectType):
class Meta:
model = models.ConsoleServerPort
@ -85,7 +121,7 @@ class ConsoleServerPortType(TaggedObjectType):
return self.type or None
class ConsoleServerPortTemplateType(BaseObjectType):
class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.ConsoleServerPortTemplate
@ -96,7 +132,7 @@ class ConsoleServerPortTemplateType(BaseObjectType):
return self.type or None
class DeviceType(TaggedObjectType):
class DeviceType(ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Device
@ -107,7 +143,7 @@ class DeviceType(TaggedObjectType):
return self.face or None
class DeviceBayType(TaggedObjectType):
class DeviceBayType(ComponentObjectType):
class Meta:
model = models.DeviceBay
@ -115,7 +151,7 @@ class DeviceBayType(TaggedObjectType):
filterset_class = filtersets.DeviceBayFilterSet
class DeviceBayTemplateType(BaseObjectType):
class DeviceBayTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.DeviceBayTemplate
@ -123,7 +159,7 @@ class DeviceBayTemplateType(BaseObjectType):
filterset_class = filtersets.DeviceBayTemplateFilterSet
class DeviceRoleType(ObjectType):
class DeviceRoleType(OrganizationalObjectType):
class Meta:
model = models.DeviceRole
@ -131,7 +167,7 @@ class DeviceRoleType(ObjectType):
filterset_class = filtersets.DeviceRoleFilterSet
class DeviceTypeType(TaggedObjectType):
class DeviceTypeType(PrimaryObjectType):
class Meta:
model = models.DeviceType
@ -142,7 +178,7 @@ class DeviceTypeType(TaggedObjectType):
return self.subdevice_role or None
class FrontPortType(TaggedObjectType):
class FrontPortType(ComponentObjectType):
class Meta:
model = models.FrontPort
@ -150,7 +186,7 @@ class FrontPortType(TaggedObjectType):
filterset_class = filtersets.FrontPortFilterSet
class FrontPortTemplateType(BaseObjectType):
class FrontPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.FrontPortTemplate
@ -158,7 +194,7 @@ class FrontPortTemplateType(BaseObjectType):
filterset_class = filtersets.FrontPortTemplateFilterSet
class InterfaceType(TaggedObjectType):
class InterfaceType(IPAddressesMixin, ComponentObjectType):
class Meta:
model = models.Interface
@ -169,7 +205,7 @@ class InterfaceType(TaggedObjectType):
return self.mode or None
class InterfaceTemplateType(BaseObjectType):
class InterfaceTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.InterfaceTemplate
@ -177,7 +213,7 @@ class InterfaceTemplateType(BaseObjectType):
filterset_class = filtersets.InterfaceTemplateFilterSet
class InventoryItemType(TaggedObjectType):
class InventoryItemType(ComponentObjectType):
class Meta:
model = models.InventoryItem
@ -185,7 +221,7 @@ class InventoryItemType(TaggedObjectType):
filterset_class = filtersets.InventoryItemFilterSet
class LocationType(ObjectType):
class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
class Meta:
model = models.Location
@ -193,7 +229,7 @@ class LocationType(ObjectType):
filterset_class = filtersets.LocationFilterSet
class ManufacturerType(ObjectType):
class ManufacturerType(OrganizationalObjectType):
class Meta:
model = models.Manufacturer
@ -201,7 +237,7 @@ class ManufacturerType(ObjectType):
filterset_class = filtersets.ManufacturerFilterSet
class PlatformType(ObjectType):
class PlatformType(OrganizationalObjectType):
class Meta:
model = models.Platform
@ -209,7 +245,7 @@ class PlatformType(ObjectType):
filterset_class = filtersets.PlatformFilterSet
class PowerFeedType(TaggedObjectType):
class PowerFeedType(PrimaryObjectType):
class Meta:
model = models.PowerFeed
@ -217,7 +253,7 @@ class PowerFeedType(TaggedObjectType):
filterset_class = filtersets.PowerFeedFilterSet
class PowerOutletType(TaggedObjectType):
class PowerOutletType(ComponentObjectType):
class Meta:
model = models.PowerOutlet
@ -231,7 +267,7 @@ class PowerOutletType(TaggedObjectType):
return self.type or None
class PowerOutletTemplateType(BaseObjectType):
class PowerOutletTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.PowerOutletTemplate
@ -245,7 +281,7 @@ class PowerOutletTemplateType(BaseObjectType):
return self.type or None
class PowerPanelType(TaggedObjectType):
class PowerPanelType(PrimaryObjectType):
class Meta:
model = models.PowerPanel
@ -253,7 +289,7 @@ class PowerPanelType(TaggedObjectType):
filterset_class = filtersets.PowerPanelFilterSet
class PowerPortType(TaggedObjectType):
class PowerPortType(ComponentObjectType):
class Meta:
model = models.PowerPort
@ -264,7 +300,7 @@ class PowerPortType(TaggedObjectType):
return self.type or None
class PowerPortTemplateType(BaseObjectType):
class PowerPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.PowerPortTemplate
@ -275,7 +311,7 @@ class PowerPortTemplateType(BaseObjectType):
return self.type or None
class RackType(TaggedObjectType):
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Rack
@ -289,7 +325,7 @@ class RackType(TaggedObjectType):
return self.outer_unit or None
class RackReservationType(TaggedObjectType):
class RackReservationType(PrimaryObjectType):
class Meta:
model = models.RackReservation
@ -297,7 +333,7 @@ class RackReservationType(TaggedObjectType):
filterset_class = filtersets.RackReservationFilterSet
class RackRoleType(ObjectType):
class RackRoleType(OrganizationalObjectType):
class Meta:
model = models.RackRole
@ -305,7 +341,7 @@ class RackRoleType(ObjectType):
filterset_class = filtersets.RackRoleFilterSet
class RearPortType(TaggedObjectType):
class RearPortType(ComponentObjectType):
class Meta:
model = models.RearPort
@ -313,7 +349,7 @@ class RearPortType(TaggedObjectType):
filterset_class = filtersets.RearPortFilterSet
class RearPortTemplateType(BaseObjectType):
class RearPortTemplateType(ComponentTemplateObjectType):
class Meta:
model = models.RearPortTemplate
@ -321,7 +357,7 @@ class RearPortTemplateType(BaseObjectType):
filterset_class = filtersets.RearPortTemplateFilterSet
class RegionType(ObjectType):
class RegionType(VLANGroupsMixin, OrganizationalObjectType):
class Meta:
model = models.Region
@ -329,7 +365,7 @@ class RegionType(ObjectType):
filterset_class = filtersets.RegionFilterSet
class SiteType(TaggedObjectType):
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
class Meta:
model = models.Site
@ -337,7 +373,7 @@ class SiteType(TaggedObjectType):
filterset_class = filtersets.SiteFilterSet
class SiteGroupType(ObjectType):
class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
class Meta:
model = models.SiteGroup
@ -345,7 +381,7 @@ class SiteGroupType(ObjectType):
filterset_class = filtersets.SiteGroupFilterSet
class VirtualChassisType(TaggedObjectType):
class VirtualChassisType(PrimaryObjectType):
class Meta:
model = models.VirtualChassis

View File

@ -175,6 +175,12 @@ class Rack(PrimaryModel):
comments = models.TextField(
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='rack'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@ -53,6 +53,12 @@ class Region(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='region'
)
def get_absolute_url(self):
return reverse('dcim:region', args=[self.pk])
@ -95,6 +101,12 @@ class SiteGroup(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site_group'
)
def get_absolute_url(self):
return reverse('dcim:sitegroup', args=[self.pk])
@ -210,6 +222,12 @@ class Site(PrimaryModel):
comments = models.TextField(
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='site'
)
images = GenericRelation(
to='extras.ImageAttachment'
)
@ -267,6 +285,12 @@ class Location(NestedGroupModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='location'
)
images = GenericRelation(
to='extras.ImageAttachment'
)

View File

@ -0,0 +1,45 @@
import graphene
from graphene.types.generic import GenericScalar
__all__ = (
'ChangelogMixin',
'CustomFieldsMixin',
'ImageAttachmentsMixin',
'JournalEntriesMixin',
'TagsMixin',
)
class ChangelogMixin:
changelog = graphene.List('extras.graphql.types.ObjectChangeType')
def resolve_changelog(self, info):
return self.object_changes.restrict(info.context.user, 'view')
class CustomFieldsMixin:
custom_fields = GenericScalar()
def resolve_custom_fields(self, info):
return self.custom_field_data
class ImageAttachmentsMixin:
image_attachments = graphene.List('extras.graphql.types.ImageAttachmentType')
def resolve_image_attachments(self, info):
return self.images.restrict(info.context.user, 'view')
class JournalEntriesMixin:
journal_entries = graphene.List('extras.graphql.types.JournalEntryType')
def resolve_journal_entries(self, info):
return self.journal_entries.restrict(info.context.user, 'view')
class TagsMixin:
tags = graphene.List(graphene.String)
def resolve_tags(self, info):
return self.tags.all()

View File

@ -1,5 +1,5 @@
from extras import filtersets, models
from netbox.graphql.types import BaseObjectType
from netbox.graphql.types import BaseObjectType, ObjectType
__all__ = (
'ConfigContextType',
@ -8,12 +8,13 @@ __all__ = (
'ExportTemplateType',
'ImageAttachmentType',
'JournalEntryType',
'ObjectChangeType',
'TagType',
'WebhookType',
)
class ConfigContextType(BaseObjectType):
class ConfigContextType(ObjectType):
class Meta:
model = models.ConfigContext
@ -21,7 +22,7 @@ class ConfigContextType(BaseObjectType):
filterset_class = filtersets.ConfigContextFilterSet
class CustomFieldType(BaseObjectType):
class CustomFieldType(ObjectType):
class Meta:
model = models.CustomField
@ -29,7 +30,7 @@ class CustomFieldType(BaseObjectType):
filterset_class = filtersets.CustomFieldFilterSet
class CustomLinkType(BaseObjectType):
class CustomLinkType(ObjectType):
class Meta:
model = models.CustomLink
@ -37,7 +38,7 @@ class CustomLinkType(BaseObjectType):
filterset_class = filtersets.CustomLinkFilterSet
class ExportTemplateType(BaseObjectType):
class ExportTemplateType(ObjectType):
class Meta:
model = models.ExportTemplate
@ -53,7 +54,7 @@ class ImageAttachmentType(BaseObjectType):
filterset_class = filtersets.ImageAttachmentFilterSet
class JournalEntryType(BaseObjectType):
class JournalEntryType(ObjectType):
class Meta:
model = models.JournalEntry
@ -61,7 +62,15 @@ class JournalEntryType(BaseObjectType):
filterset_class = filtersets.JournalEntryFilterSet
class TagType(BaseObjectType):
class ObjectChangeType(BaseObjectType):
class Meta:
model = models.ObjectChange
fields = '__all__'
filterset_class = filtersets.ObjectChangeFilterSet
class TagType(ObjectType):
class Meta:
model = models.Tag
@ -69,7 +78,7 @@ class TagType(BaseObjectType):
filterset_class = filtersets.TagFilterSet
class WebhookType(BaseObjectType):
class WebhookType(ObjectType):
class Meta:
model = models.Webhook

View File

@ -0,0 +1,20 @@
import graphene
__all__ = (
'IPAddressesMixin',
'VLANGroupsMixin',
)
class IPAddressesMixin:
ip_addresses = graphene.List('ipam.graphql.types.IPAddressType')
def resolve_ip_addresses(self, info):
return self.ip_addresses.restrict(info.context.user, 'view')
class VLANGroupsMixin:
vlan_groups = graphene.List('ipam.graphql.types.VLANGroupType')
def resolve_vlan_groups(self, info):
return self.vlan_groups.restrict(info.context.user, 'view')

View File

@ -1,5 +1,5 @@
from ipam import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'AggregateType',
@ -16,7 +16,7 @@ __all__ = (
)
class AggregateType(TaggedObjectType):
class AggregateType(PrimaryObjectType):
class Meta:
model = models.Aggregate
@ -24,7 +24,7 @@ class AggregateType(TaggedObjectType):
filterset_class = filtersets.AggregateFilterSet
class IPAddressType(TaggedObjectType):
class IPAddressType(PrimaryObjectType):
class Meta:
model = models.IPAddress
@ -35,7 +35,7 @@ class IPAddressType(TaggedObjectType):
return self.role or None
class IPRangeType(TaggedObjectType):
class IPRangeType(PrimaryObjectType):
class Meta:
model = models.IPRange
@ -46,7 +46,7 @@ class IPRangeType(TaggedObjectType):
return self.role or None
class PrefixType(TaggedObjectType):
class PrefixType(PrimaryObjectType):
class Meta:
model = models.Prefix
@ -54,7 +54,7 @@ class PrefixType(TaggedObjectType):
filterset_class = filtersets.PrefixFilterSet
class RIRType(ObjectType):
class RIRType(OrganizationalObjectType):
class Meta:
model = models.RIR
@ -62,7 +62,7 @@ class RIRType(ObjectType):
filterset_class = filtersets.RIRFilterSet
class RoleType(ObjectType):
class RoleType(OrganizationalObjectType):
class Meta:
model = models.Role
@ -70,7 +70,7 @@ class RoleType(ObjectType):
filterset_class = filtersets.RoleFilterSet
class RouteTargetType(TaggedObjectType):
class RouteTargetType(PrimaryObjectType):
class Meta:
model = models.RouteTarget
@ -78,7 +78,7 @@ class RouteTargetType(TaggedObjectType):
filterset_class = filtersets.RouteTargetFilterSet
class ServiceType(TaggedObjectType):
class ServiceType(PrimaryObjectType):
class Meta:
model = models.Service
@ -86,7 +86,7 @@ class ServiceType(TaggedObjectType):
filterset_class = filtersets.ServiceFilterSet
class VLANType(TaggedObjectType):
class VLANType(PrimaryObjectType):
class Meta:
model = models.VLAN
@ -94,7 +94,7 @@ class VLANType(TaggedObjectType):
filterset_class = filtersets.VLANFilterSet
class VLANGroupType(ObjectType):
class VLANGroupType(OrganizationalObjectType):
class Meta:
model = models.VLANGroup
@ -102,7 +102,7 @@ class VLANGroupType(ObjectType):
filterset_class = filtersets.VLANGroupFilterSet
class VRFType(TaggedObjectType):
class VRFType(PrimaryObjectType):
class Meta:
model = models.VRF

View File

@ -1,12 +1,12 @@
import graphene
from django.contrib.contenttypes.models import ContentType
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
from extras.graphql.mixins import ChangelogMixin, CustomFieldsMixin, JournalEntriesMixin, TagsMixin
__all__ = (
'BaseObjectType',
'ObjectType',
'TaggedObjectType',
'OrganizationalObjectType',
'PrimaryObjectType',
)
@ -27,30 +27,41 @@ class BaseObjectType(DjangoObjectType):
return queryset.restrict(info.context.user, 'view')
class ObjectType(BaseObjectType):
class ObjectType(
ChangelogMixin,
BaseObjectType
):
"""
Extends BaseObjectType with support for custom field data.
Base GraphQL object type for unclassified models which support change logging
"""
custom_fields = GenericScalar()
class Meta:
abstract = True
def resolve_custom_fields(self, info):
return self.custom_field_data
class TaggedObjectType(ObjectType):
class OrganizationalObjectType(
ChangelogMixin,
CustomFieldsMixin,
BaseObjectType
):
"""
Extends ObjectType with support for Tags
Base type for organizational models
"""
tags = graphene.List(graphene.String)
class Meta:
abstract = True
def resolve_tags(self, info):
return self.tags.all()
class PrimaryObjectType(
ChangelogMixin,
CustomFieldsMixin,
JournalEntriesMixin,
TagsMixin,
BaseObjectType
):
"""
Base type for primary models
"""
class Meta:
abstract = True
#

View File

@ -40,6 +40,11 @@ class ChangeLoggingMixin(models.Model):
blank=True,
null=True
)
object_changes = GenericRelation(
to='extras.ObjectChange',
content_type_field='changed_object_type',
object_id_field='changed_object_id'
)
class Meta:
abstract = True

View File

@ -1,5 +1,5 @@
from tenancy import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'TenantType',
@ -7,7 +7,7 @@ __all__ = (
)
class TenantType(TaggedObjectType):
class TenantType(PrimaryObjectType):
class Meta:
model = models.Tenant
@ -15,7 +15,7 @@ class TenantType(TaggedObjectType):
filterset_class = filtersets.TenantFilterSet
class TenantGroupType(ObjectType):
class TenantGroupType(OrganizationalObjectType):
class Meta:
model = models.TenantGroup

View File

@ -5,7 +5,7 @@ from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.test import override_settings
from graphene.types.dynamic import Dynamic
from graphene.types import Dynamic as GQLDynamic, List as GQLList
from rest_framework import status
from rest_framework.test import APIClient
@ -446,9 +446,13 @@ class APIViewTestCases:
# Compile list of fields to include
fields_string = ''
for field_name, field in type_class._meta.fields.items():
if type(field) is Dynamic:
if type(field) is GQLDynamic:
# Dynamic fields must specify a subselection
fields_string += f'{field_name} {{ id }}\n'
elif type(field.type) is GQLList and field_name not in ('tags', 'choices'):
# TODO: Come up with something more elegant
# Temporary hack to support automated testing of reverse generic relations
fields_string += f'{field_name} {{ id }}\n'
else:
fields_string += f'{field_name}\n'

View File

@ -1,5 +1,7 @@
from dcim.graphql.types import ComponentObjectType
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from virtualization import filtersets, models
from netbox.graphql.types import ObjectType, TaggedObjectType
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'ClusterType',
@ -10,7 +12,7 @@ __all__ = (
)
class ClusterType(TaggedObjectType):
class ClusterType(VLANGroupsMixin, PrimaryObjectType):
class Meta:
model = models.Cluster
@ -18,7 +20,7 @@ class ClusterType(TaggedObjectType):
filterset_class = filtersets.ClusterFilterSet
class ClusterGroupType(ObjectType):
class ClusterGroupType(VLANGroupsMixin, OrganizationalObjectType):
class Meta:
model = models.ClusterGroup
@ -26,7 +28,7 @@ class ClusterGroupType(ObjectType):
filterset_class = filtersets.ClusterGroupFilterSet
class ClusterTypeType(ObjectType):
class ClusterTypeType(OrganizationalObjectType):
class Meta:
model = models.ClusterType
@ -34,7 +36,7 @@ class ClusterTypeType(ObjectType):
filterset_class = filtersets.ClusterTypeFilterSet
class VirtualMachineType(TaggedObjectType):
class VirtualMachineType(PrimaryObjectType):
class Meta:
model = models.VirtualMachine
@ -42,7 +44,7 @@ class VirtualMachineType(TaggedObjectType):
filterset_class = filtersets.VirtualMachineFilterSet
class VMInterfaceType(TaggedObjectType):
class VMInterfaceType(IPAddressesMixin, ComponentObjectType):
class Meta:
model = models.VMInterface

View File

@ -81,6 +81,12 @@ class ClusterGroup(OrganizationalModel):
max_length=200,
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='cluster_group'
)
objects = RestrictedQuerySet.as_manager()
@ -136,6 +142,12 @@ class Cluster(PrimaryModel):
comments = models.TextField(
blank=True
)
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='cluster'
)
objects = RestrictedQuerySet.as_manager()