From c35f5f829a97bb9ecaf65b255101f38c80a82250 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 7 Mar 2025 13:49:06 -0500 Subject: [PATCH] Closes #7598: Enable custom field filtering for GraphQL (#18701) --- docs/development/adding-models.md | 8 +- docs/integrations/graphql-api.md | 39 +- docs/plugins/development/filtersets.md | 2 +- netbox/circuits/graphql/enums.py | 20 + netbox/circuits/graphql/filter_mixins.py | 19 + netbox/circuits/graphql/filters.py | 183 +++- netbox/circuits/graphql/types.py | 14 +- netbox/core/graphql/filter_mixins.py | 36 + netbox/core/graphql/filters.py | 81 +- netbox/core/graphql/mixins.py | 5 +- netbox/core/graphql/types.py | 7 + netbox/dcim/graphql/enums.py | 77 ++ netbox/dcim/graphql/filter_mixins.py | 149 ++++ netbox/dcim/graphql/filters.py | 806 +++++++++++++++--- netbox/dcim/graphql/types.py | 55 +- netbox/extras/graphql/enums.py | 26 + netbox/extras/graphql/filter_mixins.py | 52 ++ netbox/extras/graphql/filters.py | 288 ++++++- netbox/extras/graphql/types.py | 21 +- netbox/ipam/graphql/enums.py | 27 + netbox/ipam/graphql/filter_mixins.py | 25 + netbox/ipam/graphql/filters.py | 285 +++++-- netbox/ipam/graphql/types.py | 27 +- netbox/netbox/graphql/enums.py | 13 + netbox/netbox/graphql/filter_lookups.py | 219 +++++ netbox/netbox/graphql/filter_mixins.py | 296 +++---- netbox/netbox/graphql/types.py | 1 + netbox/netbox/settings.py | 1 - netbox/netbox/tests/test_graphql.py | 4 +- netbox/tenancy/graphql/enums.py | 9 + netbox/tenancy/graphql/filter_mixins.py | 38 + netbox/tenancy/graphql/filters.py | 178 +++- netbox/tenancy/graphql/mixins.py | 1 - netbox/tenancy/graphql/types.py | 138 +-- netbox/users/graphql/filters.py | 31 +- netbox/utilities/choices.py | 19 + netbox/virtualization/graphql/enums.py | 11 + .../virtualization/graphql/filter_mixins.py | 26 + netbox/virtualization/graphql/filters.py | 149 +++- netbox/virtualization/graphql/types.py | 19 +- netbox/vpn/graphql/enums.py | 31 + netbox/vpn/graphql/filters.py | 161 +++- netbox/vpn/graphql/types.py | 11 +- netbox/wireless/graphql/enums.py | 17 + netbox/wireless/graphql/filter_mixins.py | 26 + netbox/wireless/graphql/filters.py | 62 +- netbox/wireless/graphql/types.py | 9 +- 47 files changed, 3041 insertions(+), 681 deletions(-) create mode 100644 netbox/circuits/graphql/enums.py create mode 100644 netbox/circuits/graphql/filter_mixins.py create mode 100644 netbox/core/graphql/filter_mixins.py create mode 100644 netbox/dcim/graphql/enums.py create mode 100644 netbox/dcim/graphql/filter_mixins.py create mode 100644 netbox/extras/graphql/enums.py create mode 100644 netbox/extras/graphql/filter_mixins.py create mode 100644 netbox/ipam/graphql/enums.py create mode 100644 netbox/ipam/graphql/filter_mixins.py create mode 100644 netbox/netbox/graphql/enums.py create mode 100644 netbox/netbox/graphql/filter_lookups.py create mode 100644 netbox/tenancy/graphql/enums.py create mode 100644 netbox/tenancy/graphql/filter_mixins.py create mode 100644 netbox/virtualization/graphql/enums.py create mode 100644 netbox/virtualization/graphql/filter_mixins.py create mode 100644 netbox/vpn/graphql/enums.py create mode 100644 netbox/wireless/graphql/enums.py create mode 100644 netbox/wireless/graphql/filter_mixins.py diff --git a/docs/development/adding-models.md b/docs/development/adding-models.md index 0bf020662..da0e49511 100644 --- a/docs/development/adding-models.md +++ b/docs/development/adding-models.md @@ -76,11 +76,13 @@ Create the following for each model: ## 13. GraphQL API components -Create a GraphQL object type for the model in `graphql/types.py` by subclassing the appropriate class from `netbox.graphql.types`. +Create the following for each model: -**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["policy"])` or similar. +* GraphQL object type for the model in `graphql/types.py` (subclass the appropriate class from `netbox.graphql.types`) +* Add a GraphQL filter for the model in `graphql/filters.py` +* Extend the query class for the app in `graphql/schema.py` with the individual object and object list fields -Also extend the schema class defined in `graphql/schema.py` with the individual object and object list fields per the established convention. +**Note:** GraphQL unit tests may fail citing null values on a non-nullable field if related objects are prefetched. You may need to fix this by setting the type annotation to be `= strawberry_django.field(select_related=["foo"])` or similar. ## 14. Add tests diff --git a/docs/integrations/graphql-api.md b/docs/integrations/graphql-api.md index c02045f34..23824ad2b 100644 --- a/docs/integrations/graphql-api.md +++ b/docs/integrations/graphql-api.md @@ -11,7 +11,7 @@ curl -H "Authorization: Token $TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ http://netbox/graphql/ \ ---data '{"query": "query {circuit_list(status:\"active\") {cid provider {name}}}"}' +--data '{"query": "query {circuit_list(filters:{status: STATUS_ACTIVE}) {cid provider {name}}}"}' ``` The response will include the requested data formatted as JSON: @@ -51,19 +51,48 @@ For more detail on constructing GraphQL queries, see the [GraphQL queries docume ## Filtering -The GraphQL API employs the same filtering logic as the UI and REST API. Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only sites within the North Carolina region with a status of active: +!!! note "Changed in NetBox v4.3" + The filtering syntax fo the GraphQL API has changed substantially in NetBox v4.3. + +Filters can be specified as key-value pairs within parentheses immediately following the query name. For example, the following will return only active sites: ``` query { - site_list(filters: {region: "us-nc", status: "active"}) { + site_list( + filters: { + status: STATUS_ACTIVE + } + ) { name } } ``` -In addition, filtering can be done on list of related objects as shown in the following query: + +Filters can be combined with logical operators, such as `OR` and `NOT`. For example, the following will return every site that is planned _or_ assigned to a tenant named Foo: ``` -{ +query { + site_list( + filters: { + status: STATUS_PLANNED, + OR: { + tenant: { + name: { + exact: "Foo" + } + } + } + } + ) { + name + } +} +``` + +Filtering can also be applied to related objects. For example, the following query will return only enabled interfaces for each device: + +``` +query { device_list { id name diff --git a/docs/plugins/development/filtersets.md b/docs/plugins/development/filtersets.md index d803ce2f4..224802397 100644 --- a/docs/plugins/development/filtersets.md +++ b/docs/plugins/development/filtersets.md @@ -1,6 +1,6 @@ # Filters & Filter Sets -Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI, REST API, or GraphQL API. NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets. +Filter sets define the mechanisms available for filtering or searching through a set of objects in NetBox. For instance, sites can be filtered by their parent region or group, status, facility ID, and so on. The same filter set is used consistently for a model whether the request is made via the UI or REST API. (Note that the GraphQL API uses a separate filter class.) NetBox employs the [django-filters2](https://django-tables2.readthedocs.io/en/latest/) library to define filter sets. ## FilterSet Classes diff --git a/netbox/circuits/graphql/enums.py b/netbox/circuits/graphql/enums.py new file mode 100644 index 000000000..609e4435b --- /dev/null +++ b/netbox/circuits/graphql/enums.py @@ -0,0 +1,20 @@ +import strawberry + +from circuits.choices import * + +__all__ = ( + 'CircuitStatusEnum', + 'CircuitCommitRateEnum', + 'CircuitTerminationSideEnum', + 'CircuitTerminationPortSpeedEnum', + 'CircuitPriorityEnum', + 'VirtualCircuitTerminationRoleEnum', +) + + +CircuitCommitRateEnum = strawberry.enum(CircuitCommitRateChoices.as_enum()) +CircuitPriorityEnum = strawberry.enum(CircuitPriorityChoices.as_enum()) +CircuitStatusEnum = strawberry.enum(CircuitStatusChoices.as_enum()) +CircuitTerminationSideEnum = strawberry.enum(CircuitTerminationSideChoices.as_enum()) +CircuitTerminationPortSpeedEnum = strawberry.enum(CircuitTerminationPortSpeedChoices.as_enum()) +VirtualCircuitTerminationRoleEnum = strawberry.enum(VirtualCircuitTerminationRoleChoices.as_enum()) diff --git a/netbox/circuits/graphql/filter_mixins.py b/netbox/circuits/graphql/filter_mixins.py new file mode 100644 index 000000000..3ae6fa82e --- /dev/null +++ b/netbox/circuits/graphql/filter_mixins.py @@ -0,0 +1,19 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django + +from netbox.graphql.filter_mixins import OrganizationalModelFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.enums import ColorEnum + +__all__ = ( + 'BaseCircuitTypeFilterMixin', +) + + +@dataclass +class BaseCircuitTypeFilterMixin(OrganizationalModelFilterMixin): + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() diff --git a/netbox/circuits/graphql/filters.py b/netbox/circuits/graphql/filters.py index 7d066f428..48b4252ac 100644 --- a/netbox/circuits/graphql/filters.py +++ b/netbox/circuits/graphql/filters.py @@ -1,7 +1,30 @@ -import strawberry_django +from datetime import date +from typing import Annotated, TYPE_CHECKING -from circuits import filtersets, models -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup, DateFilterLookup + +from circuits import models +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin +from dcim.graphql.filter_mixins import CabledObjectModelFilterMixin +from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin +from netbox.graphql.filter_mixins import ( + DistanceFilterMixin, + ImageAttachmentFilterMixin, + OrganizationalModelFilterMixin, + PrimaryModelFilterMixin, +) +from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin +from .filter_mixins import BaseCircuitTypeFilterMixin + +if TYPE_CHECKING: + from core.graphql.filters import ContentTypeFilter + from dcim.graphql.filters import InterfaceFilter + from ipam.graphql.filters import ASNFilter + from netbox.graphql.filter_lookups import IntegerLookup + from .enums import * __all__ = ( 'CircuitFilter', @@ -19,66 +42,160 @@ __all__ = ( @strawberry_django.filter(models.CircuitTermination, lookups=True) -@autotype_decorator(filtersets.CircuitTerminationFilterSet) -class CircuitTerminationFilter(BaseFilterMixin): - pass +class CircuitTerminationFilter( + BaseObjectTypeFilterMixin, + CustomFieldsFilterMixin, + TagsFilterMixin, + ChangeLogFilterMixin, + CabledObjectModelFilterMixin, +): + circuit: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + term_side: Annotated['CircuitTerminationSideEnum', strawberry.lazy('circuits.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + termination_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + termination_id: ID | None = strawberry_django.filter_field() + port_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + upstream_speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + xconnect_id: FilterLookup[str] | None = strawberry_django.filter_field() + pp_info: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Circuit, lookups=True) -@autotype_decorator(filtersets.CircuitFilterSet) -class CircuitFilter(BaseFilterMixin): - pass +class CircuitFilter( + ContactFilterMixin, + ImageAttachmentFilterMixin, + DistanceFilterMixin, + TenancyFilterMixin, + PrimaryModelFilterMixin +): + cid: FilterLookup[str] | None = strawberry_django.filter_field() + provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_id: ID | None = strawberry_django.filter_field() + provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_account_id: ID | None = strawberry_django.filter_field() + type: Annotated['CircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + type_id: ID | None = strawberry_django.filter_field() + status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + install_date: DateFilterLookup[date] | None = strawberry_django.filter_field() + termination_date: DateFilterLookup[date] | None = strawberry_django.filter_field() + commit_rate: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.CircuitType, lookups=True) -@autotype_decorator(filtersets.CircuitTypeFilterSet) -class CircuitTypeFilter(BaseFilterMixin): +class CircuitTypeFilter(BaseCircuitTypeFilterMixin): pass @strawberry_django.filter(models.CircuitGroup, lookups=True) -@autotype_decorator(filtersets.CircuitGroupFilterSet) -class CircuitGroupFilter(BaseFilterMixin): +class CircuitGroupFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): pass @strawberry_django.filter(models.CircuitGroupAssignment, lookups=True) -@autotype_decorator(filtersets.CircuitGroupAssignmentFilterSet) -class CircuitGroupAssignmentFilter(BaseFilterMixin): - pass +class CircuitGroupAssignmentFilter( + BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin +): + member_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + member_id: ID | None = strawberry_django.filter_field() + group: Annotated['CircuitGroupFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + priority: Annotated['CircuitPriorityEnum', strawberry.lazy('circuits.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Provider, lookups=True) -@autotype_decorator(filtersets.ProviderFilterSet) -class ProviderFilter(BaseFilterMixin): - pass +class ProviderFilter(ContactFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ProviderAccount, lookups=True) -@autotype_decorator(filtersets.ProviderAccountFilterSet) -class ProviderAccountFilter(BaseFilterMixin): - pass +class ProviderAccountFilter(ContactFilterMixin, PrimaryModelFilterMixin): + provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_id: ID | None = strawberry_django.filter_field() + account: FilterLookup[str] | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ProviderNetwork, lookups=True) -@autotype_decorator(filtersets.ProviderNetworkFilterSet) -class ProviderNetworkFilter(BaseFilterMixin): - pass +class ProviderNetworkFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + provider: Annotated['ProviderFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_id: ID | None = strawberry_django.filter_field() + service_id: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.VirtualCircuitType, lookups=True) -@autotype_decorator(filtersets.VirtualCircuitTypeFilterSet) -class VirtualCircuitTypeFilter(BaseFilterMixin): +class VirtualCircuitTypeFilter(BaseCircuitTypeFilterMixin): pass @strawberry_django.filter(models.VirtualCircuit, lookups=True) -@autotype_decorator(filtersets.VirtualCircuitFilterSet) -class VirtualCircuitFilter(BaseFilterMixin): - pass +class VirtualCircuitFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + cid: FilterLookup[str] | None = strawberry_django.filter_field() + provider_network: Annotated['ProviderNetworkFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_network_id: ID | None = strawberry_django.filter_field() + provider_account: Annotated['ProviderAccountFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + provider_account_id: ID | None = strawberry_django.filter_field() + type: Annotated['VirtualCircuitTypeFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + type_id: ID | None = strawberry_django.filter_field() + status: Annotated['CircuitStatusEnum', strawberry.lazy('circuits.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + group_assignments: Annotated['CircuitGroupAssignmentFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VirtualCircuitTermination, lookups=True) -@autotype_decorator(filtersets.VirtualCircuitTerminationFilterSet) -class VirtualCircuitTerminationFilter(BaseFilterMixin): - pass +class VirtualCircuitTerminationFilter( + BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin +): + virtual_circuit: Annotated['VirtualCircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + virtual_circuit_id: ID | None = strawberry_django.filter_field() + role: Annotated['VirtualCircuitTerminationRoleEnum', strawberry.lazy('circuits.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + interface: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + interface_id: ID | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/circuits/graphql/types.py b/netbox/circuits/graphql/types.py index 564b5ed6f..860c19852 100644 --- a/netbox/circuits/graphql/types.py +++ b/netbox/circuits/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -10,11 +10,15 @@ from netbox.graphql.types import BaseObjectType, NetBoxObjectType, ObjectType, O from tenancy.graphql.types import TenantType from .filters import * +if TYPE_CHECKING: + from dcim.graphql.types import InterfaceType, LocationType, RegionType, SiteGroupType, SiteType + from ipam.graphql.types import ASNType + __all__ = ( - 'CircuitTerminationType', - 'CircuitType', 'CircuitGroupAssignmentType', 'CircuitGroupType', + 'CircuitTerminationType', + 'CircuitType', 'CircuitTypeType', 'ProviderType', 'ProviderAccountType', @@ -62,7 +66,7 @@ class ProviderNetworkType(NetBoxObjectType): @strawberry_django.type( models.CircuitTermination, - exclude=('termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'), + exclude=['termination_type', 'termination_id', '_location', '_region', '_site', '_site_group', '_provider_network'], filters=CircuitTerminationFilter ) class CircuitTerminationType(CustomFieldsMixin, TagsMixin, CabledObjectMixin, ObjectType): @@ -117,7 +121,7 @@ class CircuitGroupType(OrganizationalObjectType): @strawberry_django.type( models.CircuitGroupAssignment, - exclude=('member_type', 'member_id'), + exclude=['member_type', 'member_id'], filters=CircuitGroupAssignmentFilter ) class CircuitGroupAssignmentType(TagsMixin, BaseObjectType): diff --git a/netbox/core/graphql/filter_mixins.py b/netbox/core/graphql/filter_mixins.py new file mode 100644 index 000000000..670ec2ebb --- /dev/null +++ b/netbox/core/graphql/filter_mixins.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry import ID +from strawberry_django import DatetimeFilterLookup + +if TYPE_CHECKING: + from .filters import * + +__all__ = ( + 'BaseFilterMixin', + 'BaseObjectTypeFilterMixin', + 'ChangeLogFilterMixin', +) + + +# @strawberry.input +class BaseFilterMixin: ... + + +@dataclass +class BaseObjectTypeFilterMixin(BaseFilterMixin): + id: ID | None = strawberry.UNSET + + +@dataclass +class ChangeLogFilterMixin(BaseFilterMixin): + id: ID | None = strawberry.UNSET + changelog: Annotated['ObjectChangeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() diff --git a/netbox/core/graphql/filters.py b/netbox/core/graphql/filters.py index 82da685a5..e5d44674a 100644 --- a/netbox/core/graphql/filters.py +++ b/netbox/core/graphql/filters.py @@ -1,28 +1,89 @@ -import strawberry_django +from datetime import datetime +from typing import Annotated, TYPE_CHECKING -from core import filtersets, models -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +import strawberry +import strawberry_django +from django.contrib.contenttypes.models import ContentType as DjangoContentType +from strawberry.scalars import ID +from strawberry_django import DatetimeFilterLookup, FilterLookup + +from core import models +from core.graphql.filter_mixins import BaseFilterMixin +from netbox.graphql.filter_mixins import PrimaryModelFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import IntegerLookup, JSONFilter + from users.graphql.filters import UserFilter __all__ = ( 'DataFileFilter', 'DataSourceFilter', 'ObjectChangeFilter', + 'ContentTypeFilter', ) @strawberry_django.filter(models.DataFile, lookups=True) -@autotype_decorator(filtersets.DataFileFilterSet) class DataFileFilter(BaseFilterMixin): - pass + id: ID | None = strawberry_django.filter_field() + created: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + last_updated: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + source_id: ID | None = strawberry_django.filter_field() + path: FilterLookup[str] | None = strawberry_django.filter_field() + size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + hash: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.DataSource, lookups=True) -@autotype_decorator(filtersets.DataSourceFilterSet) -class DataSourceFilter(BaseFilterMixin): - pass +class DataSourceFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + type: FilterLookup[str] | None = strawberry_django.filter_field() + source_url: FilterLookup[str] | None = strawberry_django.filter_field() + status: FilterLookup[str] | None = strawberry_django.filter_field() + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + ignore_rules: FilterLookup[str] | None = strawberry_django.filter_field() + parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + last_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + datafiles: Annotated['DataFileFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ObjectChange, lookups=True) -@autotype_decorator(filtersets.ObjectChangeFilterSet) class ObjectChangeFilter(BaseFilterMixin): - pass + id: ID | None = strawberry_django.filter_field() + time: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_name: FilterLookup[str] | None = strawberry_django.filter_field() + request_id: FilterLookup[str] | None = strawberry_django.filter_field() + action: FilterLookup[str] | None = strawberry_django.filter_field() + changed_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + changed_object_type_id: ID | None = strawberry_django.filter_field() + changed_object_id: ID | None = strawberry_django.filter_field() + related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + related_object_id: ID | None = strawberry_django.filter_field() + object_repr: FilterLookup[str] | None = strawberry_django.filter_field() + prechange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + postchange_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + + +@strawberry_django.filter(DjangoContentType, lookups=True) +class ContentTypeFilter(BaseFilterMixin): + id: ID | None = strawberry_django.filter_field() + app_label: FilterLookup[str] | None = strawberry_django.filter_field() + model: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/core/graphql/mixins.py b/netbox/core/graphql/mixins.py index 5195b52a0..72191e6fd 100644 --- a/netbox/core/graphql/mixins.py +++ b/netbox/core/graphql/mixins.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated, List, TYPE_CHECKING import strawberry import strawberry_django @@ -6,6 +6,9 @@ from django.contrib.contenttypes.models import ContentType from core.models import ObjectChange +if TYPE_CHECKING: + from netbox.core.graphql.types import ObjectChangeType + __all__ = ( 'ChangelogMixin', ) diff --git a/netbox/core/graphql/types.py b/netbox/core/graphql/types.py index 09385d7c1..1c57a2dbc 100644 --- a/netbox/core/graphql/types.py +++ b/netbox/core/graphql/types.py @@ -2,12 +2,14 @@ from typing import Annotated, List import strawberry import strawberry_django +from django.contrib.contenttypes.models import ContentType as DjangoContentType from core import models from netbox.graphql.types import BaseObjectType, NetBoxObjectType from .filters import * __all__ = ( + 'ContentType', 'DataFileType', 'DataSourceType', 'ObjectChangeType', @@ -40,3 +42,8 @@ class DataSourceType(NetBoxObjectType): ) class ObjectChangeType(BaseObjectType): pass + + +@strawberry_django.type(DjangoContentType, fields='__all__') +class ContentType: + pass diff --git a/netbox/dcim/graphql/enums.py b/netbox/dcim/graphql/enums.py new file mode 100644 index 000000000..a60c0df14 --- /dev/null +++ b/netbox/dcim/graphql/enums.py @@ -0,0 +1,77 @@ +import strawberry + +from dcim.choices import * + +__all__ = ( + 'CableEndEnum', + 'CableLengthUnitEnum', + 'CableTypeEnum', + 'ConsolePortSpeedEnum', + 'ConsolePortTypeEnum', + 'DeviceAirflowEnum', + 'DeviceFaceEnum', + 'DeviceStatusEnum', + 'InterfaceDuplexEnum', + 'InterfaceModeEnum', + 'InterfacePoEModeEnum', + 'InterfacePoETypeEnum', + 'InterfaceSpeedEnum', + 'InterfaceTypeEnum', + 'InventoryItemStatusEnum', + 'LinkStatusEnum', + 'LocationStatusEnum', + 'ModuleAirflowEnum', + 'ModuleStatusEnum', + 'PortTypeEnum', + 'PowerFeedPhaseEnum', + 'PowerFeedStatusEnum', + 'PowerFeedSupplyEnum', + 'PowerFeedTypeEnum', + 'PowerOutletFeedLegEnum', + 'PowerOutletTypeEnum', + 'PowerPortTypeEnum', + 'RackAirflowEnum', + 'RackDimensionUnitEnum', + 'RackFormFactorEnum', + 'RackStatusEnum', + 'RackWidthEnum', + 'SiteStatusEnum', + 'SubdeviceRoleEnum', + 'VirtualDeviceContextStatusEnum', +) + +CableEndEnum = strawberry.enum(CableEndChoices.as_enum()) +CableLengthUnitEnum = strawberry.enum(CableLengthUnitChoices.as_enum()) +CableTypeEnum = strawberry.enum(CableTypeChoices.as_enum()) +ConsolePortSpeedEnum = strawberry.enum(ConsolePortSpeedChoices.as_enum()) +ConsolePortTypeEnum = strawberry.enum(ConsolePortTypeChoices.as_enum()) +DeviceAirflowEnum = strawberry.enum(DeviceAirflowChoices.as_enum()) +DeviceFaceEnum = strawberry.enum(DeviceFaceChoices.as_enum()) +DeviceStatusEnum = strawberry.enum(DeviceStatusChoices.as_enum()) +InterfaceDuplexEnum = strawberry.enum(InterfaceDuplexChoices.as_enum()) +InterfaceModeEnum = strawberry.enum(InterfaceModeChoices.as_enum()) +InterfacePoEModeEnum = strawberry.enum(InterfacePoEModeChoices.as_enum()) +InterfacePoETypeEnum = strawberry.enum(InterfacePoETypeChoices.as_enum()) +InterfaceSpeedEnum = strawberry.enum(InterfaceSpeedChoices.as_enum()) +InterfaceTypeEnum = strawberry.enum(InterfaceTypeChoices.as_enum()) +InventoryItemStatusEnum = strawberry.enum(InventoryItemStatusChoices.as_enum()) +LinkStatusEnum = strawberry.enum(LinkStatusChoices.as_enum()) +LocationStatusEnum = strawberry.enum(LocationStatusChoices.as_enum()) +ModuleAirflowEnum = strawberry.enum(ModuleAirflowChoices.as_enum()) +ModuleStatusEnum = strawberry.enum(ModuleStatusChoices.as_enum()) +PortTypeEnum = strawberry.enum(PortTypeChoices.as_enum()) +PowerFeedPhaseEnum = strawberry.enum(PowerFeedPhaseChoices.as_enum()) +PowerFeedStatusEnum = strawberry.enum(PowerFeedStatusChoices.as_enum()) +PowerFeedSupplyEnum = strawberry.enum(PowerFeedSupplyChoices.as_enum()) +PowerFeedTypeEnum = strawberry.enum(PowerFeedTypeChoices.as_enum()) +PowerOutletFeedLegEnum = strawberry.enum(PowerOutletFeedLegChoices.as_enum()) +PowerOutletTypeEnum = strawberry.enum(PowerOutletTypeChoices.as_enum()) +PowerPortTypeEnum = strawberry.enum(PowerPortTypeChoices.as_enum()) +RackAirflowEnum = strawberry.enum(RackAirflowChoices.as_enum()) +RackDimensionUnitEnum = strawberry.enum(RackDimensionUnitChoices.as_enum()) +RackFormFactorEnum = strawberry.enum(RackFormFactorChoices.as_enum()) +RackStatusEnum = strawberry.enum(RackStatusChoices.as_enum()) +RackWidthEnum = strawberry.enum(RackWidthChoices.as_enum()) +SiteStatusEnum = strawberry.enum(SiteStatusChoices.as_enum()) +SubdeviceRoleEnum = strawberry.enum(SubdeviceRoleChoices.as_enum()) +VirtualDeviceContextStatusEnum = strawberry.enum(VirtualDeviceContextStatusChoices.as_enum()) diff --git a/netbox/dcim/graphql/filter_mixins.py b/netbox/dcim/graphql/filter_mixins.py new file mode 100644 index 000000000..47a75d08e --- /dev/null +++ b/netbox/dcim/graphql/filter_mixins.py @@ -0,0 +1,149 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry import ID +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import BaseFilterMixin, ChangeLogFilterMixin +from core.graphql.filters import ContentTypeFilter +from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin +from .enums import * + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import IntegerLookup + from extras.graphql.filters import ConfigTemplateFilter + from ipam.graphql.filters import VLANFilter, VLANTranslationPolicyFilter + from .filters import * + +__all__ = ( + 'CabledObjectModelFilterMixin', + 'ComponentModelFilterMixin', + 'ComponentTemplateFilterMixin', + 'InterfaceBaseFilterMixin', + 'ModularComponentModelFilterMixin', + 'ModularComponentTemplateFilterMixin', + 'RackBaseFilterMixin', + 'RenderConfigFilterMixin', + 'ScopedFilterMixin', +) + + +@dataclass +class ScopedFilterMixin(BaseFilterMixin): + scope_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + scope_id: ID | None = strawberry_django.filter_field() + + +@dataclass +class ComponentModelFilterMixin(NetBoxModelFilterMixin): + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + label: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + + +@dataclass +class ModularComponentModelFilterMixin(ComponentModelFilterMixin): + module: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + module_id: ID | None = strawberry_django.filter_field() + inventory_items: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class CabledObjectModelFilterMixin(BaseFilterMixin): + cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + cable_id: ID | None = strawberry_django.filter_field() + cable_end: CableEndEnum | None = strawberry_django.filter_field() + mark_connected: FilterLookup[bool] | None = strawberry_django.filter_field() + + +@dataclass +class ComponentTemplateFilterMixin(ChangeLogFilterMixin): + device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + device_type_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + label: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + + +@dataclass +class ModularComponentTemplateFilterMixin(ComponentTemplateFilterMixin): + module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class RenderConfigFilterMixin(BaseFilterMixin): + config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + config_template_id: ID | None = strawberry_django.filter_field() + + +@dataclass +class InterfaceBaseFilterMixin(BaseFilterMixin): + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + mtu: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + mode: InterfaceModeEnum | None = strawberry_django.filter_field() + parent: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + parent_id: ID | None = strawberry_django.filter_field() + bridge: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + bridge_id: ID | None = strawberry_django.filter_field() + untagged_vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tagged_vlans: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + qinq_svlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vlan_translation_policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None \ + = strawberry_django.filter_field() + primary_mac_address: Annotated['MACAddressFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_mac_address_id: ID | None = strawberry_django.filter_field() + + +@dataclass +class RackBaseFilterMixin(WeightFilterMixin, PrimaryModelFilterMixin): + width: Annotated['RackWidthEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + u_height: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + starting_unit: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + desc_units: FilterLookup[bool] | None = strawberry_django.filter_field() + outer_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + outer_depth: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + outer_unit: Annotated['RackDimensionUnitEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + mounting_depth: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + max_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/dcim/graphql/filters.py b/netbox/dcim/graphql/filters.py index 94f2c6d38..4203517cc 100644 --- a/netbox/dcim/graphql/filters.py +++ b/netbox/dcim/graphql/filters.py @@ -1,7 +1,46 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from dcim import filtersets, models -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import ChangeLogFilterMixin +from dcim import models +from extras.graphql.filter_mixins import ConfigContextFilterMixin +from netbox.graphql.filter_mixins import ( + PrimaryModelFilterMixin, + OrganizationalModelFilterMixin, + NestedGroupModelFilterMixin, + ImageAttachmentFilterMixin, + WeightFilterMixin, +) +from tenancy.graphql.filter_mixins import TenancyFilterMixin, ContactFilterMixin +from .filter_mixins import ( + CabledObjectModelFilterMixin, + ComponentModelFilterMixin, + ComponentTemplateFilterMixin, + InterfaceBaseFilterMixin, + ModularComponentModelFilterMixin, + ModularComponentTemplateFilterMixin, + RackBaseFilterMixin, + RenderConfigFilterMixin, +) + +if TYPE_CHECKING: + from core.graphql.filters import ContentTypeFilter + from extras.graphql.filters import ConfigTemplateFilter, ImageAttachmentFilter + from ipam.graphql.filters import ( + ASNFilter, FHRPGroupAssignmentFilter, IPAddressFilter, PrefixFilter, VLANGroupFilter, VRFFilter, + ) + from netbox.graphql.enums import ColorEnum + from netbox.graphql.filter_lookups import FloatLookup, IntegerArrayLookup, IntegerLookup, TreeNodeFilter + from users.graphql.filters import UserFilter + from virtualization.graphql.filters import ClusterFilter + from vpn.graphql.filters import L2VPNFilter, TunnelTerminationFilter + from wireless.graphql.enums import WirelessChannelEnum, WirelessRoleEnum + from wireless.graphql.filters import WirelessLANFilter, WirelessLinkFilter + from .enums import * __all__ = ( 'CableFilter', @@ -13,7 +52,6 @@ __all__ = ( 'DeviceFilter', 'DeviceBayFilter', 'DeviceBayTemplateFilter', - 'InventoryItemTemplateFilter', 'DeviceRoleFilter', 'DeviceTypeFilter', 'FrontPortFilter', @@ -22,6 +60,7 @@ __all__ = ( 'InterfaceTemplateFilter', 'InventoryItemFilter', 'InventoryItemRoleFilter', + 'InventoryItemTemplateFilter', 'LocationFilter', 'MACAddressFilter', 'ManufacturerFilter', @@ -51,258 +90,763 @@ __all__ = ( @strawberry_django.filter(models.Cable, lookups=True) -@autotype_decorator(filtersets.CableFilterSet) -class CableFilter(BaseFilterMixin): - pass +class CableFilter(PrimaryModelFilterMixin, TenancyFilterMixin): + type: Annotated['CableTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + status: Annotated['LinkStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + label: FilterLookup[str] | None = strawberry_django.filter_field() + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + length: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + length_unit: Annotated['CableLengthUnitEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + terminations: Annotated['CableTerminationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.CableTermination, lookups=True) -@autotype_decorator(filtersets.CableTerminationFilterSet) -class CableTerminationFilter(BaseFilterMixin): - pass +class CableTerminationFilter(ChangeLogFilterMixin): + cable: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + cable_id: ID | None = strawberry_django.filter_field() + cable_end: Annotated['CableEndEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + termination_type: Annotated['CableTerminationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + termination_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.ConsolePort, lookups=True) -@autotype_decorator(filtersets.ConsolePortFilterSet) -class ConsolePortFilter(BaseFilterMixin): - pass +class ConsolePortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + speed: Annotated['ConsolePortSpeedEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ConsolePortTemplate, lookups=True) -@autotype_decorator(filtersets.ConsolePortTemplateFilterSet) -class ConsolePortTemplateFilter(BaseFilterMixin): - pass +class ConsolePortTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ConsoleServerPort, lookups=True) -@autotype_decorator(filtersets.ConsoleServerPortFilterSet) -class ConsoleServerPortFilter(BaseFilterMixin): - pass +class ConsoleServerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + speed: Annotated['ConsolePortSpeedEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ConsoleServerPortTemplate, lookups=True) -@autotype_decorator(filtersets.ConsoleServerPortTemplateFilterSet) -class ConsoleServerPortTemplateFilter(BaseFilterMixin): - pass +class ConsoleServerPortTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['ConsolePortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Device, lookups=True) -@autotype_decorator(filtersets.DeviceFilterSet) -class DeviceFilter(BaseFilterMixin): - pass +class DeviceFilter( + ContactFilterMixin, + TenancyFilterMixin, + ImageAttachmentFilterMixin, + RenderConfigFilterMixin, + ConfigContextFilterMixin, + PrimaryModelFilterMixin, +): + device_type: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + device_type_id: ID | None = strawberry_django.filter_field() + role: Annotated['DeviceRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + role_id: ID | None = strawberry_django.filter_field() + platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() + serial: FilterLookup[str] | None = strawberry_django.filter_field() + asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + rack_id: ID | None = strawberry_django.filter_field() + position: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + face: Annotated['DeviceFaceEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + status: Annotated['DeviceStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + airflow: Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip4_id: ID | None = strawberry_django.filter_field() + primary_ip6: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip6_id: ID | None = strawberry_django.filter_field() + oob_ip: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + oob_ip_id: ID | None = strawberry_django.filter_field() + cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cluster_id: ID | None = strawberry_django.filter_field() + virtual_chassis: Annotated['VirtualChassisFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + virtual_chassis_id: ID | None = strawberry_django.filter_field() + vc_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + vc_priority: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + interfaces: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + consoleports: Annotated['ConsolePortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + consoleserverports: Annotated['ConsoleServerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + poweroutlets: Annotated['PowerOutletFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + powerports: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + devicebays: Annotated['DeviceBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + frontports: Annotated['FrontPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rearports: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + modulebays: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + modules: Annotated['ModuleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + console_port_count: FilterLookup[int] | None = strawberry_django.filter_field() + console_server_port_count: FilterLookup[int] | None = strawberry_django.filter_field() + power_port_count: FilterLookup[int] | None = strawberry_django.filter_field() + power_outlet_count: FilterLookup[int] | None = strawberry_django.filter_field() + interface_count: FilterLookup[int] | None = strawberry_django.filter_field() + front_port_count: FilterLookup[int] | None = strawberry_django.filter_field() + rear_port_count: FilterLookup[int] | None = strawberry_django.filter_field() + device_bay_count: FilterLookup[int] | None = strawberry_django.filter_field() + module_bay_count: FilterLookup[int] | None = strawberry_django.filter_field() + inventory_item_count: FilterLookup[int] | None = strawberry_django.filter_field() @strawberry_django.filter(models.DeviceBay, lookups=True) -@autotype_decorator(filtersets.DeviceBayFilterSet) -class DeviceBayFilter(BaseFilterMixin): - pass +class DeviceBayFilter(ComponentModelFilterMixin): + installed_device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + installed_device_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.DeviceBayTemplate, lookups=True) -@autotype_decorator(filtersets.DeviceBayTemplateFilterSet) -class DeviceBayTemplateFilter(BaseFilterMixin): +class DeviceBayTemplateFilter(ComponentTemplateFilterMixin): pass @strawberry_django.filter(models.InventoryItemTemplate, lookups=True) -@autotype_decorator(filtersets.InventoryItemTemplateFilterSet) -class InventoryItemTemplateFilter(BaseFilterMixin): - pass +class InventoryItemTemplateFilter(ComponentTemplateFilterMixin): + parent: Annotated['InventoryItemTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + component_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + component_id: ID | None = strawberry_django.filter_field() + role: Annotated['InventoryItemRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + role_id: ID | None = strawberry_django.filter_field() + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + part_id: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.DeviceRole, lookups=True) -@autotype_decorator(filtersets.DeviceRoleFilterSet) -class DeviceRoleFilter(BaseFilterMixin): - pass +class DeviceRoleFilter(OrganizationalModelFilterMixin, RenderConfigFilterMixin): + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + vm_role: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.DeviceType, lookups=True) -@autotype_decorator(filtersets.DeviceTypeFilterSet) -class DeviceTypeFilter(BaseFilterMixin): - pass +class DeviceTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + model: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + default_platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + default_platform_id: ID | None = strawberry_django.filter_field() + part_number: FilterLookup[str] | None = strawberry_django.filter_field() + u_height: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + exclude_from_utilization: FilterLookup[bool] | None = strawberry_django.filter_field() + is_full_depth: FilterLookup[bool] | None = strawberry_django.filter_field() + subdevice_role: Annotated['SubdeviceRoleEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + airflow: Annotated['DeviceAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + front_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rear_image: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + console_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + console_server_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + power_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + power_outlet_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + interface_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + front_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + rear_port_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + device_bay_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + module_bay_template_count: FilterLookup[int] | None = strawberry_django.filter_field() + inventory_item_template_count: FilterLookup[int] | None = strawberry_django.filter_field() @strawberry_django.filter(models.FrontPort, lookups=True) -@autotype_decorator(filtersets.FrontPortFilterSet) -class FrontPortFilter(BaseFilterMixin): - pass +class FrontPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + rear_port: Annotated['RearPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rear_port_id: ID | None = strawberry_django.filter_field() + rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.FrontPortTemplate, lookups=True) -@autotype_decorator(filtersets.FrontPortTemplateFilterSet) -class FrontPortTemplateFilter(BaseFilterMixin): - pass +class FrontPortTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + rear_port: Annotated['RearPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rear_port_id: ID | None = strawberry_django.filter_field() + rear_port_position: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.MACAddress, lookups=True) -@autotype_decorator(filtersets.MACAddressFilterSet) -class MACAddressFilter(BaseFilterMixin): - pass +class MACAddressFilter(PrimaryModelFilterMixin): + mac_address: FilterLookup[str] | None = strawberry_django.filter_field() + assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + assigned_object_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.Interface, lookups=True) -@autotype_decorator(filtersets.InterfaceFilterSet) -class InterfaceFilter(BaseFilterMixin): - pass +class InterfaceFilter(ModularComponentModelFilterMixin, InterfaceBaseFilterMixin, CabledObjectModelFilterMixin): + vcdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + lag: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + lag_id: ID | None = strawberry_django.filter_field() + type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + mgmt_only: FilterLookup[bool] | None = strawberry_django.filter_field() + speed: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + duplex: Annotated['InterfaceDuplexEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + wwn: FilterLookup[str] | None = strawberry_django.filter_field() + rf_role: Annotated['WirelessRoleEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + rf_channel: Annotated['WirelessChannelEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + rf_channel_frequency: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + rf_channel_width: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + tx_power: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + poe_mode: Annotated['InterfacePoEModeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + poe_type: Annotated['InterfacePoETypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + wireless_link: Annotated['WirelessLinkFilter', strawberry.lazy('wireless.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + wireless_link_id: ID | None = strawberry_django.filter_field() + wireless_lans: Annotated['WirelessLANFilter', strawberry.lazy('wireless.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vrf_id: ID | None = strawberry_django.filter_field() + ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + mac_addresses: Annotated['MACAddressFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + fhrp_group_assignments: Annotated['FHRPGroupAssignmentFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tunnel_terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + l2vpn_terminations: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.InterfaceTemplate, lookups=True) -@autotype_decorator(filtersets.InterfaceTemplateFilterSet) -class InterfaceTemplateFilter(BaseFilterMixin): - pass +class InterfaceTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['InterfaceTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + mgmt_only: FilterLookup[bool] | None = strawberry_django.filter_field() + bridge: Annotated['InterfaceTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + bridge_id: ID | None = strawberry_django.filter_field() + poe_mode: Annotated['InterfacePoEModeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + poe_type: Annotated['InterfacePoETypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + rf_role: Annotated['WirelessRoleEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.InventoryItem, lookups=True) -@autotype_decorator(filtersets.InventoryItemFilterSet) -class InventoryItemFilter(BaseFilterMixin): - pass +class InventoryItemFilter(ComponentModelFilterMixin): + parent: Annotated['InventoryItemFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + parent_id: ID | None = strawberry_django.filter_field() + component_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + component_id: ID | None = strawberry_django.filter_field() + status: Annotated['InventoryItemStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + role: Annotated['InventoryItemRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + role_id: ID | None = strawberry_django.filter_field() + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + part_id: FilterLookup[str] | None = strawberry_django.filter_field() + serial: FilterLookup[str] | None = strawberry_django.filter_field() + asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() + discovered: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.InventoryItemRole, lookups=True) -@autotype_decorator(filtersets.InventoryItemRoleFilterSet) -class InventoryItemRoleFilter(BaseFilterMixin): - pass +class InventoryItemRoleFilter(OrganizationalModelFilterMixin): + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Location, lookups=True) -@autotype_decorator(filtersets.LocationFilterSet) -class LocationFilter(BaseFilterMixin): - pass +class LocationFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, NestedGroupModelFilterMixin): + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + status: Annotated['LocationStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + facility: FilterLookup[str] | None = strawberry_django.filter_field() + prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Manufacturer, lookups=True) -@autotype_decorator(filtersets.ManufacturerFilterSet) -class ManufacturerFilter(BaseFilterMixin): +class ManufacturerFilter(ContactFilterMixin, OrganizationalModelFilterMixin): pass @strawberry_django.filter(models.Module, lookups=True) -@autotype_decorator(filtersets.ModuleFilterSet) -class ModuleFilter(BaseFilterMixin): - pass +class ModuleFilter(PrimaryModelFilterMixin, ConfigContextFilterMixin): + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + module_bay: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + module_bay_id: ID | None = strawberry_django.filter_field() + module_type: Annotated['ModuleTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + module_type_id: ID | None = strawberry_django.filter_field() + status: Annotated['ModuleStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + serial: FilterLookup[str] | None = strawberry_django.filter_field() + asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ModuleBay, lookups=True) -@autotype_decorator(filtersets.ModuleBayFilterSet) -class ModuleBayFilter(BaseFilterMixin): - pass +class ModuleBayFilter(ModularComponentModelFilterMixin): + parent: Annotated['ModuleBayFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + parent_id: ID | None = strawberry_django.filter_field() + position: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ModuleBayTemplate, lookups=True) -@autotype_decorator(filtersets.ModuleBayTemplateFilterSet) -class ModuleBayTemplateFilter(BaseFilterMixin): - pass +class ModuleBayTemplateFilter(ModularComponentTemplateFilterMixin): + position: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ModuleType, lookups=True) -@autotype_decorator(filtersets.ModuleTypeFilterSet) -class ModuleTypeFilter(BaseFilterMixin): - pass +class ModuleTypeFilter(ImageAttachmentFilterMixin, PrimaryModelFilterMixin, WeightFilterMixin): + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + model: FilterLookup[str] | None = strawberry_django.filter_field() + part_number: FilterLookup[str] | None = strawberry_django.filter_field() + airflow: Annotated['ModuleAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Platform, lookups=True) -@autotype_decorator(filtersets.PlatformFilterSet) -class PlatformFilter(BaseFilterMixin): - pass +class PlatformFilter(OrganizationalModelFilterMixin): + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + config_template: Annotated['ConfigTemplateFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + config_template_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.PowerFeed, lookups=True) -@autotype_decorator(filtersets.PowerFeedFilterSet) -class PowerFeedFilter(BaseFilterMixin): - pass +class PowerFeedFilter(CabledObjectModelFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + power_panel: Annotated['PowerPanelFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + power_panel_id: ID | None = strawberry_django.filter_field() + rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + rack_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['PowerFeedStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + type: Annotated['PowerFeedTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + supply: Annotated['PowerFeedSupplyEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + phase: Annotated['PowerFeedPhaseEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + voltage: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + amperage: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + max_utilization: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + available_power: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.PowerOutlet, lookups=True) -@autotype_decorator(filtersets.PowerOutletFilterSet) -class PowerOutletFilter(BaseFilterMixin): - pass +class PowerOutletFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + power_port: Annotated['PowerPortFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + power_port_id: ID | None = strawberry_django.filter_field() + feed_leg: Annotated['PowerOutletFeedLegEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.PowerOutletTemplate, lookups=True) -@autotype_decorator(filtersets.PowerOutletTemplateFilterSet) -class PowerOutletTemplateFilter(BaseFilterMixin): - pass +class PowerOutletTemplateFilter(ModularComponentModelFilterMixin): + type: Annotated['PowerOutletTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + power_port: Annotated['PowerPortTemplateFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + power_port_id: ID | None = strawberry_django.filter_field() + feed_leg: Annotated['PowerOutletFeedLegEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.PowerPanel, lookups=True) -@autotype_decorator(filtersets.PowerPanelFilterSet) -class PowerPanelFilter(BaseFilterMixin): - pass +class PowerPanelFilter(ContactFilterMixin, ImageAttachmentFilterMixin, PrimaryModelFilterMixin): + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.PowerPort, lookups=True) -@autotype_decorator(filtersets.PowerPortFilterSet) -class PowerPortFilter(BaseFilterMixin): - pass +class PowerPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + maximum_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + allocated_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.PowerPortTemplate, lookups=True) -@autotype_decorator(filtersets.PowerPortTemplateFilterSet) -class PowerPortTemplateFilter(BaseFilterMixin): - pass +class PowerPortTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['PowerPortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + maximum_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + allocated_draw: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.RackType, lookups=True) -@autotype_decorator(filtersets.RackTypeFilterSet) -class RackTypeFilter(BaseFilterMixin): - pass +class RackTypeFilter(RackBaseFilterMixin): + form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + manufacturer: Annotated['ManufacturerFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + manufacturer_id: ID | None = strawberry_django.filter_field() + model: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Rack, lookups=True) -@autotype_decorator(filtersets.RackFilterSet) -class RackFilter(BaseFilterMixin): - pass +class RackFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, RackBaseFilterMixin): + form_factor: Annotated['RackFormFactorEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + rack_type: Annotated['RackTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rack_type_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + facility_id: FilterLookup[str] | None = strawberry_django.filter_field() + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + location: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + location_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + status: Annotated['RackStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + role: Annotated['RackRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + role_id: ID | None = strawberry_django.filter_field() + serial: FilterLookup[str] | None = strawberry_django.filter_field() + asset_tag: FilterLookup[str] | None = strawberry_django.filter_field() + airflow: Annotated['RackAirflowEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.RackReservation, lookups=True) -@autotype_decorator(filtersets.RackReservationFilterSet) -class RackReservationFilter(BaseFilterMixin): - pass +class RackReservationFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + rack: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + rack_id: ID | None = strawberry_django.filter_field() + units: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_id: ID | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.RackRole, lookups=True) -@autotype_decorator(filtersets.RackRoleFilterSet) -class RackRoleFilter(BaseFilterMixin): - pass +class RackRoleFilter(OrganizationalModelFilterMixin): + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.RearPort, lookups=True) -@autotype_decorator(filtersets.RearPortFilterSet) -class RearPortFilter(BaseFilterMixin): - pass +class RearPortFilter(ModularComponentModelFilterMixin, CabledObjectModelFilterMixin): + type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + positions: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.RearPortTemplate, lookups=True) -@autotype_decorator(filtersets.RearPortTemplateFilterSet) -class RearPortTemplateFilter(BaseFilterMixin): - pass +class RearPortTemplateFilter(ModularComponentTemplateFilterMixin): + type: Annotated['PortTypeEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + positions: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Region, lookups=True) -@autotype_decorator(filtersets.RegionFilterSet) -class RegionFilter(BaseFilterMixin): - pass +class RegionFilter(ContactFilterMixin, NestedGroupModelFilterMixin): + prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Site, lookups=True) -@autotype_decorator(filtersets.SiteFilterSet) -class SiteFilter(BaseFilterMixin): - pass +class SiteFilter(ContactFilterMixin, ImageAttachmentFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['SiteStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = strawberry_django.filter_field() + region: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + region_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + group: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + facility: FilterLookup[str] | None = strawberry_django.filter_field() + asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + time_zone: FilterLookup[str] | None = strawberry_django.filter_field() + physical_address: FilterLookup[str] | None = strawberry_django.filter_field() + shipping_address: FilterLookup[str] | None = strawberry_django.filter_field() + latitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + longitude: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.SiteGroup, lookups=True) -@autotype_decorator(filtersets.SiteGroupFilterSet) -class SiteGroupFilter(BaseFilterMixin): - pass +class SiteGroupFilter(ContactFilterMixin, NestedGroupModelFilterMixin): + prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VirtualChassis, lookups=True) -@autotype_decorator(filtersets.VirtualChassisFilterSet) -class VirtualChassisFilter(BaseFilterMixin): - pass +class VirtualChassisFilter(PrimaryModelFilterMixin): + master: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + master_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + domain: FilterLookup[str] | None = strawberry_django.filter_field() + member_count: FilterLookup[int] | None = strawberry_django.filter_field() @strawberry_django.filter(models.VirtualDeviceContext, lookups=True) -@autotype_decorator(filtersets.VirtualDeviceContextFilterSet) -class VirtualDeviceContextFilter(BaseFilterMixin): - pass +class VirtualDeviceContextFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['VirtualDeviceContextStatusEnum', strawberry.lazy('dcim.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip4_id: ID | None = strawberry_django.filter_field() + primary_ip6: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip6_id: ID | None = strawberry_django.filter_field() + comments: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/dcim/graphql/types.py b/netbox/dcim/graphql/types.py index 8d992176a..6cd072479 100644 --- a/netbox/dcim/graphql/types.py +++ b/netbox/dcim/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -6,7 +6,11 @@ import strawberry_django from core.graphql.mixins import ChangelogMixin from dcim import models from extras.graphql.mixins import ( - ConfigContextMixin, ContactsMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin, + ConfigContextMixin, + ContactsMixin, + CustomFieldsMixin, + ImageAttachmentsMixin, + TagsMixin, ) from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin from netbox.graphql.scalars import BigInt @@ -14,6 +18,23 @@ from netbox.graphql.types import BaseObjectType, NetBoxObjectType, Organizationa from .filters import * from .mixins import CabledObjectMixin, PathEndpointMixin +if TYPE_CHECKING: + from circuits.graphql.types import CircuitTerminationType + from extras.graphql.types import ConfigTemplateType + from ipam.graphql.types import ( + ASNType, + IPAddressType, + PrefixType, + ServiceType, + VLANTranslationPolicyType, + VLANType, + VRFType, + ) + from tenancy.graphql.types import TenantType + from users.graphql.types import UserType + from virtualization.graphql.types import ClusterType, VMInterfaceType, VirtualMachineType + from wireless.graphql.types import WirelessLANType, WirelessLinkType + __all__ = ( 'CableType', 'ComponentType', @@ -111,7 +132,7 @@ class ModularComponentTemplateType(ComponentTemplateType): @strawberry_django.type( models.CableTermination, - exclude=('termination_type', 'termination_id', '_device', '_rack', '_location', '_site'), + exclude=['termination_type', 'termination_id', '_device', '_rack', '_location', '_site'], filters=CableTerminationFilter ) class CableTerminationType(NetBoxObjectType): @@ -167,7 +188,7 @@ class CableType(NetBoxObjectType): @strawberry_django.type( models.ConsolePort, - exclude=('_path',), + exclude=['_path'], filters=ConsolePortFilter ) class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): @@ -185,7 +206,7 @@ class ConsolePortTemplateType(ModularComponentTemplateType): @strawberry_django.type( models.ConsoleServerPort, - exclude=('_path',), + exclude=['_path'], filters=ConsoleServerPortFilter ) class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): @@ -276,7 +297,7 @@ class DeviceBayTemplateType(ComponentTemplateType): @strawberry_django.type( models.InventoryItemTemplate, - exclude=('component_type', 'component_id', 'parent'), + exclude=['component_type', 'component_id', 'parent'], filters=InventoryItemTemplateFilter ) class InventoryItemTemplateType(ComponentTemplateType): @@ -369,7 +390,7 @@ class FrontPortTemplateType(ModularComponentTemplateType): @strawberry_django.type( models.MACAddress, - exclude=('assigned_object_type', 'assigned_object_id'), + exclude=['assigned_object_type', 'assigned_object_id'], filters=MACAddressFilter ) class MACAddressType(NetBoxObjectType): @@ -385,7 +406,7 @@ class MACAddressType(NetBoxObjectType): @strawberry_django.type( models.Interface, - exclude=('_path',), + exclude=['_path'], filters=InterfaceFilter ) class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin): @@ -424,7 +445,7 @@ class InterfaceTemplateType(ModularComponentTemplateType): @strawberry_django.type( models.InventoryItem, - exclude=('component_type', 'component_id', 'parent'), + exclude=['component_type', 'component_id', 'parent'], filters=InventoryItemFilter ) class InventoryItemType(ComponentType): @@ -463,7 +484,7 @@ class InventoryItemRoleType(OrganizationalObjectType): @strawberry_django.type( models.Location, # fields='__all__', - exclude=('parent',), # bug - temp + exclude=['parent'], # bug - temp filters=LocationFilter ) class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, OrganizationalObjectType): @@ -524,7 +545,7 @@ class ModuleType(NetBoxObjectType): @strawberry_django.type( models.ModuleBay, # fields='__all__', - exclude=('parent',), + exclude=['parent'], filters=ModuleBayFilter ) class ModuleBayType(ModularComponentType): @@ -579,7 +600,7 @@ class PlatformType(OrganizationalObjectType): @strawberry_django.type( models.PowerFeed, - exclude=('_path',), + exclude=['_path'], filters=PowerFeedFilter ) class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): @@ -590,7 +611,7 @@ class PowerFeedType(NetBoxObjectType, CabledObjectMixin, PathEndpointMixin): @strawberry_django.type( models.PowerOutlet, - exclude=('_path',), + exclude=['_path'], filters=PowerOutletFilter ) class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): @@ -621,7 +642,7 @@ class PowerPanelType(NetBoxObjectType, ContactsMixin): @strawberry_django.type( models.PowerPort, - exclude=('_path',), + exclude=['_path'], filters=PowerPortFilter ) class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin): @@ -712,8 +733,7 @@ class RearPortTemplateType(ModularComponentTemplateType): @strawberry_django.type( models.Region, - exclude=('parent',), - # fields='__all__', + exclude=['parent'], filters=RegionFilter ) class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): @@ -772,8 +792,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje @strawberry_django.type( models.SiteGroup, - # fields='__all__', - exclude=('parent',), # bug - temp + exclude=['parent'], # bug - temp filters=SiteGroupFilter ) class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType): diff --git a/netbox/extras/graphql/enums.py b/netbox/extras/graphql/enums.py new file mode 100644 index 000000000..0d352b835 --- /dev/null +++ b/netbox/extras/graphql/enums.py @@ -0,0 +1,26 @@ +import strawberry + +from extras.choices import * + +__all__ = ( + 'CustomFieldChoiceSetBaseEnum', + 'CustomFieldFilterLogicEnum', + 'CustomFieldTypeEnum', + 'CustomFieldUIEditableEnum', + 'CustomFieldUIVisibleEnum', + 'CustomLinkButtonClassEnum', + 'EventRuleActionEnum', + 'JournalEntryKindEnum', + 'WebhookHttpMethodEnum', +) + + +CustomFieldChoiceSetBaseEnum = strawberry.enum(CustomFieldChoiceSetBaseChoices.as_enum()) +CustomFieldFilterLogicEnum = strawberry.enum(CustomFieldFilterLogicChoices.as_enum()) +CustomFieldTypeEnum = strawberry.enum(CustomFieldTypeChoices.as_enum()) +CustomFieldUIEditableEnum = strawberry.enum(CustomFieldUIEditableChoices.as_enum()) +CustomFieldUIVisibleEnum = strawberry.enum(CustomFieldUIVisibleChoices.as_enum()) +CustomLinkButtonClassEnum = strawberry.enum(CustomLinkButtonClassChoices.as_enum()) +EventRuleActionEnum = strawberry.enum(EventRuleActionChoices.as_enum()) +JournalEntryKindEnum = strawberry.enum(JournalEntryKindChoices.as_enum()) +WebhookHttpMethodEnum = strawberry.enum(WebhookHttpMethodChoices.as_enum()) diff --git a/netbox/extras/graphql/filter_mixins.py b/netbox/extras/graphql/filter_mixins.py new file mode 100644 index 000000000..7e9a970f2 --- /dev/null +++ b/netbox/extras/graphql/filter_mixins.py @@ -0,0 +1,52 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import BaseFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import JSONFilter + from .filters import * + +__all__ = ( + 'CustomFieldsFilterMixin', + 'JournalEntriesFilterMixin', + 'TagsFilterMixin', + 'ConfigContextFilterMixin', + 'TagBaseFilterMixin', +) + + +@dataclass +class CustomFieldsFilterMixin(BaseFilterMixin): + custom_field_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class JournalEntriesFilterMixin(BaseFilterMixin): + journal_entries: Annotated['JournalEntryFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class TagsFilterMixin(BaseFilterMixin): + tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field() + + +@dataclass +class ConfigContextFilterMixin(BaseFilterMixin): + local_context_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class TagBaseFilterMixin(BaseFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/extras/graphql/filters.py b/netbox/extras/graphql/filters.py index ff2e6a0f1..e22bda0ac 100644 --- a/netbox/extras/graphql/filters.py +++ b/netbox/extras/graphql/filters.py @@ -1,7 +1,26 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from extras import filtersets, models -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin +from extras import models +from extras.graphql.filter_mixins import TagBaseFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin +from netbox.graphql.filter_mixins import SyncedDataFilterMixin + +if TYPE_CHECKING: + from core.graphql.filters import ContentTypeFilter + from dcim.graphql.filters import ( + DeviceRoleFilter, DeviceTypeFilter, LocationFilter, PlatformFilter, RegionFilter, SiteFilter, SiteGroupFilter, + ) + from tenancy.graphql.filters import TenantFilter, TenantGroupFilter + from netbox.graphql.enums import ColorEnum + from netbox.graphql.filter_lookups import IntegerLookup, JSONFilter, StringArrayLookup, TreeNodeFilter + from users.graphql.filters import GroupFilter, UserFilter + from virtualization.graphql.filters import ClusterFilter, ClusterGroupFilter, ClusterTypeFilter + from .enums import * __all__ = ( 'ConfigContextFilter', @@ -21,78 +40,263 @@ __all__ = ( @strawberry_django.filter(models.ConfigContext, lookups=True) -@autotype_decorator(filtersets.ConfigContextFilterSet) -class ConfigContextFilter(BaseFilterMixin): - pass +class ConfigContextFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] = strawberry_django.filter_field() + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + description: FilterLookup[str] = strawberry_django.filter_field() + is_active: FilterLookup[bool] = strawberry_django.filter_field() + regions: Annotated['RegionFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + region_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + site_groups: Annotated['SiteGroupFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + site_group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + sites: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + locations: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + device_types: Annotated['DeviceTypeFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + roles: Annotated['DeviceRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + platforms: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cluster_types: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cluster_groups: Annotated['ClusterGroupFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + clusters: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tenant_groups: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tenant_group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + tenants: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tags: Annotated['TagFilter', strawberry.lazy('extras.graphql.filters')] | None = strawberry_django.filter_field() + data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ConfigTemplate, lookups=True) -@autotype_decorator(filtersets.ConfigTemplateFilterSet) -class ConfigTemplateFilter(BaseFilterMixin): - pass +class ConfigTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + template_code: FilterLookup[str] | None = strawberry_django.filter_field() + environment_params: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.CustomField, lookups=True) -@autotype_decorator(filtersets.CustomFieldFilterSet) -class CustomFieldFilter(BaseFilterMixin): - pass +class CustomFieldFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + type: Annotated['CustomFieldTypeEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + object_types: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + related_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() + label: FilterLookup[str] | None = strawberry_django.filter_field() + group_name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + required: FilterLookup[bool] | None = strawberry_django.filter_field() + unique: FilterLookup[bool] | None = strawberry_django.filter_field() + search_weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + filter_logic: Annotated['CustomFieldFilterLogicEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + default: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + related_object_filter: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + validation_minimum: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + validation_maximum: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + validation_regex: FilterLookup[str] | None = strawberry_django.filter_field() + choice_set: Annotated['CustomFieldChoiceSetFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + choice_set_id: ID | None = strawberry_django.filter_field() + ui_visible: Annotated['CustomFieldUIVisibleEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + ui_editable: Annotated['CustomFieldUIEditableEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + is_cloneable: FilterLookup[bool] | None = strawberry_django.filter_field() + comments: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.CustomFieldChoiceSet, lookups=True) -@autotype_decorator(filtersets.CustomFieldChoiceSetFilterSet) -class CustomFieldChoiceSetFilter(BaseFilterMixin): - pass +class CustomFieldChoiceSetFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + base_choices: Annotated['CustomFieldChoiceSetBaseEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + extra_choices: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + order_alphabetically: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.CustomLink, lookups=True) -@autotype_decorator(filtersets.CustomLinkFilterSet) -class CustomLinkFilter(BaseFilterMixin): - pass +class CustomLinkFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + link_text: FilterLookup[str] | None = strawberry_django.filter_field() + link_url: FilterLookup[str] | None = strawberry_django.filter_field() + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + group_name: FilterLookup[str] | None = strawberry_django.filter_field() + button_class: Annotated['CustomLinkButtonClassEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + new_window: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ExportTemplate, lookups=True) -@autotype_decorator(filtersets.ExportTemplateFilterSet) -class ExportTemplateFilter(BaseFilterMixin): - pass +class ExportTemplateFilter(BaseObjectTypeFilterMixin, SyncedDataFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + template_code: FilterLookup[str] | None = strawberry_django.filter_field() + mime_type: FilterLookup[str] | None = strawberry_django.filter_field() + file_extension: FilterLookup[str] | None = strawberry_django.filter_field() + as_attachment: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.ImageAttachment, lookups=True) -@autotype_decorator(filtersets.ImageAttachmentFilterSet) -class ImageAttachmentFilter(BaseFilterMixin): - pass +class ImageAttachmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + object_id: ID | None = strawberry_django.filter_field() + image_height: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + image_width: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.JournalEntry, lookups=True) -@autotype_decorator(filtersets.JournalEntryFilterSet) -class JournalEntryFilter(BaseFilterMixin): - pass +class JournalEntryFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): + assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + assigned_object_type_id: ID | None = strawberry_django.filter_field() + assigned_object_id: ID | None = strawberry_django.filter_field() + created_by: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + kind: Annotated['JournalEntryKindEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + comments: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.NotificationGroup, lookups=True) -@autotype_decorator(filtersets.NotificationGroupFilterSet) -class NotificationGroupFilter(BaseFilterMixin): - pass +class NotificationGroupFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + users: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.SavedFilter, lookups=True) -@autotype_decorator(filtersets.SavedFilterFilterSet) -class SavedFilterFilter(BaseFilterMixin): - pass +class SavedFilterFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + user: Annotated['UserFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() + user_id: ID | None = strawberry_django.filter_field() + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + shared: FilterLookup[bool] | None = strawberry_django.filter_field() + parameters: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Tag, lookups=True) -@autotype_decorator(filtersets.TagFilterSet) -class TagFilter(BaseFilterMixin): - pass +class TagFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin, TagBaseFilterMixin): + color: Annotated['ColorEnum', strawberry.lazy('netbox.graphql.enums')] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Webhook, lookups=True) -@autotype_decorator(filtersets.WebhookFilterSet) -class WebhookFilter(BaseFilterMixin): - pass +class WebhookFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + payload_url: FilterLookup[str] | None = strawberry_django.filter_field() + http_method: Annotated['WebhookHttpMethodEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + http_content_type: FilterLookup[str] | None = strawberry_django.filter_field() + additional_headers: FilterLookup[str] | None = strawberry_django.filter_field() + body_template: FilterLookup[str] | None = strawberry_django.filter_field() + secret: FilterLookup[str] | None = strawberry_django.filter_field() + ssl_verification: FilterLookup[bool] | None = strawberry_django.filter_field() + ca_file_path: FilterLookup[str] | None = strawberry_django.filter_field() + events: Annotated['EventRuleFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.EventRule, lookups=True) -@autotype_decorator(filtersets.EventRuleFilterSet) -class EventRuleFilter(BaseFilterMixin): - pass +class EventRuleFilter(BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + event_types: Annotated['StringArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + conditions: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + action_type: Annotated['EventRuleActionEnum', strawberry.lazy('extras.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + action_object_type: FilterLookup[str] | None = strawberry_django.filter_field() + action_object_type_id: ID | None = strawberry_django.filter_field() + action_object_id: ID | None = strawberry_django.filter_field() + action_data: Annotated['JSONFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + comments: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/extras/graphql/types.py b/netbox/extras/graphql/types.py index a53c7bed3..1ceb2682c 100644 --- a/netbox/extras/graphql/types.py +++ b/netbox/extras/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated, List, TYPE_CHECKING import strawberry import strawberry_django @@ -8,6 +8,22 @@ from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, ContentTypeType, ObjectType, OrganizationalObjectType from .filters import * +if TYPE_CHECKING: + from core.graphql.types import DataFileType, DataSourceType + from dcim.graphql.types import ( + DeviceRoleType, + DeviceType, + DeviceTypeType, + LocationType, + PlatformType, + RegionType, + SiteGroupType, + SiteType, + ) + from tenancy.graphql.types import TenantGroupType, TenantType + from users.graphql.types import GroupType, UserType + from virtualization.graphql.types import ClusterGroupType, ClusterType, ClusterTypeType, VirtualMachineType + __all__ = ( 'ConfigContextType', 'ConfigTemplateType', @@ -35,7 +51,6 @@ __all__ = ( class ConfigContextType(ObjectType): data_source: Annotated["DataSourceType", strawberry.lazy('core.graphql.types')] | None data_file: Annotated["DataFileType", strawberry.lazy('core.graphql.types')] | None - roles: List[Annotated["DeviceRoleType", strawberry.lazy('dcim.graphql.types')]] device_types: List[Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]] tags: List[Annotated["TagType", strawberry.lazy('extras.graphql.types')]] @@ -78,7 +93,7 @@ class CustomFieldType(ObjectType): @strawberry_django.type( models.CustomFieldChoiceSet, - exclude=('extra_choices', ), + exclude=['extra_choices'], filters=CustomFieldChoiceSetFilter ) class CustomFieldChoiceSetType(ObjectType): diff --git a/netbox/ipam/graphql/enums.py b/netbox/ipam/graphql/enums.py new file mode 100644 index 000000000..34fb1a6fd --- /dev/null +++ b/netbox/ipam/graphql/enums.py @@ -0,0 +1,27 @@ +import strawberry + +from ipam.choices import * + +__all__ = ( + 'FHRPGroupAuthTypeEnum', + 'FHRPGroupProtocolEnum', + 'IPAddressFamilyEnum', + 'IPAddressRoleEnum', + 'IPAddressStatusEnum', + 'IPRangeStatusEnum', + 'PrefixStatusEnum', + 'ServiceProtocolEnum', + 'VLANStatusEnum', + 'VLANQinQRoleEnum', +) + +FHRPGroupAuthTypeEnum = strawberry.enum(FHRPGroupAuthTypeChoices.as_enum()) +FHRPGroupProtocolEnum = strawberry.enum(FHRPGroupProtocolChoices.as_enum()) +IPAddressFamilyEnum = strawberry.enum(IPAddressFamilyChoices.as_enum()) +IPAddressRoleEnum = strawberry.enum(IPAddressRoleChoices.as_enum()) +IPAddressStatusEnum = strawberry.enum(IPAddressStatusChoices.as_enum()) +IPRangeStatusEnum = strawberry.enum(IPRangeStatusChoices.as_enum()) +PrefixStatusEnum = strawberry.enum(PrefixStatusChoices.as_enum()) +ServiceProtocolEnum = strawberry.enum(ServiceProtocolChoices.as_enum()) +VLANStatusEnum = strawberry.enum(VLANStatusChoices.as_enum()) +VLANQinQRoleEnum = strawberry.enum(VLANQinQRoleChoices.as_enum()) diff --git a/netbox/ipam/graphql/filter_mixins.py b/netbox/ipam/graphql/filter_mixins.py new file mode 100644 index 000000000..511850285 --- /dev/null +++ b/netbox/ipam/graphql/filter_mixins.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django + +from core.graphql.filter_mixins import BaseFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import IntegerLookup + from .enums import * + +__all__ = ( + 'ServiceBaseFilterMixin', +) + + +@dataclass +class ServiceBaseFilterMixin(BaseFilterMixin): + protocol: Annotated['ServiceProtocolEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + ports: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/ipam/graphql/filters.py b/netbox/ipam/graphql/filters.py index 1b0e0133b..2f4e185f1 100644 --- a/netbox/ipam/graphql/filters.py +++ b/netbox/ipam/graphql/filters.py @@ -1,7 +1,28 @@ -import strawberry_django +from datetime import date +from typing import Annotated, TYPE_CHECKING -from ipam import filtersets, models -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin +import netaddr +import strawberry +import strawberry_django +from django.db.models import Q +from netaddr.core import AddrFormatError +from strawberry.scalars import ID +from strawberry_django import FilterLookup, DateFilterLookup + +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin +from dcim.graphql.filter_mixins import ScopedFilterMixin +from ipam import models +from ipam.graphql.filter_mixins import ServiceBaseFilterMixin +from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin +from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import IntegerArrayLookup, IntegerLookup + from core.graphql.filters import ContentTypeFilter + from dcim.graphql.filters import DeviceFilter, SiteFilter + from virtualization.graphql.filters import VirtualMachineFilter + from vpn.graphql.filters import L2VPNFilter + from .enums import * __all__ = ( 'ASNFilter', @@ -26,108 +47,258 @@ __all__ = ( @strawberry_django.filter(models.ASN, lookups=True) -@autotype_decorator(filtersets.ASNFilterSet) -class ASNFilter(BaseFilterMixin): - pass +class ASNFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + rir_id: ID | None = strawberry_django.filter_field() + asn: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ASNRange, lookups=True) -@autotype_decorator(filtersets.ASNRangeFilterSet) -class ASNRangeFilter(BaseFilterMixin): - pass +class ASNRangeFilter(TenancyFilterMixin, OrganizationalModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + rir_id: ID | None = strawberry_django.filter_field() + start: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + end: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Aggregate, lookups=True) -@autotype_decorator(filtersets.AggregateFilterSet) -class AggregateFilter(BaseFilterMixin): - pass +class AggregateFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + prefix: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + prefix_id: ID | None = strawberry_django.filter_field() + rir: Annotated['RIRFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + rir_id: ID | None = strawberry_django.filter_field() + date_added: DateFilterLookup[date] | None = strawberry_django.filter_field() @strawberry_django.filter(models.FHRPGroup, lookups=True) -@autotype_decorator(filtersets.FHRPGroupFilterSet) -class FHRPGroupFilter(BaseFilterMixin): - pass +class FHRPGroupFilter(PrimaryModelFilterMixin): + group_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() + protocol: Annotated['FHRPGroupProtocolEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + auth_type: Annotated['FHRPGroupAuthTypeEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + auth_key: FilterLookup[str] | None = strawberry_django.filter_field() + ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.FHRPGroupAssignment, lookups=True) -@autotype_decorator(filtersets.FHRPGroupAssignmentFilterSet) -class FHRPGroupAssignmentFilter(BaseFilterMixin): - pass +class FHRPGroupAssignmentFilter(BaseObjectTypeFilterMixin, ChangeLogFilterMixin): + interface_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + interface_id: FilterLookup[str] | None = strawberry_django.filter_field() + group: Annotated['FHRPGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + priority: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.IPAddress, lookups=True) -@autotype_decorator(filtersets.IPAddressFilterSet) -class IPAddressFilter(BaseFilterMixin): - pass +class IPAddressFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + address: FilterLookup[str] | None = strawberry_django.filter_field() + vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vrf_id: ID | None = strawberry_django.filter_field() + status: Annotated['IPAddressStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + role: Annotated['IPAddressRoleEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + assigned_object_id: ID | None = strawberry_django.filter_field() + nat_inside: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + nat_inside_id: ID | None = strawberry_django.filter_field() + nat_outside: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + nat_outside_id: ID | None = strawberry_django.filter_field() + dns_name: FilterLookup[str] | None = strawberry_django.filter_field() + + @strawberry_django.filter_field() + def parent(self, value: list[str], prefix) -> Q: + if not value: + return Q() + q = Q() + for subnet in value: + try: + query = str(netaddr.IPNetwork(subnet.strip()).cidr) + q |= Q(address__net_host_contained=query) + except (AddrFormatError, ValueError): + return Q() + return q @strawberry_django.filter(models.IPRange, lookups=True) -@autotype_decorator(filtersets.IPRangeFilterSet) -class IPRangeFilter(BaseFilterMixin): - pass +class IPRangeFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + start_address: FilterLookup[str] | None = strawberry_django.filter_field() + end_address: FilterLookup[str] | None = strawberry_django.filter_field() + size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vrf_id: ID | None = strawberry_django.filter_field() + status: Annotated['IPRangeStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + role: Annotated['IPAddressRoleEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field() + + @strawberry_django.filter_field() + def parent(self, value: list[str], prefix) -> Q: + if not value: + return Q() + q = Q() + for subnet in value: + try: + query = str(netaddr.IPNetwork(subnet.strip()).cidr) + q |= Q(start_address__net_host_contained=query, end_address__net_host_contained=query) + except (AddrFormatError, ValueError): + return Q() + return q @strawberry_django.filter(models.Prefix, lookups=True) -@autotype_decorator(filtersets.PrefixFilterSet) -class PrefixFilter(BaseFilterMixin): - pass +class PrefixFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + prefix: FilterLookup[str] | None = strawberry_django.filter_field() + vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vrf_id: ID | None = strawberry_django.filter_field() + vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vlan_id: ID | None = strawberry_django.filter_field() + status: Annotated['PrefixStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + role: Annotated['RoleFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + role_id: ID | None = strawberry_django.filter_field() + is_pool: FilterLookup[bool] | None = strawberry_django.filter_field() + mark_utilized: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.RIR, lookups=True) -@autotype_decorator(filtersets.RIRFilterSet) -class RIRFilter(BaseFilterMixin): - pass +class RIRFilter(OrganizationalModelFilterMixin): + is_private: FilterLookup[bool] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Role, lookups=True) -@autotype_decorator(filtersets.RoleFilterSet) -class RoleFilter(BaseFilterMixin): - pass +class RoleFilter(OrganizationalModelFilterMixin): + weight: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.RouteTarget, lookups=True) -@autotype_decorator(filtersets.RouteTargetFilterSet) -class RouteTargetFilter(BaseFilterMixin): - pass +class RouteTargetFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.Service, lookups=True) -@autotype_decorator(filtersets.ServiceFilterSet) -class ServiceFilter(BaseFilterMixin): - pass +class ServiceFilter(ContactFilterMixin, ServiceBaseFilterMixin, PrimaryModelFilterMixin): + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + virtual_machine: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + virtual_machine_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + ipaddresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ServiceTemplate, lookups=True) -@autotype_decorator(filtersets.ServiceTemplateFilterSet) -class ServiceTemplateFilter(BaseFilterMixin): - pass +class ServiceTemplateFilter(ServiceBaseFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.VLAN, lookups=True) -@autotype_decorator(filtersets.VLANFilterSet) -class VLANFilter(BaseFilterMixin): - pass +class VLANFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + group: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + name: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['VLANStatusEnum', strawberry.lazy('ipam.graphql.enums')] | None = strawberry_django.filter_field() + role: Annotated['RoleFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + role_id: ID | None = strawberry_django.filter_field() + qinq_svlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + qinq_svlan_id: ID | None = strawberry_django.filter_field() + qinq_cvlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + qinq_cvlan_id: ID | None = strawberry_django.filter_field() + qinq_role: Annotated['VLANQinQRoleEnum', strawberry.lazy('ipam.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + l2vpn_terminations: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VLANGroup, lookups=True) -@autotype_decorator(filtersets.VLANGroupFilterSet) -class VLANGroupFilter(BaseFilterMixin): - pass +class VLANGroupFilter(ScopedFilterMixin, OrganizationalModelFilterMixin): + vid_ranges: Annotated['IntegerArrayLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VLANTranslationPolicy, lookups=True) -@autotype_decorator(filtersets.VLANTranslationPolicyFilterSet) -class VLANTranslationPolicyFilter(BaseFilterMixin): - pass +class VLANTranslationPolicyFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.VLANTranslationRule, lookups=True) -@autotype_decorator(filtersets.VLANTranslationRuleFilterSet) -class VLANTranslationRuleFilter(BaseFilterMixin): - pass +class VLANTranslationRuleFilter(NetBoxModelFilterMixin): + policy: Annotated['VLANTranslationPolicyFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + policy_id: ID | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + local_vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + remote_vid: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VRF, lookups=True) -@autotype_decorator(filtersets.VRFFilterSet) -class VRFFilter(BaseFilterMixin): - pass +class VRFFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + rd: FilterLookup[str] | None = strawberry_django.filter_field() + enforce_unique: FilterLookup[bool] | None = strawberry_django.filter_field() + import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + export_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index b16cf29fe..e63bebcb1 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -11,6 +11,21 @@ from netbox.graphql.types import BaseObjectType, NetBoxObjectType, Organizationa from .filters import * from .mixins import IPAddressesMixin +if TYPE_CHECKING: + from dcim.graphql.types import ( + DeviceType, + InterfaceType, + LocationType, + RackType, + RegionType, + SiteGroupType, + SiteType, + ) + from tenancy.graphql.types import TenantType + from virtualization.graphql.types import ClusterGroupType, ClusterType, VMInterfaceType, VirtualMachineType + from vpn.graphql.types import L2VPNType, TunnelTerminationType + from wireless.graphql.types import WirelessLANType + __all__ = ( 'ASNType', 'ASNRangeType', @@ -101,7 +116,7 @@ class FHRPGroupType(NetBoxObjectType, IPAddressesMixin): @strawberry_django.type( models.FHRPGroupAssignment, - exclude=('interface_type', 'interface_id'), + exclude=['interface_type', 'interface_id'], filters=FHRPGroupAssignmentFilter ) class FHRPGroupAssignmentType(BaseObjectType): @@ -117,7 +132,7 @@ class FHRPGroupAssignmentType(BaseObjectType): @strawberry_django.type( models.IPAddress, - exclude=('assigned_object_type', 'assigned_object_id', 'address'), + exclude=['assigned_object_type', 'assigned_object_id', 'address'], filters=IPAddressFilter ) class IPAddressType(NetBoxObjectType, BaseIPAddressFamilyType): @@ -154,7 +169,7 @@ class IPRangeType(NetBoxObjectType): @strawberry_django.type( models.Prefix, - exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), + exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'], filters=PrefixFilter ) class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType): @@ -236,7 +251,7 @@ class ServiceTemplateType(NetBoxObjectType): @strawberry_django.type( models.VLAN, - exclude=('qinq_svlan',), + exclude=['qinq_svlan'], filters=VLANFilter ) class VLANType(NetBoxObjectType): @@ -259,7 +274,7 @@ class VLANType(NetBoxObjectType): @strawberry_django.type( models.VLANGroup, - exclude=('scope_type', 'scope_id'), + exclude=['scope_type', 'scope_id'], filters=VLANGroupFilter ) class VLANGroupType(OrganizationalObjectType): diff --git a/netbox/netbox/graphql/enums.py b/netbox/netbox/graphql/enums.py new file mode 100644 index 000000000..df62f8b3d --- /dev/null +++ b/netbox/netbox/graphql/enums.py @@ -0,0 +1,13 @@ +import strawberry + +from netbox.choices import * + +__all__ = ( + 'ColorEnum', + 'DistanceUnitEnum', + 'WeightUnitEnum', +) + +ColorEnum = strawberry.enum(ColorChoices.as_enum()) +DistanceUnitEnum = strawberry.enum(DistanceUnitChoices.as_enum()) +WeightUnitEnum = strawberry.enum(WeightUnitChoices.as_enum()) diff --git a/netbox/netbox/graphql/filter_lookups.py b/netbox/netbox/graphql/filter_lookups.py new file mode 100644 index 000000000..859236e4d --- /dev/null +++ b/netbox/netbox/graphql/filter_lookups.py @@ -0,0 +1,219 @@ +from enum import Enum +from typing import TypeVar, Tuple, Generic + +import strawberry +import strawberry_django +from django.core.exceptions import FieldDoesNotExist +from django.db.models import Q, QuerySet +from django.db.models.fields.related import ForeignKey, ManyToManyField, ManyToManyRel, ManyToOneRel +from strawberry import ID +from strawberry.types import Info +from strawberry_django import ( + ComparisonFilterLookup, + DateFilterLookup, + DatetimeFilterLookup, + FilterLookup, + RangeLookup, + TimeFilterLookup, + process_filters, +) + +__all__ = ( + 'ArrayLookup', + 'FloatArrayLookup', + 'FloatLookup', + 'IntegerArrayLookup', + 'IntegerLookup', + 'JSONFilter', + 'StringArrayLookup', + 'TreeNodeFilter', +) + +T = TypeVar('T') +SKIP_MSG = 'Filter will be skipped on `null` value' + + +@strawberry.input(one_of=True, description='Lookup for JSON field. Only one of the lookup fields can be set.') +class JSONLookup: + string_lookup: FilterLookup[str] | None = strawberry_django.filter_field() + int_range_lookup: RangeLookup[int] | None = strawberry_django.filter_field() + int_comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field() + float_range_lookup: RangeLookup[float] | None = strawberry_django.filter_field() + float_comparison_lookup: ComparisonFilterLookup[float] | None = strawberry_django.filter_field() + date_lookup: DateFilterLookup[str] | None = strawberry_django.filter_field() + datetime_lookup: DatetimeFilterLookup[str] | None = strawberry_django.filter_field() + time_lookup: TimeFilterLookup[str] | None = strawberry_django.filter_field() + boolean_lookup: FilterLookup[bool] | None = strawberry_django.filter_field() + + def get_filter(self): + for field in self.__strawberry_definition__.fields: + value = getattr(self, field.name, None) + if value is not strawberry.UNSET: + return value + return None + + +@strawberry.input(one_of=True, description='Lookup for Integer fields. Only one of the lookup fields can be set.') +class IntegerLookup: + filter_lookup: FilterLookup[int] | None = strawberry_django.filter_field() + range_lookup: RangeLookup[int] | None = strawberry_django.filter_field() + comparison_lookup: ComparisonFilterLookup[int] | None = strawberry_django.filter_field() + + def get_filter(self): + for field in self.__strawberry_definition__.fields: + value = getattr(self, field.name, None) + if value is not strawberry.UNSET: + return value + return None + + @strawberry_django.filter_field + def filter(self, info: Info, queryset: QuerySet, prefix: str = '') -> Tuple[QuerySet, Q]: + filters = self.get_filter() + + if not filters: + return queryset, Q() + + return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix) + + +@strawberry.input(one_of=True, description='Lookup for Float fields. Only one of the lookup fields can be set.') +class FloatLookup: + filter_lookup: FilterLookup[float] | None = strawberry_django.filter_field() + range_lookup: RangeLookup[float] | None = strawberry_django.filter_field() + comparison_lookup: ComparisonFilterLookup[float] | None = strawberry_django.filter_field() + + def get_filter(self): + for field in self.__strawberry_definition__.fields: + value = getattr(self, field.name, None) + if value is not strawberry.UNSET: + return value + return None + + @strawberry_django.filter_field + def filter(self, info: Info, queryset: QuerySet, prefix: str = '') -> Tuple[QuerySet, Q]: + filters = self.get_filter() + + if not filters: + return queryset, Q() + + return process_filters(filters=filters, queryset=queryset, info=info, prefix=prefix) + + +@strawberry.input +class JSONFilter: + """ + Class for JSON field lookups with paths + """ + + path: str + lookup: JSONLookup + + @strawberry_django.filter_field + def filter(self, info: Info, queryset: QuerySet, prefix: str = '') -> Tuple[QuerySet, Q]: + filters = self.lookup.get_filter() + + if not filters: + return queryset, Q() + + json_path = f'{prefix}{self.path}__' + return process_filters(filters=filters, queryset=queryset, info=info, prefix=json_path) + + +@strawberry.enum +class TreeNodeMatch(Enum): + EXACT = 'exact' # Just the node itself + DESCENDANTS = 'descendants' # Node and all descendants + SELF_AND_DESCENDANTS = 'self_and_descendants' # Node and all descendants + CHILDREN = 'children' # Just immediate children + SIBLINGS = 'siblings' # Nodes with same parent + ANCESTORS = 'ancestors' # All parent nodes + PARENT = 'parent' # Just immediate parent + + +@strawberry.input +class TreeNodeFilter: + id: ID + match_type: TreeNodeMatch + + @strawberry_django.filter_field + def filter(self, info: Info, queryset: QuerySet, prefix: str = '') -> Tuple[QuerySet, Q]: + model_field_name = prefix.removesuffix('__').removesuffix('_id') + model_field = None + try: + model_field = queryset.model._meta.get_field(model_field_name) + except FieldDoesNotExist: + try: + model_field = queryset.model._meta.get_field(f'{model_field_name}s') + except FieldDoesNotExist: + return queryset, Q(pk__in=[]) + + if hasattr(model_field, 'related_model'): + related_model = model_field.related_model + else: + return queryset, Q(pk__in=[]) + + # Generate base Q filter for the related model without prefix + q_filter = generate_tree_node_q_filter(related_model, self) + + # Handle different relationship types + if isinstance(model_field, (ManyToManyField, ManyToManyRel)): + return queryset, Q(**{f'{model_field_name}__in': related_model.objects.filter(q_filter)}) + elif isinstance(model_field, ForeignKey): + return queryset, Q(**{f'{model_field_name}__{k}': v for k, v in q_filter.children}) + elif isinstance(model_field, ManyToOneRel): + return queryset, Q(**{f'{model_field_name}__in': related_model.objects.filter(q_filter)}) + else: + return queryset, Q(**{f'{model_field_name}__{k}': v for k, v in q_filter.children}) + + +def generate_tree_node_q_filter(model_class, filter_value: TreeNodeFilter) -> Q: + """ + Generate appropriate Q filter for MPTT tree filtering based on match type + """ + try: + node = model_class.objects.get(id=filter_value.id) + except model_class.DoesNotExist: + return Q(pk__in=[]) + + if filter_value.match_type == TreeNodeMatch.EXACT: + return Q(id=filter_value.id) + elif filter_value.match_type == TreeNodeMatch.DESCENDANTS: + return Q(tree_id=node.tree_id, lft__gt=node.lft, rght__lt=node.rght) + elif filter_value.match_type == TreeNodeMatch.SELF_AND_DESCENDANTS: + return Q(tree_id=node.tree_id, lft__gte=node.lft, rght__lte=node.rght) + elif filter_value.match_type == TreeNodeMatch.CHILDREN: + return Q(tree_id=node.tree_id, level=node.level + 1, lft__gt=node.lft, rght__lt=node.rght) + elif filter_value.match_type == TreeNodeMatch.SIBLINGS: + return Q(tree_id=node.tree_id, level=node.level, parent=node.parent) & ~Q(id=node.id) + elif filter_value.match_type == TreeNodeMatch.ANCESTORS: + return Q(tree_id=node.tree_id, lft__lt=node.lft, rght__gt=node.rght) + elif filter_value.match_type == TreeNodeMatch.PARENT: + return Q(id=node.parent_id) if node.parent_id else Q(pk__in=[]) + return Q() + + +@strawberry.input(one_of=True, description='Lookup for Array fields. Only one of the lookup fields can be set.') +class ArrayLookup(Generic[T]): + """ + Class for Array field lookups + """ + + contains: list[T] | None = strawberry_django.filter_field(description='Contains the value') + contained_by: list[T] | None = strawberry_django.filter_field(description='Contained by the value') + overlap: list[T] | None = strawberry_django.filter_field(description='Overlaps with the value') + length: int | None = strawberry_django.filter_field(description='Length of the array') + + +@strawberry.input(one_of=True, description='Lookup for Array fields. Only one of the lookup fields can be set.') +class IntegerArrayLookup(ArrayLookup[int]): + pass + + +@strawberry.input(one_of=True, description='Lookup for Array fields. Only one of the lookup fields can be set.') +class FloatArrayLookup(ArrayLookup[float]): + pass + + +@strawberry.input(one_of=True, description='Lookup for Array fields. Only one of the lookup fields can be set.') +class StringArrayLookup(ArrayLookup[str]): + pass diff --git a/netbox/netbox/graphql/filter_mixins.py b/netbox/netbox/graphql/filter_mixins.py index 2044a1dde..ce6d81a86 100644 --- a/netbox/netbox/graphql/filter_mixins.py +++ b/netbox/netbox/graphql/filter_mixins.py @@ -1,209 +1,109 @@ -from functools import partialmethod -from typing import List +from dataclasses import dataclass +from datetime import datetime +from typing import TypeVar, TYPE_CHECKING, Annotated -import django_filters import strawberry import strawberry_django -from django.core.exceptions import FieldDoesNotExist -from strawberry import auto +from strawberry import ID +from strawberry_django import FilterLookup, DatetimeFilterLookup -from ipam.fields import ASNField -from netbox.graphql.scalars import BigInt -from utilities.fields import ColorField, CounterCacheField -from utilities.filters import * +from core.graphql.filter_mixins import BaseFilterMixin, BaseObjectTypeFilterMixin, ChangeLogFilterMixin +from extras.graphql.filter_mixins import CustomFieldsFilterMixin, JournalEntriesFilterMixin, TagsFilterMixin +from netbox.graphql.filter_lookups import IntegerLookup + +__all__ = ( + 'DistanceFilterMixin', + 'ImageAttachmentFilterMixin', + 'NestedGroupModelFilterMixin', + 'NetBoxModelFilterMixin', + 'OrganizationalModelFilterMixin', + 'PrimaryModelFilterMixin', + 'SyncedDataFilterMixin', + 'WeightFilterMixin', +) + +T = TypeVar('T') -def map_strawberry_type(field): - should_create_function = False - attr_type = None - - # NetBox Filter types - put base classes after derived classes - if isinstance(field, ContentTypeFilter): - should_create_function = True - attr_type = str | None - elif isinstance(field, MultiValueArrayFilter): - pass - elif isinstance(field, MultiValueCharFilter): - # Note: Need to use the legacy FilterLookup from filters, not from - # strawberry_django.FilterLookup as we currently have USE_DEPRECATED_FILTERS - attr_type = strawberry_django.filters.FilterLookup[str] | None - elif isinstance(field, MultiValueDateFilter): - attr_type = auto - elif isinstance(field, MultiValueDateTimeFilter): - attr_type = auto - elif isinstance(field, MultiValueDecimalFilter): - pass - elif isinstance(field, MultiValueMACAddressFilter): - should_create_function = True - attr_type = List[str] | None - elif isinstance(field, MultiValueNumberFilter): - should_create_function = True - attr_type = List[str] | None - elif isinstance(field, MultiValueTimeFilter): - pass - elif isinstance(field, MultiValueWWNFilter): - should_create_function = True - attr_type = List[str] | None - elif isinstance(field, NullableCharFieldFilter): - pass - elif isinstance(field, NumericArrayFilter): - should_create_function = True - attr_type = int | None - elif isinstance(field, TreeNodeMultipleChoiceFilter): - should_create_function = True - attr_type = List[str] | None - - # From django_filters - ordering of these matters as base classes must - # come after derived classes so the base class doesn't get matched first - # a pass for the check (no attr_type) means we don't currently handle - # or use that type - elif issubclass(type(field), django_filters.OrderingFilter): - pass - elif issubclass(type(field), django_filters.BaseRangeFilter): - pass - elif issubclass(type(field), django_filters.BaseInFilter): - pass - elif issubclass(type(field), django_filters.LookupChoiceFilter): - pass - elif issubclass(type(field), django_filters.AllValuesMultipleFilter): - pass - elif issubclass(type(field), django_filters.AllValuesFilter): - pass - elif issubclass(type(field), django_filters.TimeRangeFilter): - pass - elif issubclass(type(field), django_filters.IsoDateTimeFromToRangeFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.DateTimeFromToRangeFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.DateFromToRangeFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.DateRangeFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.RangeFilter): - pass - elif issubclass(type(field), django_filters.NumericRangeFilter): - pass - elif issubclass(type(field), django_filters.NumberFilter): - should_create_function = True - attr_type = int | None - elif issubclass(type(field), django_filters.ModelMultipleChoiceFilter): - should_create_function = True - attr_type = List[str] | None - elif issubclass(type(field), django_filters.ModelChoiceFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.DurationFilter): - pass - elif issubclass(type(field), django_filters.IsoDateTimeFilter): - pass - elif issubclass(type(field), django_filters.DateTimeFilter): - attr_type = auto - elif issubclass(type(field), django_filters.TimeFilter): - attr_type = auto - elif issubclass(type(field), django_filters.DateFilter): - attr_type = auto - elif issubclass(type(field), django_filters.TypedMultipleChoiceFilter): - pass - elif issubclass(type(field), django_filters.MultipleChoiceFilter): - attr_type = str | None - elif issubclass(type(field), django_filters.TypedChoiceFilter): - pass - elif issubclass(type(field), django_filters.ChoiceFilter): - pass - elif issubclass(type(field), django_filters.BooleanFilter): - should_create_function = True - attr_type = bool | None - elif issubclass(type(field), django_filters.UUIDFilter): - should_create_function = True - attr_type = str | None - elif issubclass(type(field), django_filters.CharFilter): - # looks like only used by 'q' - should_create_function = True - attr_type = str | None - - return should_create_function, attr_type +if TYPE_CHECKING: + from .enums import * + from core.graphql.filters import * + from extras.graphql.filters import * -def autotype_decorator(filterset): - """ - Decorator used to auto creates a dataclass used by Strawberry based on a filterset. - Must go after the Strawberry decorator as follows: - - @strawberry_django.filter(models.Example, lookups=True) - @autotype_decorator(filtersets.ExampleFilterSet) - class ExampleFilter(BaseFilterMixin): - pass - - The Filter itself must be derived from BaseFilterMixin. For items listed in meta.fields - of the filterset, usually just a type specifier is generated, so for - `fields = [created, ]` the dataclass would be: - - class ExampleFilter(BaseFilterMixin): - created: auto - - For other filter fields a function needs to be created for Strawberry with the - naming convention `filter_{fieldname}` which is auto detected and called by - Strawberry, this function uses the filterset to handle the query. - """ - def create_attribute_and_function(cls, fieldname, attr_type, should_create_function): - if fieldname not in cls.__annotations__ and attr_type: - cls.__annotations__[fieldname] = attr_type - - filter_name = f"filter_{fieldname}" - if should_create_function and not hasattr(cls, filter_name): - filter_by_filterset = getattr(cls, 'filter_by_filterset') - setattr(cls, filter_name, partialmethod(filter_by_filterset, key=fieldname)) - - def wrapper(cls): - cls.filterset = filterset - fields = filterset.get_fields() - model = filterset._meta.model - for fieldname in fields.keys(): - should_create_function = False - attr_type = auto - if fieldname not in cls.__annotations__: - try: - field = model._meta.get_field(fieldname) - except FieldDoesNotExist: - continue - - if isinstance(field, CounterCacheField): - should_create_function = True - attr_type = BigInt | None - elif isinstance(field, ASNField): - should_create_function = True - attr_type = List[str] | None - elif isinstance(field, ColorField): - should_create_function = True - attr_type = List[str] | None - - create_attribute_and_function(cls, fieldname, attr_type, should_create_function) - - declared_filters = filterset.declared_filters - for fieldname, field in declared_filters.items(): - - should_create_function, attr_type = map_strawberry_type(field) - if attr_type is None: - raise NotImplementedError(f"GraphQL Filter field unknown: {fieldname}: {field}") - - create_attribute_and_function(cls, fieldname, attr_type, should_create_function) - - return cls - - return wrapper +class NetBoxModelFilterMixin( + ChangeLogFilterMixin, + CustomFieldsFilterMixin, + JournalEntriesFilterMixin, + TagsFilterMixin, + BaseObjectTypeFilterMixin, +): + pass -@strawberry.input -class BaseFilterMixin: +@dataclass +class NestedGroupModelFilterMixin(NetBoxModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + lft: IntegerLookup | None = strawberry_django.filter_field() + rght: IntegerLookup | None = strawberry_django.filter_field() + tree_id: IntegerLookup | None = strawberry_django.filter_field() + level: IntegerLookup | None = strawberry_django.filter_field() + parent_id: ID | None = strawberry_django.filter_field() - def filter_by_filterset(self, queryset, key): - filterset = self.filterset(data={key: getattr(self, key)}, queryset=queryset) - if not filterset.is_valid(): - # We could raise validation error but strawberry logs it all to the - # console i.e. raise ValidationError(f"{k}: {v[0]}") - return filterset.qs.none() - return filterset.qs + +@dataclass +class OrganizationalModelFilterMixin( + ChangeLogFilterMixin, + CustomFieldsFilterMixin, + TagsFilterMixin, + BaseObjectTypeFilterMixin, +): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + + +@dataclass +class PrimaryModelFilterMixin(NetBoxModelFilterMixin): + description: FilterLookup[str] | None = strawberry_django.filter_field() + comments: FilterLookup[str] | None = strawberry_django.filter_field() + + +@dataclass +class ImageAttachmentFilterMixin(BaseFilterMixin): + images: Annotated['ImageAttachmentFilter', strawberry.lazy('extras.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class WeightFilterMixin(BaseFilterMixin): + weight: FilterLookup[float] | None = strawberry_django.filter_field() + weight_unit: Annotated['WeightUnitEnum', strawberry.lazy('netbox.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class SyncedDataFilterMixin(BaseFilterMixin): + data_source: Annotated['DataSourceFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + data_source_id: FilterLookup[int] | None = strawberry_django.filter_field() + data_file: Annotated['DataFileFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + data_file_id: FilterLookup[int] | None = strawberry_django.filter_field() + data_path: FilterLookup[str] | None = strawberry_django.filter_field() + auto_sync_enabled: FilterLookup[bool] | None = strawberry_django.filter_field() + data_synced: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + + +@dataclass +class DistanceFilterMixin(BaseFilterMixin): + distance: FilterLookup[float] | None = strawberry_django.filter_field() + distance_unit: Annotated['DistanceUnitEnum', strawberry.lazy('netbox.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/netbox/graphql/types.py b/netbox/netbox/graphql/types.py index a4fc99080..5df4cfd38 100644 --- a/netbox/netbox/graphql/types.py +++ b/netbox/netbox/graphql/types.py @@ -8,6 +8,7 @@ from extras.graphql.mixins import CustomFieldsMixin, JournalEntriesMixin, TagsMi __all__ = ( 'BaseObjectType', + 'ContentTypeType', 'ObjectType', 'OrganizationalObjectType', 'NetBoxObjectType', diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 0ad46b21e..bb187ac73 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -787,7 +787,6 @@ LOCALE_PATHS = ( STRAWBERRY_DJANGO = { "DEFAULT_PK_FIELD_NAME": "id", "TYPE_DESCRIPTION_FROM_MODEL_DOCSTRING": True, - "USE_DEPRECATED_FILTERS": True, } # diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index b04d42d24..ca231526f 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -99,8 +99,8 @@ class GraphQLAPITestCase(APITestCase): # Test OR logic query = """{ location_list( filters: { - status: \"""" + LocationStatusChoices.STATUS_PLANNED + """\", - OR: {status: \"""" + LocationStatusChoices.STATUS_STAGING + """\"} + status: STATUS_PLANNED, + OR: {status: STATUS_STAGING} }) { id site {id} } diff --git a/netbox/tenancy/graphql/enums.py b/netbox/tenancy/graphql/enums.py new file mode 100644 index 000000000..90fc30483 --- /dev/null +++ b/netbox/tenancy/graphql/enums.py @@ -0,0 +1,9 @@ +import strawberry + +from tenancy.choices import * + +__all__ = ( + 'ContactPriorityEnum', +) + +ContactPriorityEnum = strawberry.enum(ContactPriorityChoices.as_enum()) diff --git a/netbox/tenancy/graphql/filter_mixins.py b/netbox/tenancy/graphql/filter_mixins.py new file mode 100644 index 000000000..cc4a4297c --- /dev/null +++ b/netbox/tenancy/graphql/filter_mixins.py @@ -0,0 +1,38 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry import ID + +from core.graphql.filter_mixins import BaseFilterMixin + +if TYPE_CHECKING: + from netbox.graphql.filter_lookups import TreeNodeFilter + from .filters import ContactFilter, TenantFilter, TenantGroupFilter + +__all__ = ( + 'ContactFilterMixin', + 'TenancyFilterMixin', +) + + +@dataclass +class ContactFilterMixin(BaseFilterMixin): + contacts: Annotated['ContactFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + + +@dataclass +class TenancyFilterMixin(BaseFilterMixin): + tenant: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tenant_id: ID | None = strawberry_django.filter_field() + tenant_group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tenant_group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/tenancy/graphql/filters.py b/netbox/tenancy/graphql/filters.py index e82b1cd07..5abfa0a6c 100644 --- a/netbox/tenancy/graphql/filters.py +++ b/netbox/tenancy/graphql/filters.py @@ -1,7 +1,49 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin -from tenancy import filtersets, models +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import ChangeLogFilterMixin +from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin +from netbox.graphql.filter_mixins import ( + NestedGroupModelFilterMixin, + OrganizationalModelFilterMixin, + PrimaryModelFilterMixin, +) +from tenancy import models +from .filter_mixins import ContactFilterMixin + +if TYPE_CHECKING: + from core.graphql.filters import ContentTypeFilter + from circuits.graphql.filters import CircuitFilter + from dcim.graphql.filters import ( + CableFilter, + DeviceFilter, + LocationFilter, + PowerFeedFilter, + RackFilter, + RackReservationFilter, + SiteFilter, + VirtualDeviceContextFilter, + ) + from ipam.graphql.filters import ( + AggregateFilter, + ASNFilter, + ASNRangeFilter, + IPAddressFilter, + IPRangeFilter, + PrefixFilter, + RouteTargetFilter, + VLANFilter, + VRFFilter, + ) + from netbox.graphql.filter_lookups import TreeNodeFilter + from wireless.graphql.filters import WirelessLANFilter, WirelessLinkFilter + from virtualization.graphql.filters import ClusterFilter, VirtualMachineFilter + from vpn.graphql.filters import L2VPNFilter, TunnelFilter + from .enums import * __all__ = ( 'TenantFilter', @@ -14,36 +56,132 @@ __all__ = ( @strawberry_django.filter(models.Tenant, lookups=True) -@autotype_decorator(filtersets.TenantFilterSet) -class TenantFilter(BaseFilterMixin): - pass +class TenantFilter(PrimaryModelFilterMixin, ContactFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + group: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + asns: Annotated['ASNFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + circuits: Annotated['CircuitFilter', strawberry.lazy('circuits.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + sites: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + vlans: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + wireless_lans: Annotated['WirelessLANFilter', strawberry.lazy('wireless.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + route_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + locations: Annotated['LocationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + ip_ranges: Annotated['IPRangeFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + rackreservations: Annotated['RackReservationFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + racks: Annotated['RackFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + vdcs: Annotated['VirtualDeviceContextFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + prefixes: Annotated['PrefixFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cables: Annotated['CableFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + virtual_machines: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vrfs: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + asn_ranges: Annotated['ASNRangeFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + wireless_links: Annotated['WirelessLinkFilter', strawberry.lazy('wireless.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + aggregates: Annotated['AggregateFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + power_feeds: Annotated['PowerFeedFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + devices: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tunnels: Annotated['TunnelFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field() + ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + clusters: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + l2vpns: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.TenantGroup, lookups=True) -@autotype_decorator(filtersets.TenantGroupFilterSet) -class TenantGroupFilter(BaseFilterMixin): - pass +class TenantGroupFilter(OrganizationalModelFilterMixin): + parent: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + parent_id: ID | None = strawberry.UNSET + tenants: Annotated['TenantFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + children: Annotated['TenantGroupFilter', strawberry.lazy('tenancy.graphql.filters'), True] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.Contact, lookups=True) -@autotype_decorator(filtersets.ContactFilterSet) -class ContactFilter(BaseFilterMixin): - pass +class ContactFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + title: FilterLookup[str] | None = strawberry_django.filter_field() + phone: FilterLookup[str] | None = strawberry_django.filter_field() + email: FilterLookup[str] | None = strawberry_django.filter_field() + address: FilterLookup[str] | None = strawberry_django.filter_field() + link: FilterLookup[str] | None = strawberry_django.filter_field() + group: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: Annotated['TreeNodeFilter', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + assignments: Annotated['ContactAssignmentFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ContactRole, lookups=True) -@autotype_decorator(filtersets.ContactRoleFilterSet) -class ContactRoleFilter(BaseFilterMixin): +class ContactRoleFilter(OrganizationalModelFilterMixin): pass @strawberry_django.filter(models.ContactGroup, lookups=True) -@autotype_decorator(filtersets.ContactGroupFilterSet) -class ContactGroupFilter(BaseFilterMixin): - pass +class ContactGroupFilter(NestedGroupModelFilterMixin): + parent: Annotated['ContactGroupFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ContactAssignment, lookups=True) -@autotype_decorator(filtersets.ContactAssignmentFilterSet) -class ContactAssignmentFilter(BaseFilterMixin): - pass +class ContactAssignmentFilter(CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin): + object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + object_id: ID | None = strawberry_django.filter_field() + contact: Annotated['ContactFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + contact_id: ID | None = strawberry_django.filter_field() + role: Annotated['ContactRoleFilter', strawberry.lazy('tenancy.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + role_id: ID | None = strawberry_django.filter_field() + priority: Annotated['ContactPriorityEnum', strawberry.lazy('tenancy.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/tenancy/graphql/mixins.py b/netbox/tenancy/graphql/mixins.py index 9cdba100e..9437a06f2 100644 --- a/netbox/tenancy/graphql/mixins.py +++ b/netbox/tenancy/graphql/mixins.py @@ -9,5 +9,4 @@ __all__ = ( @strawberry.type class ContactAssignmentsMixin: - assignments: List[Annotated["ContactAssignmentType", strawberry.lazy('tenancy.graphql.types')]] # noqa: F821 diff --git a/netbox/tenancy/graphql/types.py b/netbox/tenancy/graphql/types.py index 7baa136b3..c340cdf7c 100644 --- a/netbox/tenancy/graphql/types.py +++ b/netbox/tenancy/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated, List, TYPE_CHECKING import strawberry import strawberry_django @@ -6,8 +6,36 @@ import strawberry_django from extras.graphql.mixins import CustomFieldsMixin, TagsMixin from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBoxObjectType from tenancy import models -from .mixins import ContactAssignmentsMixin from .filters import * +from .mixins import ContactAssignmentsMixin + +if TYPE_CHECKING: + from circuits.graphql.types import CircuitType + from dcim.graphql.types import ( + CableType, + DeviceType, + LocationType, + PowerFeedType, + RackType, + RackReservationType, + SiteType, + VirtualDeviceContextType, + ) + from ipam.graphql.types import ( + AggregateType, + ASNType, + ASNRangeType, + IPAddressType, + IPRangeType, + PrefixType, + RouteTargetType, + VLANType, + VRFType, + ) + from netbox.graphql.types import ContentTypeType + from wireless.graphql.types import WirelessLANType, WirelessLinkType + from virtualization.graphql.types import ClusterType, VirtualMachineType + from vpn.graphql.types import L2VPNType, TunnelType __all__ = ( 'ContactAssignmentType', @@ -23,92 +51,70 @@ __all__ = ( # Tenants # -@strawberry_django.type( - models.Tenant, - fields='__all__', - filters=TenantFilter -) + +@strawberry_django.type(models.Tenant, fields='__all__', filters=TenantFilter) class TenantType(NetBoxObjectType): - group: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None - - asns: List[Annotated["ASNType", strawberry.lazy('ipam.graphql.types')]] - circuits: List[Annotated["CircuitType", strawberry.lazy('circuits.graphql.types')]] - sites: List[Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]] - vlans: List[Annotated["VLANType", strawberry.lazy('ipam.graphql.types')]] - wireless_lans: List[Annotated["WirelessLANType", strawberry.lazy('wireless.graphql.types')]] - route_targets: List[Annotated["RouteTargetType", strawberry.lazy('ipam.graphql.types')]] - locations: List[Annotated["LocationType", strawberry.lazy('dcim.graphql.types')]] - ip_ranges: List[Annotated["IPRangeType", strawberry.lazy('ipam.graphql.types')]] - rackreservations: List[Annotated["RackReservationType", strawberry.lazy('dcim.graphql.types')]] - racks: List[Annotated["RackType", strawberry.lazy('dcim.graphql.types')]] - vdcs: List[Annotated["VirtualDeviceContextType", strawberry.lazy('dcim.graphql.types')]] - prefixes: List[Annotated["PrefixType", strawberry.lazy('ipam.graphql.types')]] - cables: List[Annotated["CableType", strawberry.lazy('dcim.graphql.types')]] - virtual_machines: List[Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]] - vrfs: List[Annotated["VRFType", strawberry.lazy('ipam.graphql.types')]] - asn_ranges: List[Annotated["ASNRangeType", strawberry.lazy('ipam.graphql.types')]] - wireless_links: List[Annotated["WirelessLinkType", strawberry.lazy('wireless.graphql.types')]] - aggregates: List[Annotated["AggregateType", strawberry.lazy('ipam.graphql.types')]] - power_feeds: List[Annotated["PowerFeedType", strawberry.lazy('dcim.graphql.types')]] - devices: List[Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]] - tunnels: List[Annotated["TunnelType", strawberry.lazy('vpn.graphql.types')]] - ip_addresses: List[Annotated["IPAddressType", strawberry.lazy('ipam.graphql.types')]] - clusters: List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]] - l2vpns: List[Annotated["L2VPNType", strawberry.lazy('vpn.graphql.types')]] + group: Annotated['TenantGroupType', strawberry.lazy('tenancy.graphql.types')] | None + contacts: List[Annotated['ContactType', strawberry.lazy('tenancy.graphql.types')]] + asns: List[Annotated['ASNType', strawberry.lazy('ipam.graphql.types')]] + circuits: List[Annotated['CircuitType', strawberry.lazy('circuits.graphql.types')]] + sites: List[Annotated['SiteType', strawberry.lazy('dcim.graphql.types')]] + vlans: List[Annotated['VLANType', strawberry.lazy('ipam.graphql.types')]] + wireless_lans: List[Annotated['WirelessLANType', strawberry.lazy('wireless.graphql.types')]] + route_targets: List[Annotated['RouteTargetType', strawberry.lazy('ipam.graphql.types')]] + locations: List[Annotated['LocationType', strawberry.lazy('dcim.graphql.types')]] + ip_ranges: List[Annotated['IPRangeType', strawberry.lazy('ipam.graphql.types')]] + rackreservations: List[Annotated['RackReservationType', strawberry.lazy('dcim.graphql.types')]] + racks: List[Annotated['RackType', strawberry.lazy('dcim.graphql.types')]] + vdcs: List[Annotated['VirtualDeviceContextType', strawberry.lazy('dcim.graphql.types')]] + prefixes: List[Annotated['PrefixType', strawberry.lazy('ipam.graphql.types')]] + cables: List[Annotated['CableType', strawberry.lazy('dcim.graphql.types')]] + virtual_machines: List[Annotated['VirtualMachineType', strawberry.lazy('virtualization.graphql.types')]] + vrfs: List[Annotated['VRFType', strawberry.lazy('ipam.graphql.types')]] + asn_ranges: List[Annotated['ASNRangeType', strawberry.lazy('ipam.graphql.types')]] + wireless_links: List[Annotated['WirelessLinkType', strawberry.lazy('wireless.graphql.types')]] + aggregates: List[Annotated['AggregateType', strawberry.lazy('ipam.graphql.types')]] + power_feeds: List[Annotated['PowerFeedType', strawberry.lazy('dcim.graphql.types')]] + devices: List[Annotated['DeviceType', strawberry.lazy('dcim.graphql.types')]] + tunnels: List[Annotated['TunnelType', strawberry.lazy('vpn.graphql.types')]] + ip_addresses: List[Annotated['IPAddressType', strawberry.lazy('ipam.graphql.types')]] + clusters: List[Annotated['ClusterType', strawberry.lazy('virtualization.graphql.types')]] + l2vpns: List[Annotated['L2VPNType', strawberry.lazy('vpn.graphql.types')]] -@strawberry_django.type( - models.TenantGroup, - fields='__all__', - filters=TenantGroupFilter -) +@strawberry_django.type(models.TenantGroup, fields='__all__', filters=TenantGroupFilter) class TenantGroupType(OrganizationalObjectType): - parent: Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')] | None + parent: Annotated['TenantGroupType', strawberry.lazy('tenancy.graphql.types')] | None tenants: List[TenantType] - children: List[Annotated["TenantGroupType", strawberry.lazy('tenancy.graphql.types')]] + children: List[Annotated['TenantGroupType', strawberry.lazy('tenancy.graphql.types')]] # # Contacts # -@strawberry_django.type( - models.Contact, - fields='__all__', - filters=ContactFilter -) + +@strawberry_django.type(models.Contact, fields='__all__', filters=ContactFilter) class ContactType(ContactAssignmentsMixin, NetBoxObjectType): - group: Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')] | None + group: Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')] | None -@strawberry_django.type( - models.ContactRole, - fields='__all__', - filters=ContactRoleFilter -) +@strawberry_django.type(models.ContactRole, fields='__all__', filters=ContactRoleFilter) class ContactRoleType(ContactAssignmentsMixin, OrganizationalObjectType): pass -@strawberry_django.type( - models.ContactGroup, - fields='__all__', - filters=ContactGroupFilter -) +@strawberry_django.type(models.ContactGroup, fields='__all__', filters=ContactGroupFilter) class ContactGroupType(OrganizationalObjectType): - parent: Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')] | None + parent: Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')] | None contacts: List[ContactType] - children: List[Annotated["ContactGroupType", strawberry.lazy('tenancy.graphql.types')]] + children: List[Annotated['ContactGroupType', strawberry.lazy('tenancy.graphql.types')]] -@strawberry_django.type( - models.ContactAssignment, - fields='__all__', - filters=ContactAssignmentFilter -) +@strawberry_django.type(models.ContactAssignment, fields='__all__', filters=ContactAssignmentFilter) class ContactAssignmentType(CustomFieldsMixin, TagsMixin, BaseObjectType): - object_type: Annotated["ContentTypeType", strawberry.lazy('netbox.graphql.types')] | None - contact: Annotated["ContactType", strawberry.lazy('tenancy.graphql.types')] | None - role: Annotated["ContactRoleType", strawberry.lazy('tenancy.graphql.types')] | None + object_type: Annotated['ContentTypeType', strawberry.lazy('netbox.graphql.types')] | None + contact: Annotated['ContactType', strawberry.lazy('tenancy.graphql.types')] | None + role: Annotated['ContactRoleType', strawberry.lazy('tenancy.graphql.types')] | None diff --git a/netbox/users/graphql/filters.py b/netbox/users/graphql/filters.py index d30781b1c..8f8a8f946 100644 --- a/netbox/users/graphql/filters.py +++ b/netbox/users/graphql/filters.py @@ -1,7 +1,12 @@ -import strawberry_django +from datetime import datetime +from typing import Annotated -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin -from users import filtersets, models +import strawberry +import strawberry_django +from strawberry_django import DatetimeFilterLookup, FilterLookup + +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin +from users import models __all__ = ( 'GroupFilter', @@ -10,12 +15,20 @@ __all__ = ( @strawberry_django.filter(models.Group, lookups=True) -@autotype_decorator(filtersets.GroupFilterSet) -class GroupFilter(BaseFilterMixin): - pass +class GroupFilter(BaseObjectTypeFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.User, lookups=True) -@autotype_decorator(filtersets.UserFilterSet) -class UserFilter(BaseFilterMixin): - pass +class UserFilter(BaseObjectTypeFilterMixin): + username: FilterLookup[str] | None = strawberry_django.filter_field() + first_name: FilterLookup[str] | None = strawberry_django.filter_field() + last_name: FilterLookup[str] | None = strawberry_django.filter_field() + email: FilterLookup[str] | None = strawberry_django.filter_field() + is_superuser: FilterLookup[bool] | None = strawberry_django.filter_field() + is_staff: FilterLookup[bool] | None = strawberry_django.filter_field() + is_active: FilterLookup[bool] | None = strawberry_django.filter_field() + date_joined: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + last_login: DatetimeFilterLookup[datetime] | None = strawberry_django.filter_field() + groups: Annotated['GroupFilter', strawberry.lazy('users.graphql.filters')] | None = strawberry_django.filter_field() diff --git a/netbox/utilities/choices.py b/netbox/utilities/choices.py index 25d055942..7b3648afa 100644 --- a/netbox/utilities/choices.py +++ b/netbox/utilities/choices.py @@ -1,3 +1,5 @@ +import enum + from django.conf import settings from django.utils.translation import gettext_lazy as _ @@ -65,6 +67,23 @@ class ChoiceSet(metaclass=ChoiceSetMeta): def values(cls): return [c[0] for c in unpack_grouped_choices(cls._choices)] + @classmethod + def as_enum(cls, name=None): + """ + Return the ChoiceSet as an Enum. If no name is provided, "Choices" will be stripped from the class name (if + present) and "Enum" will be appended. For example, "CircuitStatusChoices" will become "CircuitStatusEnum". + """ + name = name or f"{cls.__name__.split('Choices')[0]}Enum" + data = {} + choices = cls.values() + + for attr in dir(cls): + value = getattr(cls, attr) + if attr.isupper() and value in choices: + data[attr] = value + + return enum.Enum(name, data) + def unpack_grouped_choices(choices): """ diff --git a/netbox/virtualization/graphql/enums.py b/netbox/virtualization/graphql/enums.py new file mode 100644 index 000000000..5b1c54e0c --- /dev/null +++ b/netbox/virtualization/graphql/enums.py @@ -0,0 +1,11 @@ +import strawberry + +from virtualization.choices import * + +__all__ = ( + 'ClusterStatusEnum', + 'VirtualMachineStatusEnum', +) + +ClusterStatusEnum = strawberry.enum(ClusterStatusChoices.as_enum()) +VirtualMachineStatusEnum = strawberry.enum(VirtualMachineStatusChoices.as_enum()) diff --git a/netbox/virtualization/graphql/filter_mixins.py b/netbox/virtualization/graphql/filter_mixins.py new file mode 100644 index 000000000..e4c334425 --- /dev/null +++ b/netbox/virtualization/graphql/filter_mixins.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry import ID +from strawberry_django import FilterLookup + +from netbox.graphql.filter_mixins import NetBoxModelFilterMixin + +if TYPE_CHECKING: + from .filters import VirtualMachineFilter + +__all__ = ( + 'VMComponentFilterMixin', +) + + +@dataclass +class VMComponentFilterMixin(NetBoxModelFilterMixin): + virtual_manchine: Annotated['VirtualMachineFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + virtual_machine_id: ID | None = strawberry_django.filter_field() + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/virtualization/graphql/filters.py b/netbox/virtualization/graphql/filters.py index 610275d37..ab4753616 100644 --- a/netbox/virtualization/graphql/filters.py +++ b/netbox/virtualization/graphql/filters.py @@ -1,7 +1,33 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin -from virtualization import filtersets, models +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from dcim.graphql.filter_mixins import InterfaceBaseFilterMixin, RenderConfigFilterMixin, ScopedFilterMixin +from extras.graphql.filter_mixins import ConfigContextFilterMixin +from netbox.graphql.filter_mixins import ( + ImageAttachmentFilterMixin, + OrganizationalModelFilterMixin, + PrimaryModelFilterMixin, +) +from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin +from virtualization import models +from virtualization.graphql.filter_mixins import VMComponentFilterMixin + +if TYPE_CHECKING: + from .enums import * + from netbox.graphql.filter_lookups import FloatLookup, IntegerLookup + from dcim.graphql.filters import DeviceFilter, DeviceRoleFilter, MACAddressFilter, PlatformFilter, SiteFilter + from ipam.graphql.filters import ( + FHRPGroupAssignmentFilter, + IPAddressFilter, + ServiceFilter, + VLANGroupFilter, + VRFFilter, + ) + from vpn.graphql.filters import L2VPNFilter, TunnelTerminationFilter __all__ = ( 'ClusterFilter', @@ -14,36 +40,119 @@ __all__ = ( @strawberry_django.filter(models.Cluster, lookups=True) -@autotype_decorator(filtersets.ClusterFilterSet) -class ClusterFilter(BaseFilterMixin): - pass +class ClusterFilter(ContactFilterMixin, ScopedFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + type: Annotated['ClusterTypeFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + type_id: ID | None = strawberry_django.filter_field() + group: Annotated['ClusterGroupFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + status: Annotated['ClusterStatusEnum', strawberry.lazy('virtualization.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ClusterGroup, lookups=True) -@autotype_decorator(filtersets.ClusterGroupFilterSet) -class ClusterGroupFilter(BaseFilterMixin): - pass +class ClusterGroupFilter(ContactFilterMixin, OrganizationalModelFilterMixin): + vlan_groups: Annotated['VLANGroupFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.ClusterType, lookups=True) -@autotype_decorator(filtersets.ClusterTypeFilterSet) -class ClusterTypeFilter(BaseFilterMixin): +class ClusterTypeFilter(OrganizationalModelFilterMixin): pass @strawberry_django.filter(models.VirtualMachine, lookups=True) -@autotype_decorator(filtersets.VirtualMachineFilterSet) -class VirtualMachineFilter(BaseFilterMixin): - pass +class VirtualMachineFilter( + ContactFilterMixin, + ImageAttachmentFilterMixin, + RenderConfigFilterMixin, + ConfigContextFilterMixin, + TenancyFilterMixin, + PrimaryModelFilterMixin, +): + name: FilterLookup[str] | None = strawberry_django.filter_field() + site: Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + site_id: ID | None = strawberry_django.filter_field() + cluster: Annotated['ClusterFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + cluster_id: ID | None = strawberry_django.filter_field() + device: Annotated['DeviceFilter', strawberry.lazy('dcim.graphql.filters')] | None = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + platform: Annotated['PlatformFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + platform_id: ID | None = strawberry_django.filter_field() + status: Annotated['VirtualMachineStatusEnum', strawberry.lazy('virtualization.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + role: Annotated['DeviceRoleFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + role_id: ID | None = strawberry_django.filter_field() + primary_ip4: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip4_id: ID | None = strawberry_django.filter_field() + primary_ip6: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + primary_ip6_id: ID | None = strawberry_django.filter_field() + vcpus: Annotated['FloatLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + memory: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + disk: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + serial: FilterLookup[str] | None = strawberry_django.filter_field() + interface_count: FilterLookup[int] | None = strawberry_django.filter_field() + virtual_disk_count: FilterLookup[int] | None = strawberry_django.filter_field() + interfaces: Annotated['VMInterfaceFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + services: Annotated['ServiceFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + virtual_disks: Annotated['VirtualDiskFilter', strawberry.lazy('virtualization.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VMInterface, lookups=True) -@autotype_decorator(filtersets.VMInterfaceFilterSet) -class VMInterfaceFilter(BaseFilterMixin): - pass +class VMInterfaceFilter(VMComponentFilterMixin, InterfaceBaseFilterMixin): + ip_addresses: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + vrf: Annotated['VRFFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vrf_id: ID | None = strawberry_django.filter_field() + fhrp_group_assignments: Annotated['FHRPGroupAssignmentFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tunnel_terminations: Annotated['TunnelTerminationFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + l2vpn_terminations: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + mac_addresses: Annotated['MACAddressFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.VirtualDisk, lookups=True) -@autotype_decorator(filtersets.VirtualDiskFilterSet) -class VirtualDiskFilter(BaseFilterMixin): - pass +class VirtualDiskFilter(VMComponentFilterMixin): + size: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/virtualization/graphql/types.py b/netbox/virtualization/graphql/types.py index 33d6ce450..2fcffc20f 100644 --- a/netbox/virtualization/graphql/types.py +++ b/netbox/virtualization/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -10,6 +10,21 @@ from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType from virtualization import models from .filters import * +if TYPE_CHECKING: + from dcim.graphql.types import ( + DeviceRoleType, + DeviceType, + LocationType, + MACAddressType, + PlatformType, + RegionType, + SiteGroupType, + SiteType, + ) + from extras.graphql.types import ConfigTemplateType + from ipam.graphql.types import IPAddressType, ServiceType, VLANTranslationPolicyType, VLANType, VRFType + from tenancy.graphql.types import TenantType + __all__ = ( 'ClusterType', 'ClusterGroupType', @@ -30,7 +45,7 @@ class ComponentType(NetBoxObjectType): @strawberry_django.type( models.Cluster, - exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), + exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'], filters=ClusterFilter ) class ClusterType(VLANGroupsMixin, NetBoxObjectType): diff --git a/netbox/vpn/graphql/enums.py b/netbox/vpn/graphql/enums.py new file mode 100644 index 000000000..053932ade --- /dev/null +++ b/netbox/vpn/graphql/enums.py @@ -0,0 +1,31 @@ +import strawberry + +from vpn.choices import * + +__all__ = ( + 'AuthenticationAlgorithmEnum', + 'AuthenticationMethodEnum', + 'DHGroupEnum', + 'EncryptionAlgorithmEnum', + 'IKEModeEnum', + 'IKEVersionEnum', + 'IPSecModeEnum', + 'L2VPNTypeEnum', + 'TunnelEncapsulationEnum', + 'TunnelStatusEnum', + 'TunnelTerminationRoleEnum', + 'TunnelTerminationTypeEnum', +) + +AuthenticationAlgorithmEnum = strawberry.enum(AuthenticationAlgorithmChoices.as_enum()) +AuthenticationMethodEnum = strawberry.enum(AuthenticationMethodChoices.as_enum()) +DHGroupEnum = strawberry.enum(DHGroupChoices.as_enum()) +EncryptionAlgorithmEnum = strawberry.enum(EncryptionAlgorithmChoices.as_enum()) +IKEModeEnum = strawberry.enum(IKEModeChoices.as_enum()) +IKEVersionEnum = strawberry.enum(IKEVersionChoices.as_enum()) +IPSecModeEnum = strawberry.enum(IPSecModeChoices.as_enum()) +L2VPNTypeEnum = strawberry.enum(L2VPNTypeChoices.as_enum()) +TunnelEncapsulationEnum = strawberry.enum(TunnelEncapsulationChoices.as_enum()) +TunnelStatusEnum = strawberry.enum(TunnelStatusChoices.as_enum()) +TunnelTerminationRoleEnum = strawberry.enum(TunnelTerminationRoleChoices.as_enum()) +TunnelTerminationTypeEnum = strawberry.enum(TunnelTerminationTypeChoices.as_enum()) diff --git a/netbox/vpn/graphql/filters.py b/netbox/vpn/graphql/filters.py index 34594458b..4e12012dd 100644 --- a/netbox/vpn/graphql/filters.py +++ b/netbox/vpn/graphql/filters.py @@ -1,7 +1,21 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin -from vpn import filtersets, models +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import BaseObjectTypeFilterMixin, ChangeLogFilterMixin +from extras.graphql.filter_mixins import CustomFieldsFilterMixin, TagsFilterMixin +from netbox.graphql.filter_mixins import NetBoxModelFilterMixin, OrganizationalModelFilterMixin, PrimaryModelFilterMixin +from tenancy.graphql.filter_mixins import ContactFilterMixin, TenancyFilterMixin +from vpn import models + +if TYPE_CHECKING: + from core.graphql.filters import ContentTypeFilter + from ipam.graphql.filters import IPAddressFilter, RouteTargetFilter + from netbox.graphql.filter_lookups import IntegerLookup + from .enums import * __all__ = ( 'TunnelGroupFilter', @@ -18,60 +32,143 @@ __all__ = ( @strawberry_django.filter(models.TunnelGroup, lookups=True) -@autotype_decorator(filtersets.TunnelGroupFilterSet) -class TunnelGroupFilter(BaseFilterMixin): +class TunnelGroupFilter(OrganizationalModelFilterMixin): pass @strawberry_django.filter(models.TunnelTermination, lookups=True) -@autotype_decorator(filtersets.TunnelTerminationFilterSet) -class TunnelTerminationFilter(BaseFilterMixin): - pass +class TunnelTerminationFilter( + BaseObjectTypeFilterMixin, CustomFieldsFilterMixin, TagsFilterMixin, ChangeLogFilterMixin +): + tunnel: Annotated['TunnelFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field() + tunnel_id: ID | None = strawberry_django.filter_field() + role: Annotated['TunnelTerminationRoleEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + termination_type: Annotated['TunnelTerminationTypeEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + termination_type_id: ID | None = strawberry_django.filter_field() + termination_id: ID | None = strawberry_django.filter_field() + outside_ip: Annotated['IPAddressFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + outside_ip_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.Tunnel, lookups=True) -@autotype_decorator(filtersets.TunnelFilterSet) -class TunnelFilter(BaseFilterMixin): - pass +class TunnelFilter(TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['TunnelStatusEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + group: Annotated['TunnelGroupFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + encapsulation: Annotated['TunnelEncapsulationEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + ipsec_profile: Annotated['IPSecProfileFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + tunnel_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.IKEProposal, lookups=True) -@autotype_decorator(filtersets.IKEProposalFilterSet) -class IKEProposalFilter(BaseFilterMixin): - pass +class IKEProposalFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + authentication_method: Annotated['AuthenticationMethodEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + encryption_algorithm: Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + authentication_algorithm: Annotated['AuthenticationAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + group: Annotated['DHGroupEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() + sa_lifetime: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.IKEPolicy, lookups=True) -@autotype_decorator(filtersets.IKEPolicyFilterSet) -class IKEPolicyFilter(BaseFilterMixin): - pass +class IKEPolicyFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + version: Annotated['IKEVersionEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() + mode: Annotated['IKEModeEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() + proposals: Annotated['IKEProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + preshared_key: FilterLookup[str] | None = strawberry_django.filter_field() @strawberry_django.filter(models.IPSecProposal, lookups=True) -@autotype_decorator(filtersets.IPSecProposalFilterSet) -class IPSecProposalFilter(BaseFilterMixin): - pass +class IPSecProposalFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + encryption_algorithm: Annotated['EncryptionAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + authentication_algorithm: Annotated['AuthenticationAlgorithmEnum', strawberry.lazy('vpn.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + sa_lifetime_seconds: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + sa_lifetime_data: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.IPSecPolicy, lookups=True) -@autotype_decorator(filtersets.IPSecPolicyFilterSet) -class IPSecPolicyFilter(BaseFilterMixin): - pass +class IPSecPolicyFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + proposals: Annotated['IPSecProposalFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + pfs_group: Annotated['DHGroupEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() @strawberry_django.filter(models.IPSecProfile, lookups=True) -@autotype_decorator(filtersets.IPSecProfileFilterSet) -class IPSecProfileFilter(BaseFilterMixin): - pass +class IPSecProfileFilter(PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + mode: Annotated['IPSecModeEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() + ike_policy: Annotated['IKEPolicyFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + ike_policy_id: ID | None = strawberry_django.filter_field() + ipsec_policy: Annotated['IPSecPolicyFilter', strawberry.lazy('vpn.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + ipsec_policy_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.L2VPN, lookups=True) -@autotype_decorator(filtersets.L2VPNFilterSet) -class L2VPNFilter(BaseFilterMixin): - pass +class L2VPNFilter(ContactFilterMixin, TenancyFilterMixin, PrimaryModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + slug: FilterLookup[str] | None = strawberry_django.filter_field() + type: Annotated['L2VPNTypeEnum', strawberry.lazy('vpn.graphql.enums')] | None = strawberry_django.filter_field() + identifier: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) + import_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + export_targets: Annotated['RouteTargetFilter', strawberry.lazy('ipam.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) @strawberry_django.filter(models.L2VPNTermination, lookups=True) -@autotype_decorator(filtersets.L2VPNTerminationFilterSet) -class L2VPNTerminationFilter(BaseFilterMixin): - pass +class L2VPNTerminationFilter(NetBoxModelFilterMixin): + l2vpn: Annotated['L2VPNFilter', strawberry.lazy('vpn.graphql.filters')] | None = strawberry_django.filter_field() + l2vpn_id: ID | None = strawberry_django.filter_field() + assigned_object_type: Annotated['ContentTypeFilter', strawberry.lazy('core.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + assigned_object_id: Annotated['IntegerLookup', strawberry.lazy('netbox.graphql.filter_lookups')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/vpn/graphql/types.py b/netbox/vpn/graphql/types.py index 7940bd326..cc133ee6f 100644 --- a/netbox/vpn/graphql/types.py +++ b/netbox/vpn/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -8,6 +8,13 @@ from netbox.graphql.types import ObjectType, OrganizationalObjectType, NetBoxObj from vpn import models from .filters import * +if TYPE_CHECKING: + from dcim.graphql.types import InterfaceType + from ipam.graphql.types import IPAddressType, RouteTargetType, VLANType + from netbox.graphql.types import ContentTypeType + from tenancy.graphql.types import TenantType + from virtualization.graphql.types import VMInterfaceType + __all__ = ( 'IKEPolicyType', 'IKEProposalType', @@ -125,7 +132,7 @@ class L2VPNType(ContactsMixin, NetBoxObjectType): @strawberry_django.type( models.L2VPNTermination, - exclude=('assigned_object_type', 'assigned_object_id'), + exclude=['assigned_object_type', 'assigned_object_id'], filters=L2VPNTerminationFilter ) class L2VPNTerminationType(NetBoxObjectType): diff --git a/netbox/wireless/graphql/enums.py b/netbox/wireless/graphql/enums.py new file mode 100644 index 000000000..d3c6ad21a --- /dev/null +++ b/netbox/wireless/graphql/enums.py @@ -0,0 +1,17 @@ +import strawberry + +from wireless.choices import * + +__all__ = ( + 'WirelessAuthCipherEnum', + 'WirelessAuthTypeEnum', + 'WirelessChannelEnum', + 'WirelessLANStatusEnum', + 'WirelessRoleEnum', +) + +WirelessAuthCipherEnum = strawberry.enum(WirelessAuthCipherChoices.as_enum()) +WirelessAuthTypeEnum = strawberry.enum(WirelessAuthTypeChoices.as_enum()) +WirelessChannelEnum = strawberry.enum(WirelessChannelChoices.as_enum()) +WirelessLANStatusEnum = strawberry.enum(WirelessLANStatusChoices.as_enum()) +WirelessRoleEnum = strawberry.enum(WirelessRoleChoices.as_enum()) diff --git a/netbox/wireless/graphql/filter_mixins.py b/netbox/wireless/graphql/filter_mixins.py new file mode 100644 index 000000000..636bc8a52 --- /dev/null +++ b/netbox/wireless/graphql/filter_mixins.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass +from typing import Annotated, TYPE_CHECKING + +import strawberry +import strawberry_django +from strawberry_django import FilterLookup + +from core.graphql.filter_mixins import BaseFilterMixin + +if TYPE_CHECKING: + from .enums import * + +__all__ = ( + 'WirelessAuthenticationBaseFilterMixin', +) + + +@dataclass +class WirelessAuthenticationBaseFilterMixin(BaseFilterMixin): + auth_type: Annotated['WirelessAuthTypeEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + auth_cipher: Annotated['WirelessAuthCipherEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + auth_psk: FilterLookup[str] | None = strawberry_django.filter_field() diff --git a/netbox/wireless/graphql/filters.py b/netbox/wireless/graphql/filters.py index 47d04bedc..d71af7ae2 100644 --- a/netbox/wireless/graphql/filters.py +++ b/netbox/wireless/graphql/filters.py @@ -1,7 +1,20 @@ -import strawberry_django +from typing import Annotated, TYPE_CHECKING -from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin -from wireless import filtersets, models +import strawberry +import strawberry_django +from strawberry.scalars import ID +from strawberry_django import FilterLookup + +from dcim.graphql.filter_mixins import ScopedFilterMixin +from netbox.graphql.filter_mixins import DistanceFilterMixin, PrimaryModelFilterMixin, NestedGroupModelFilterMixin +from tenancy.graphql.filter_mixins import TenancyFilterMixin +from wireless import models +from .filter_mixins import WirelessAuthenticationBaseFilterMixin + +if TYPE_CHECKING: + from dcim.graphql.filters import InterfaceFilter + from ipam.graphql.filters import VLANFilter + from .enums import * __all__ = ( 'WirelessLANGroupFilter', @@ -11,18 +24,45 @@ __all__ = ( @strawberry_django.filter(models.WirelessLANGroup, lookups=True) -@autotype_decorator(filtersets.WirelessLANGroupFilterSet) -class WirelessLANGroupFilter(BaseFilterMixin): +class WirelessLANGroupFilter(NestedGroupModelFilterMixin): pass @strawberry_django.filter(models.WirelessLAN, lookups=True) -@autotype_decorator(filtersets.WirelessLANFilterSet) -class WirelessLANFilter(BaseFilterMixin): - pass +class WirelessLANFilter( + WirelessAuthenticationBaseFilterMixin, + ScopedFilterMixin, + TenancyFilterMixin, + PrimaryModelFilterMixin +): + ssid: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) + group: Annotated['WirelessLANGroupFilter', strawberry.lazy('wireless.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + group_id: ID | None = strawberry_django.filter_field() + vlan: Annotated['VLANFilter', strawberry.lazy('ipam.graphql.filters')] | None = strawberry_django.filter_field() + vlan_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter(models.WirelessLink, lookups=True) -@autotype_decorator(filtersets.WirelessLinkFilterSet) -class WirelessLinkFilter(BaseFilterMixin): - pass +class WirelessLinkFilter( + WirelessAuthenticationBaseFilterMixin, + DistanceFilterMixin, + TenancyFilterMixin, + PrimaryModelFilterMixin +): + interface_a: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + interface_a_id: ID | None = strawberry_django.filter_field() + interface_b: Annotated['InterfaceFilter', strawberry.lazy('dcim.graphql.filters')] | None = ( + strawberry_django.filter_field() + ) + interface_b_id: ID | None = strawberry_django.filter_field() + ssid: FilterLookup[str] | None = strawberry_django.filter_field() + status: Annotated['WirelessLANStatusEnum', strawberry.lazy('wireless.graphql.enums')] | None = ( + strawberry_django.filter_field() + ) diff --git a/netbox/wireless/graphql/types.py b/netbox/wireless/graphql/types.py index aa44e9b9f..7c31dfec8 100644 --- a/netbox/wireless/graphql/types.py +++ b/netbox/wireless/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List, Union +from typing import Annotated, List, TYPE_CHECKING, Union import strawberry import strawberry_django @@ -7,6 +7,11 @@ from netbox.graphql.types import OrganizationalObjectType, NetBoxObjectType from wireless import models from .filters import * +if TYPE_CHECKING: + from dcim.graphql.types import DeviceType, InterfaceType, LocationType, RegionType, SiteGroupType, SiteType + from ipam.graphql.types import VLANType + from tenancy.graphql.types import TenantType + __all__ = ( 'WirelessLANType', 'WirelessLANGroupType', @@ -28,7 +33,7 @@ class WirelessLANGroupType(OrganizationalObjectType): @strawberry_django.type( models.WirelessLAN, - exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'), + exclude=['scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'], filters=WirelessLANFilter ) class WirelessLANType(NetBoxObjectType):