diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index e96fe98a5..5582de798 100644 --- a/netbox/circuits/graphql/types.py +++ b/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 diff --git a/netbox/dcim/graphql/gfk_mixins.py b/netbox/dcim/graphql/gfk_mixins.py index d6be138bc..f3b8b696b 100644 --- a/netbox/dcim/graphql/gfk_mixins.py +++ b/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 diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 78cabbcd1..bb414ed00 100644 --- a/netbox/dcim/graphql/types.py +++ b/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 diff --git a/netbox/extras/graphql/mixins.py b/netbox/extras/graphql/mixins.py index 462ba721f..7045575fb 100644 --- a/netbox/extras/graphql/mixins.py +++ b/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()) diff --git a/netbox/ipam/graphql/gfk_mixins.py b/netbox/ipam/graphql/gfk_mixins.py new file mode 100644 index 000000000..31742c4a4 --- /dev/null +++ b/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 diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index 5af2ca72a..b8f6221bc 100644 --- a/netbox/ipam/graphql/types.py +++ b/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 diff --git a/netbox/utilities/testing/api.py b/netbox/utilities/testing/api.py index 8815ede1f..c0836b71d 100644 --- a/netbox/utilities/testing/api.py +++ b/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