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

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 circuits import filtersets, models
 from dcim.graphql.mixins import CabledObjectMixin
 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
 from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObjectType
 
 
 __all__ = (
 __all__ = (
@@ -20,8 +22,7 @@ class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, Ob
         filterset_class = filtersets.CircuitTerminationFilterSet
         filterset_class = filtersets.CircuitTerminationFilterSet
 
 
 
 
-class CircuitType(NetBoxObjectType):
-
+class CircuitType(NetBoxObjectType, ContactsMixin):
     class Meta:
     class Meta:
         model = models.Circuit
         model = models.Circuit
         fields = '__all__'
         fields = '__all__'
@@ -36,7 +37,7 @@ class CircuitTypeType(OrganizationalObjectType):
         filterset_class = filtersets.CircuitTypeFilterSet
         filterset_class = filtersets.CircuitTypeFilterSet
 
 
 
 
-class ProviderType(NetBoxObjectType):
+class ProviderType(NetBoxObjectType, ContactsMixin):
 
 
     class Meta:
     class Meta:
         model = models.Provider
         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.graphql.types import CircuitTerminationType
 from circuits.models import CircuitTermination
 from circuits.models import CircuitTermination
 from dcim.graphql.types import (
 from dcim.graphql.types import (
+    ConsolePortTemplateType,
     ConsolePortType,
     ConsolePortType,
+    ConsoleServerPortTemplateType,
     ConsoleServerPortType,
     ConsoleServerPortType,
+    FrontPortTemplateType,
     FrontPortType,
     FrontPortType,
+    InterfaceTemplateType,
     InterfaceType,
     InterfaceType,
     PowerFeedType,
     PowerFeedType,
+    PowerOutletTemplateType,
     PowerOutletType,
     PowerOutletType,
+    PowerPortTemplateType,
     PowerPortType,
     PowerPortType,
+    RearPortTemplateType,
     RearPortType,
     RearPortType,
 )
 )
 from dcim.models import (
 from dcim.models import (
     ConsolePort,
     ConsolePort,
+    ConsolePortTemplate,
     ConsoleServerPort,
     ConsoleServerPort,
+    ConsoleServerPortTemplate,
     FrontPort,
     FrontPort,
+    FrontPortTemplate,
     Interface,
     Interface,
+    InterfaceTemplate,
     PowerFeed,
     PowerFeed,
     PowerOutlet,
     PowerOutlet,
+    PowerOutletTemplate,
     PowerPort,
     PowerPort,
+    PowerPortTemplate,
     RearPort,
     RearPort,
+    RearPortTemplate,
 )
 )
 
 
 
 
@@ -57,3 +71,99 @@ class LinkPeerType(graphene.Union):
             return PowerPortType
             return PowerPortType
         if type(instance) == RearPort:
         if type(instance) == RearPort:
             return RearPortType
             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 dcim import filtersets, models
 from extras.graphql.mixins import (
 from extras.graphql.mixins import (
-    ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
+    ChangelogMixin, ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
 )
 )
 from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
 from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
 from netbox.graphql.scalars import BigInt
 from netbox.graphql.scalars import BigInt
@@ -87,6 +87,8 @@ class ComponentTemplateObjectType(
 #
 #
 
 
 class CableType(NetBoxObjectType):
 class CableType(NetBoxObjectType):
+    a_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
+    b_terminations = graphene.List('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
 
 
     class Meta:
     class Meta:
         model = models.Cable
         model = models.Cable
@@ -99,12 +101,19 @@ class CableType(NetBoxObjectType):
     def resolve_length_unit(self, info):
     def resolve_length_unit(self, info):
         return self.length_unit or None
         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):
 class CableTerminationType(NetBoxObjectType):
+    termination = graphene.Field('dcim.graphql.gfk_mixins.CableTerminationTerminationType')
 
 
     class Meta:
     class Meta:
         model = models.CableTermination
         model = models.CableTermination
-        fields = '__all__'
+        exclude = ('termination_type', 'termination_id')
         filterset_class = filtersets.CableTerminationFilterSet
         filterset_class = filtersets.CableTerminationFilterSet
 
 
 
 
@@ -152,7 +161,7 @@ class ConsoleServerPortTemplateType(ComponentTemplateObjectType):
         return self.type or None
         return self.type or None
 
 
 
 
-class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 
 
     class Meta:
     class Meta:
         model = models.Device
         model = models.Device
@@ -183,10 +192,11 @@ class DeviceBayTemplateType(ComponentTemplateObjectType):
 
 
 
 
 class InventoryItemTemplateType(ComponentTemplateObjectType):
 class InventoryItemTemplateType(ComponentTemplateObjectType):
+    component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemTemplateComponentType')
 
 
     class Meta:
     class Meta:
         model = models.InventoryItemTemplate
         model = models.InventoryItemTemplate
-        fields = '__all__'
+        exclude = ('component_type', 'component_id')
         filterset_class = filtersets.InventoryItemTemplateFilterSet
         filterset_class = filtersets.InventoryItemTemplateFilterSet
 
 
 
 
@@ -269,10 +279,11 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
 
 
 
 
 class InventoryItemType(ComponentObjectType):
 class InventoryItemType(ComponentObjectType):
+    component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')
 
 
     class Meta:
     class Meta:
         model = models.InventoryItem
         model = models.InventoryItem
-        fields = '__all__'
+        exclude = ('component_type', 'component_id')
         filterset_class = filtersets.InventoryItemFilterSet
         filterset_class = filtersets.InventoryItemFilterSet
 
 
 
 
@@ -284,7 +295,7 @@ class InventoryItemRoleType(OrganizationalObjectType):
         filterset_class = filtersets.InventoryItemRoleFilterSet
         filterset_class = filtersets.InventoryItemRoleFilterSet
 
 
 
 
-class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectType):
+class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType):
 
 
     class Meta:
     class Meta:
         model = models.Location
         model = models.Location
@@ -292,7 +303,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, OrganizationalObjectT
         filterset_class = filtersets.LocationFilterSet
         filterset_class = filtersets.LocationFilterSet
 
 
 
 
-class ManufacturerType(OrganizationalObjectType):
+class ManufacturerType(OrganizationalObjectType, ContactsMixin):
 
 
     class Meta:
     class Meta:
         model = models.Manufacturer
         model = models.Manufacturer
@@ -379,7 +390,7 @@ class PowerOutletTemplateType(ComponentTemplateObjectType):
         return self.type or None
         return self.type or None
 
 
 
 
-class PowerPanelType(NetBoxObjectType):
+class PowerPanelType(NetBoxObjectType, ContactsMixin):
 
 
     class Meta:
     class Meta:
         model = models.PowerPanel
         model = models.PowerPanel
@@ -409,7 +420,7 @@ class PowerPortTemplateType(ComponentTemplateObjectType):
         return self.type or None
         return self.type or None
 
 
 
 
-class RackType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
 
 
     class Meta:
     class Meta:
         model = models.Rack
         model = models.Rack
@@ -458,7 +469,7 @@ class RearPortTemplateType(ComponentTemplateObjectType):
         filterset_class = filtersets.RearPortTemplateFilterSet
         filterset_class = filtersets.RearPortTemplateFilterSet
 
 
 
 
-class RegionType(VLANGroupsMixin, OrganizationalObjectType):
+class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
 
 
     class Meta:
     class Meta:
         model = models.Region
         model = models.Region
@@ -466,7 +477,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
         filterset_class = filtersets.RegionFilterSet
         filterset_class = filtersets.RegionFilterSet
 
 
 
 
-class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
+class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
     asn = graphene.Field(BigInt)
     asn = graphene.Field(BigInt)
 
 
     class Meta:
     class Meta:
@@ -475,7 +486,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, NetBoxObjectType):
         filterset_class = filtersets.SiteFilterSet
         filterset_class = filtersets.SiteFilterSet
 
 
 
 
-class SiteGroupType(VLANGroupsMixin, OrganizationalObjectType):
+class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
 
 
     class Meta:
     class Meta:
         model = models.SiteGroup
         model = models.SiteGroup

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

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

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

@@ -450,6 +450,9 @@ class APIViewTestCases:
                 if type(field) is GQLDynamic:
                 if type(field) is GQLDynamic:
                     # Dynamic fields must specify a subselection
                     # Dynamic fields must specify a subselection
                     fields_string += f'{field_name} {{ id }}\n'
                     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):
                 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
                     # Union types dont' have an id or consistent values
                     continue
                     continue