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

10595 extend graphql relationships (#10603)

* 9817 add graphql l2vpntermination assigned_object

* 9817 add graphql ipaddressassignment assigned_object

* 9817 ipan graphql gfk

* 9817 dcim graphql gfk

* 9817 dcim graphql gfk

* 9817 fix tests

* 10595 cable a_terminations to graphql

* 10595 add contacts to graphql

* 10595 move contacts to Provider
Arthur Hanson 3 лет назад
Родитель
Сommit
9ca4c7315b

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

@@ -1,6 +1,8 @@
+import graphene
+
 from circuits import filtersets, models
 from dcim.graphql.mixins import CabledObjectMixin
-from extras.graphql.mixins import CustomFieldsMixin, TagsMixin
+from extras.graphql.mixins import CustomFieldsMixin, TagsMixin, ContactsMixin
 from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
 
 __all__ = (
@@ -20,8 +22,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
         filterset_class = filtersets.CircuitTerminationFilterSet
 
 
-class CircuitType(NetBoxObjectType):
-
+class CircuitType(NetBoxObjectType, ContactsMixin):
     class Meta:
         model = models.Circuit
         fields = '__all__'
@@ -36,7 +37,7 @@ class CircuitTypeType(OrganizationalObjectType):
         filterset_class = filtersets.CircuitTypeFilterSet
 
 
-class ProviderType(NetBoxObjectType):
+class ProviderType(NetBoxObjectType, ContactsMixin):
 
     class Meta:
         model = models.Provider

+ 110 - 0
netbox/dcim/graphql/gfk_mixins.py

@@ -2,24 +2,38 @@ import graphene
 from circuits.graphql.types import CircuitTerminationType
 from circuits.models import CircuitTermination
 from dcim.graphql.types import (
+    ConsolePortTemplateType,
     ConsolePortType,
+    ConsoleServerPortTemplateType,
     ConsoleServerPortType,
+    FrontPortTemplateType,
     FrontPortType,
+    InterfaceTemplateType,
     InterfaceType,
     PowerFeedType,
+    PowerOutletTemplateType,
     PowerOutletType,
+    PowerPortTemplateType,
     PowerPortType,
+    RearPortTemplateType,
     RearPortType,
 )
 from dcim.models import (
     ConsolePort,
+    ConsolePortTemplate,
     ConsoleServerPort,
+    ConsoleServerPortTemplate,
     FrontPort,
+    FrontPortTemplate,
     Interface,
+    InterfaceTemplate,
     PowerFeed,
     PowerOutlet,
+    PowerOutletTemplate,
     PowerPort,
+    PowerPortTemplate,
     RearPort,
+    RearPortTemplate,
 )
 
 
@@ -57,3 +71,99 @@ class LinkPeerType(graphene.Union):
             return PowerPortType
         if type(instance) == RearPort:
             return RearPortType
+
+
+class CableTerminationTerminationType(graphene.Union):
+    class Meta:
+        types = (
+            CircuitTerminationType,
+            ConsolePortType,
+            ConsoleServerPortType,
+            FrontPortType,
+            InterfaceType,
+            PowerFeedType,
+            PowerOutletType,
+            PowerPortType,
+            RearPortType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == CircuitTermination:
+            return CircuitTerminationType
+        if type(instance) == ConsolePortType:
+            return ConsolePortType
+        if type(instance) == ConsoleServerPort:
+            return ConsoleServerPortType
+        if type(instance) == FrontPort:
+            return FrontPortType
+        if type(instance) == Interface:
+            return InterfaceType
+        if type(instance) == PowerFeed:
+            return PowerFeedType
+        if type(instance) == PowerOutlet:
+            return PowerOutletType
+        if type(instance) == PowerPort:
+            return PowerPortType
+        if type(instance) == RearPort:
+            return RearPortType
+
+
+class InventoryItemTemplateComponentType(graphene.Union):
+    class Meta:
+        types = (
+            ConsolePortTemplateType,
+            ConsoleServerPortTemplateType,
+            FrontPortTemplateType,
+            InterfaceTemplateType,
+            PowerOutletTemplateType,
+            PowerPortTemplateType,
+            RearPortTemplateType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == ConsolePortTemplate:
+            return ConsolePortTemplateType
+        if type(instance) == ConsoleServerPortTemplate:
+            return ConsoleServerPortTemplateType
+        if type(instance) == FrontPortTemplate:
+            return FrontPortTemplateType
+        if type(instance) == InterfaceTemplate:
+            return InterfaceTemplateType
+        if type(instance) == PowerOutletTemplate:
+            return PowerOutletTemplateType
+        if type(instance) == PowerPortTemplate:
+            return PowerPortTemplateType
+        if type(instance) == RearPortTemplate:
+            return RearPortTemplateType
+
+
+class InventoryItemComponentType(graphene.Union):
+    class Meta:
+        types = (
+            ConsolePortType,
+            ConsoleServerPortType,
+            FrontPortType,
+            InterfaceType,
+            PowerOutletType,
+            PowerPortType,
+            RearPortType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == ConsolePort:
+            return ConsolePortType
+        if type(instance) == ConsoleServerPort:
+            return ConsoleServerPortType
+        if type(instance) == FrontPort:
+            return FrontPortType
+        if type(instance) == Interface:
+            return InterfaceType
+        if type(instance) == PowerOutlet:
+            return PowerOutletType
+        if type(instance) == PowerPort:
+            return PowerPortType
+        if type(instance) == RearPort:
+            return RearPortType

+ 23 - 12
netbox/dcim/graphql/types.py

@@ -2,7 +2,7 @@ import graphene
 
 from dcim import filtersets, models
 from extras.graphql.mixins import (
-    ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
+    ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
 )
 from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
 from netbox.graphql.scalars import BigInt
@@ -87,6 +87,8 @@ class ComponentTemplateObjectType(
 #
 
 class CableType(NetBoxObjectType):
+    a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
+    b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
 
     class Meta:
         model = models.Cable
@@ -99,12 +101,19 @@ class CableType(NetBoxObjectType):
     def resolve_length_unit(self, info):
         return self.length_unit or None
 
+    def resolve_a_terminations(self, info):
+        return self.a_terminations
+
+    def resolve_b_terminations(self, info):
+        return self.b_terminations
+
 
 class CableTerminationType(NetBoxObjectType):
+    termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
 
     class Meta:
         model = models.CableTermination
-        fields = '__all__'
+        exclude = ('termination_type', 'termination_id')
         filterset_class = filtersets.CableTerminationFilterSet
 
 
@@ -152,7 +161,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
         return self.type or None
 
 
-class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 
     class Meta:
         model = models.Device
@@ -183,10 +192,11 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
 
 
 class InventoryItemTemplateType(ComponentTemplateObjectType):
+    component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType')
 
     class Meta:
         model = models.InventoryItemTemplate
-        fields = '__all__'
+        exclude = ('component_type', 'component_id')
         filterset_class = filtersets.InventoryItemTemplateFilterSet
 
 
@@ -269,10 +279,11 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
 
 
 class InventoryItemType(ComponentObjectType):
+    component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')
 
     class Meta:
         model = models.InventoryItem
-        fields = '__all__'
+        exclude = ('component_type', 'component_id')
         filterset_class = filtersets.InventoryItemFilterSet
 
 
@@ -284,7 +295,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
         filterset_class = filtersets.InventoryItemRoleFilterSet
 
 
-class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
+class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
 
     class Meta:
         model = models.Location
@@ -292,7 +303,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectT
         filterset_class = filtersets.LocationFilterSet
 
 
-class ManufacturerType(OrganizationalObjectType):
+class ManufacturerType(OrganizationalObjectType, ContactsMixin):
 
     class Meta:
         model = models.Manufacturer
@@ -379,7 +390,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
         return self.type or None
 
 
-class PowerPanelType(NetBoxObjectType):
+class PowerPanelType(NetBoxObjectType, ContactsMixin):
 
     class Meta:
         model = models.PowerPanel
@@ -409,7 +420,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
         return self.type or None
 
 
-class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 
     class Meta:
         model = models.Rack
@@ -458,7 +469,7 @@ class RearPortTemplateType(ComponentTemplateObjectType):
         filterset_class = filtersets.RearPortTemplateFilterSet
 
 
-class RegionType(VLANGroupsMixin, OrganizationalObjectType):
+class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
 
     class Meta:
         model = models.Region
@@ -466,7 +477,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
         filterset_class = filtersets.RegionFilterSet
 
 
-class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
     asn = graphene.Field(BigInt)
 
     class Meta:
@@ -475,7 +486,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
         filterset_class = filtersets.SiteFilterSet
 
 
-class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
+class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
 
     class Meta:
         model = models.SiteGroup

+ 7 - 0
netbox/extras/graphql/mixins.py

@@ -59,3 +59,10 @@ class TagsMixin:
 
     def resolve_tags(self, info):
         return self.tags.all()
+
+
+class ContactsMixin:
+    contacts = graphene.List('tenancy.graphql.types.ContactAssignmentType')
+
+    def resolve_contacts(self, info):
+        return list(self.contacts.all())

+ 95 - 0
netbox/ipam/graphql/gfk_mixins.py

@@ -0,0 +1,95 @@
+import graphene
+from dcim.graphql.types import (
+    InterfaceType,
+    LocationType,
+    RackType,
+    RegionType,
+    SiteGroupType,
+    SiteType,
+)
+from dcim.models import Interface, Location, Rack, Region, Site, SiteGroup
+from ipam.graphql.types import FHRPGroupType, VLANType
+from ipam.models import VLAN, FHRPGroup
+from virtualization.graphql.types import ClusterGroupType, ClusterType, VMInterfaceType
+from virtualization.models import Cluster, ClusterGroup, VMInterface
+
+
+class IPAddressAssignmentType(graphene.Union):
+    class Meta:
+        types = (
+            InterfaceType,
+            FHRPGroupType,
+            VMInterfaceType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == Interface:
+            return InterfaceType
+        if type(instance) == FHRPGroup:
+            return FHRPGroupType
+        if type(instance) == VMInterface:
+            return VMInterfaceType
+
+
+class L2VPNAssignmentType(graphene.Union):
+    class Meta:
+        types = (
+            InterfaceType,
+            VLANType,
+            VMInterfaceType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == Interface:
+            return InterfaceType
+        if type(instance) == VLAN:
+            return VLANType
+        if type(instance) == VMInterface:
+            return VMInterfaceType
+
+
+class FHRPGroupInterfaceType(graphene.Union):
+    class Meta:
+        types = (
+            InterfaceType,
+            VMInterfaceType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == Interface:
+            return InterfaceType
+        if type(instance) == VMInterface:
+            return VMInterfaceType
+
+
+class VLANGroupScopeType(graphene.Union):
+    class Meta:
+        types = (
+            ClusterType,
+            ClusterGroupType,
+            LocationType,
+            RackType,
+            RegionType,
+            SiteType,
+            SiteGroupType,
+        )
+
+    @classmethod
+    def resolve_type(cls, instance, info):
+        if type(instance) == Cluster:
+            return ClusterType
+        if type(instance) == ClusterGroup:
+            return ClusterGroupType
+        if type(instance) == Location:
+            return LocationType
+        if type(instance) == Rack:
+            return RackType
+        if type(instance) == Region:
+            return RegionType
+        if type(instance) == Site:
+            return SiteType
+        if type(instance) == SiteGroup:
+            return SiteGroupType

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

@@ -1,5 +1,7 @@
 import graphene
 
+from graphene_django import DjangoObjectType
+from extras.graphql.mixins import ContactsMixin
 from ipam import filtersets, models
 from netbox.graphql.scalars import BigInt
 from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType
@@ -54,18 +56,20 @@ class FHRPGroupType(NetBoxObjectType):
 
 
 class FHRPGroupAssignmentType(BaseObjectType):
+    interface = graphene.Field('ipam.graphql.gfk_mixins.FHRPGroupInterfaceType')
 
     class Meta:
         model = models.FHRPGroupAssignment
-        fields = '__all__'
+        exclude = ('interface_type', 'interface_id')
         filterset_class = filtersets.FHRPGroupAssignmentFilterSet
 
 
 class IPAddressType(NetBoxObjectType):
+    assigned_object = graphene.Field('ipam.graphql.gfk_mixins.IPAddressAssignmentType')
 
     class Meta:
         model = models.IPAddress
-        fields = '__all__'
+        exclude = ('assigned_object_type', 'assigned_object_id')
         filterset_class = filtersets.IPAddressFilterSet
 
     def resolve_role(self, info):
@@ -140,10 +144,11 @@ class VLANType(NetBoxObjectType):
 
 
 class VLANGroupType(OrganizationalObjectType):
+    scope = graphene.Field('ipam.graphql.gfk_mixins.VLANGroupScopeType')
 
     class Meta:
         model = models.VLANGroup
-        fields = '__all__'
+        exclude = ('scope_type', 'scope_id')
         filterset_class = filtersets.VLANGroupFilterSet
 
 
@@ -155,7 +160,7 @@ class VRFType(NetBoxObjectType):
         filterset_class = filtersets.VRFFilterSet
 
 
-class L2VPNType(NetBoxObjectType):
+class L2VPNType(ContactsMixin, NetBoxObjectType):
     class Meta:
         model = models.L2VPN
         fields = '__all__'
@@ -163,7 +168,9 @@ class L2VPNType(NetBoxObjectType):
 
 
 class L2VPNTerminationType(NetBoxObjectType):
+    assigned_object = graphene.Field('ipam.graphql.gfk_mixins.L2VPNAssignmentType')
+
     class Meta:
         model = models.L2VPNTermination
-        fields = '__all__'
+        exclude = ('assigned_object_type', 'assigned_object_id')
         filtersets_class = filtersets.L2VPNTerminationFilterSet

+ 3 - 0
netbox/utilities/testing/api.py

@@ -450,6 +450,9 @@ class APIViewTestCases:
                 if type(field) is GQLDynamic:
                     # Dynamic fields must specify a subselection
                     fields_string += f'{field_name} {{ id }}\n'
+                elif inspect.isclass(field.type) and issubclass(field.type, GQLUnion):
+                    # Union types dont' have an id or consistent values
+                    continue
                 elif type(field.type) is GQLList and inspect.isclass(field.type.of_type) and issubclass(field.type.of_type, GQLUnion):
                     # Union types dont' have an id or consistent values
                     continue