1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #15131: Dynamic queryset annotations for REST API endpoints (#15152)

* Introduce RelatedObjectCountField

* Introduce get_annotations_for_serializer() and enable dynamic annotations

* Add RelatedObjectCountFields to serializers; remove static annotations from querysets

* Remove annotations cleanup logic from BriefModeMixin

* Annotate type for RelatedObjectCountField

* Remove redundant field on TagSerializer

* Add missing reverse relationship for power feeds to rack

* Refactor RelatedObjectCountField to take a single relationship name
This commit is contained in:
Jeremy Stretch
2024-02-15 14:49:27 -05:00
committed by GitHub
parent b3f25a400b
commit 7abb2b2ab5
27 changed files with 204 additions and 221 deletions

View File

@ -1,8 +1,8 @@
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from circuits.models import *
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [
@ -36,7 +36,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
)
class NestedProviderSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True)
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
@ -64,7 +64,7 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
)
class NestedCircuitTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType

View File

@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedSiteSerializer
from dcim.api.serializers import CabledObjectSerializer
from ipam.models import ASN
from ipam.api.nested_serializers import NestedASNSerializer
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import *
@ -32,7 +32,7 @@ class ProviderSerializer(NetBoxModelSerializer):
)
# Related object counts
circuit_count = serializers.IntegerField(read_only=True)
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = Provider
@ -80,13 +80,15 @@ class ProviderNetworkSerializer(NetBoxModelSerializer):
class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
class Meta:
model = CircuitType
fields = [
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
'circuit_count',
'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
'last_updated', 'circuit_count',
]

View File

@ -4,7 +4,6 @@ from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from . import serializers
@ -21,9 +20,7 @@ class CircuitsRootView(APIRootView):
#
class ProviderViewSet(NetBoxModelViewSet):
queryset = Provider.objects.annotate(
circuit_count=count_related(Circuit, 'provider')
)
queryset = Provider.objects.all()
serializer_class = serializers.ProviderSerializer
filterset_class = filtersets.ProviderFilterSet
@ -33,9 +30,7 @@ class ProviderViewSet(NetBoxModelViewSet):
#
class CircuitTypeViewSet(NetBoxModelViewSet):
queryset = CircuitType.objects.annotate(
circuit_count=count_related(Circuit, 'type')
)
queryset = CircuitType.objects.all()
serializer_class = serializers.CircuitTypeSerializer
filterset_class = filtersets.CircuitTypeFilterSet

View File

@ -2,7 +2,7 @@ from rest_framework import serializers
from core.choices import *
from core.models import *
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer
from netbox.utils import get_data_backend_choices
from users.api.nested_serializers import NestedUserSerializer
@ -28,9 +28,7 @@ class DataSourceSerializer(NetBoxModelSerializer):
)
# Related object counts
file_count = serializers.IntegerField(
read_only=True
)
file_count = RelatedObjectCountField('datafiles')
class Meta:
model = DataSource

View File

@ -9,7 +9,6 @@ from rest_framework.viewsets import ReadOnlyModelViewSet
from core import filtersets
from core.models import *
from netbox.api.viewsets import NetBoxModelViewSet, NetBoxReadOnlyModelViewSet
from utilities.utils import count_related
from . import serializers
@ -22,9 +21,7 @@ class CoreRootView(APIRootView):
class DataSourceViewSet(NetBoxModelViewSet):
queryset = DataSource.objects.annotate(
file_count=count_related(DataFile, 'source')
)
queryset = DataSource.objects.all()
serializer_class = serializers.DataSourceSerializer
filterset_class = filtersets.DataSourceFilterSet

View File

@ -2,7 +2,8 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from dcim import models
from netbox.api.serializers import BaseModelSerializer, WritableNestedSerializer
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
__all__ = [
'ComponentNestedModuleSerializer',
@ -110,7 +111,7 @@ class NestedLocationSerializer(WritableNestedSerializer):
)
class NestedRackRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
rack_count = RelatedObjectCountField('racks')
class Meta:
model = models.RackRole
@ -122,7 +123,7 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
)
class NestedRackSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
device_count = serializers.IntegerField(read_only=True)
device_count = RelatedObjectCountField('devices')
class Meta:
model = models.Rack
@ -150,7 +151,7 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
)
class NestedManufacturerSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
devicetype_count = RelatedObjectCountField('device_types')
class Meta:
model = models.Manufacturer
@ -163,7 +164,7 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
class NestedDeviceTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
device_count = serializers.IntegerField(read_only=True)
device_count = RelatedObjectCountField('instances')
class Meta:
model = models.DeviceType
@ -173,7 +174,6 @@ class NestedDeviceTypeSerializer(WritableNestedSerializer):
class NestedModuleTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer(read_only=True)
# module_count = serializers.IntegerField(read_only=True)
class Meta:
model = models.ModuleType
@ -274,8 +274,8 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
)
class NestedDeviceRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.DeviceRole
@ -287,8 +287,8 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
)
class NestedPlatformSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = models.Platform
@ -445,7 +445,7 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
)
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = models.InventoryItemRole
@ -490,7 +490,7 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
)
class NestedPowerPanelSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
powerfeed_count = serializers.IntegerField(read_only=True)
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = models.PowerPanel

View File

@ -15,7 +15,7 @@ from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
from ipam.models import ASN, VLAN
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import (
GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
WritableNestedSerializer,
@ -144,12 +144,12 @@ class SiteSerializer(NetBoxModelSerializer):
)
# Related object counts
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
rack_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
circuit_count = RelatedObjectCountField('circuit_terminations')
device_count = RelatedObjectCountField('devices')
prefix_count = RelatedObjectCountField('prefixes')
rack_count = RelatedObjectCountField('racks')
vlan_count = RelatedObjectCountField('vlans')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Site
@ -184,7 +184,9 @@ class LocationSerializer(NestedGroupModelSerializer):
class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
# Related object counts
rack_count = RelatedObjectCountField('racks')
class Meta:
model = RackRole
@ -207,8 +209,10 @@ class RackSerializer(NetBoxModelSerializer):
width = ChoiceField(choices=RackWidthChoices, required=False)
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
powerfeed_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = Rack
@ -299,9 +303,11 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True)
platform_count = serializers.IntegerField(read_only=True)
# Related object counts
devicetype_count = RelatedObjectCountField('device_types')
inventoryitem_count = RelatedObjectCountField('inventory_items')
platform_count = RelatedObjectCountField('platforms')
class Meta:
model = Manufacturer
@ -325,7 +331,6 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
# Counter fields
console_port_template_count = serializers.IntegerField(read_only=True)
@ -339,6 +344,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
module_bay_template_count = serializers.IntegerField(read_only=True)
inventory_item_template_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('instances')
class Meta:
model = DeviceType
fields = [
@ -636,8 +644,10 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = DeviceRole
@ -651,8 +661,10 @@ class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Platform
@ -761,7 +773,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
# Related object counts
interface_count = serializers.IntegerField(read_only=True)
interface_count = RelatedObjectCountField('interfaces')
class Meta:
model = VirtualDeviceContext
@ -1092,7 +1104,9 @@ class InventoryItemSerializer(NetBoxModelSerializer):
class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
# Related object counts
inventoryitem_count = RelatedObjectCountField('inventory_items')
class Meta:
model = InventoryItemRole
@ -1204,7 +1218,9 @@ class PowerPanelSerializer(NetBoxModelSerializer):
allow_null=True,
default=None
)
powerfeed_count = serializers.IntegerField(read_only=True)
# Related object counts
powerfeed_count = RelatedObjectCountField('powerfeeds')
class Meta:
model = PowerPanel

View File

@ -13,7 +13,6 @@ from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
from dcim.models import *
from dcim.svg import CableTraceSVG
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.pagination import StripCountAnnotationsPaginator
@ -23,7 +22,6 @@ from netbox.constants import NESTED_SERIALIZER_PREFIX
from utilities.api import get_serializer_for_model
from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from virtualization.models import VirtualMachine
from . import serializers
from .exceptions import MissingFilterException
@ -129,14 +127,7 @@ class SiteGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class SiteViewSet(NetBoxModelViewSet):
queryset = Site.objects.annotate(
device_count=count_related(Device, 'site'),
rack_count=count_related(Rack, 'site'),
prefix_count=count_related(Prefix, 'site'),
vlan_count=count_related(VLAN, 'site'),
circuit_count=count_related(Circuit, 'terminations__site'),
virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
)
queryset = Site.objects.all()
serializer_class = serializers.SiteSerializer
filterset_class = filtersets.SiteFilterSet
@ -168,9 +159,7 @@ class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class RackRoleViewSet(NetBoxModelViewSet):
queryset = RackRole.objects.annotate(
rack_count=count_related(Rack, 'role')
)
queryset = RackRole.objects.all()
serializer_class = serializers.RackRoleSerializer
filterset_class = filtersets.RackRoleFilterSet
@ -180,10 +169,7 @@ class RackRoleViewSet(NetBoxModelViewSet):
#
class RackViewSet(NetBoxModelViewSet):
queryset = Rack.objects.annotate(
device_count=count_related(Device, 'rack'),
powerfeed_count=count_related(PowerFeed, 'rack')
)
queryset = Rack.objects.all()
serializer_class = serializers.RackSerializer
filterset_class = filtersets.RackFilterSet
@ -255,11 +241,7 @@ class RackReservationViewSet(NetBoxModelViewSet):
#
class ManufacturerViewSet(NetBoxModelViewSet):
queryset = Manufacturer.objects.annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
platform_count=count_related(Platform, 'manufacturer')
)
queryset = Manufacturer.objects.all()
serializer_class = serializers.ManufacturerSerializer
filterset_class = filtersets.ManufacturerFilterSet
@ -269,9 +251,7 @@ class ManufacturerViewSet(NetBoxModelViewSet):
#
class DeviceTypeViewSet(NetBoxModelViewSet):
queryset = DeviceType.objects.annotate(
device_count=count_related(Device, 'device_type')
)
queryset = DeviceType.objects.all()
serializer_class = serializers.DeviceTypeSerializer
filterset_class = filtersets.DeviceTypeFilterSet
@ -351,10 +331,7 @@ class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class DeviceRoleViewSet(NetBoxModelViewSet):
queryset = DeviceRole.objects.annotate(
device_count=count_related(Device, 'role'),
virtualmachine_count=count_related(VirtualMachine, 'role')
)
queryset = DeviceRole.objects.all()
serializer_class = serializers.DeviceRoleSerializer
filterset_class = filtersets.DeviceRoleFilterSet
@ -364,10 +341,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
#
class PlatformViewSet(NetBoxModelViewSet):
queryset = Platform.objects.annotate(
device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform')
)
queryset = Platform.objects.all()
serializer_class = serializers.PlatformSerializer
filterset_class = filtersets.PlatformFilterSet
@ -410,9 +384,7 @@ class DeviceViewSet(
class VirtualDeviceContextViewSet(NetBoxModelViewSet):
queryset = VirtualDeviceContext.objects.annotate(
interface_count=count_related(Interface, 'vdcs'),
)
queryset = VirtualDeviceContext.objects.all()
serializer_class = serializers.VirtualDeviceContextSerializer
filterset_class = filtersets.VirtualDeviceContextFilterSet
@ -513,9 +485,7 @@ class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
#
class InventoryItemRoleViewSet(NetBoxModelViewSet):
queryset = InventoryItemRole.objects.annotate(
inventoryitem_count=count_related(InventoryItem, 'role')
)
queryset = InventoryItemRole.objects.all()
serializer_class = serializers.InventoryItemRoleSerializer
filterset_class = filtersets.InventoryItemRoleFilterSet
@ -552,9 +522,7 @@ class VirtualChassisViewSet(NetBoxModelViewSet):
#
class PowerPanelViewSet(NetBoxModelViewSet):
queryset = PowerPanel.objects.annotate(
powerfeed_count=count_related(PowerFeed, 'power_panel')
)
queryset = PowerPanel.objects.all()
serializer_class = serializers.PowerPanelSerializer
filterset_class = filtersets.PowerPanelFilterSet

View File

@ -233,7 +233,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='powerfeed',
name='rack',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='dcim.rack'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='powerfeeds', to='dcim.rack'),
),
migrations.AddField(
model_name='powerfeed',

View File

@ -84,6 +84,7 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
rack = models.ForeignKey(
to='Rack',
on_delete=models.PROTECT,
related_name='powerfeeds',
blank=True,
null=True
)

View File

@ -3,7 +3,6 @@ from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from rest_framework.fields import ListField
from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer
from core.api.serializers import JobSerializer
@ -16,7 +15,7 @@ from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site
from extras.choices import *
from extras.models import *
from netbox.api.exceptions import SerializerNotFound
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
from netbox.api.serializers.features import TaggableModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
@ -288,7 +287,9 @@ class TagSerializer(ValidatedModelSerializer):
many=True,
required=False
)
tagged_items = serializers.IntegerField(read_only=True)
# Related object counts
tagged_items = RelatedObjectCountField('extras_taggeditem_items')
class Meta:
model = Tag

View File

@ -23,7 +23,7 @@ from netbox.api.metadata import ContentTypeMetadata
from netbox.api.renderers import TextRenderer
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request, count_related
from utilities.utils import copy_safe_request
from . import serializers
from .mixins import ConfigTemplateRenderMixin
@ -147,9 +147,7 @@ class BookmarkViewSet(NetBoxModelViewSet):
#
class TagViewSet(NetBoxModelViewSet):
queryset = Tag.objects.annotate(
tagged_items=count_related(TaggedItem, 'tag')
)
queryset = Tag.objects.all()
serializer_class = serializers.TagSerializer
filterset_class = filtersets.TagFilterSet

View File

@ -2,6 +2,7 @@ from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from ipam import models
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from .field_serializers import IPAddressField
@ -58,7 +59,7 @@ class NestedASNSerializer(WritableNestedSerializer):
)
class NestedVRFSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
prefix_count = serializers.IntegerField(read_only=True)
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
model = models.VRF
@ -86,7 +87,7 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
)
class NestedRIRSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
aggregate_count = RelatedObjectCountField('aggregates')
class Meta:
model = models.RIR
@ -132,8 +133,8 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
)
class NestedRoleSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = models.Role
@ -145,7 +146,7 @@ class NestedRoleSerializer(WritableNestedSerializer):
)
class NestedVLANGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
vlan_count = serializers.IntegerField(read_only=True)
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = models.VLANGroup

View File

@ -6,7 +6,7 @@ from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerial
from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
from ipam.models import *
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
@ -43,8 +43,10 @@ class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
rir = NestedRIRSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
provider_count = serializers.IntegerField(read_only=True)
# Related object counts
site_count = RelatedObjectCountField('sites')
provider_count = RelatedObjectCountField('providers')
class Meta:
model = ASN
@ -90,8 +92,10 @@ class VRFSerializer(NetBoxModelSerializer):
required=False,
many=True
)
ipaddress_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
# Related object counts
ipaddress_count = RelatedObjectCountField('ip_addresses')
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
model = VRF
@ -124,7 +128,9 @@ class RouteTargetSerializer(NetBoxModelSerializer):
class RIRSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
# Related object counts
aggregate_count = RelatedObjectCountField('aggregates')
class Meta:
model = RIR
@ -195,8 +201,10 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = Role
@ -218,9 +226,11 @@ class VLANGroupSerializer(NetBoxModelSerializer):
)
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
scope = serializers.SerializerMethodField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
utilization = serializers.CharField(read_only=True)
# Related object counts
vlan_count = RelatedObjectCountField('vlans')
class Meta:
model = VLANGroup
fields = [
@ -247,7 +257,9 @@ class VLANSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=VLANStatusChoices, required=False)
role = NestedRoleSerializer(required=False, allow_null=True)
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
prefix_count = serializers.IntegerField(read_only=True)
# Related object counts
prefix_count = RelatedObjectCountField('prefixes')
class Meta:
model = VLAN

View File

@ -12,8 +12,6 @@ from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.views import APIView
from circuits.models import Provider
from dcim.models import Site
from ipam import filtersets
from ipam.models import *
from ipam.utils import get_next_available_prefix
@ -22,7 +20,6 @@ from netbox.api.viewsets.mixins import ObjectValidationMixin
from netbox.config import get_config
from netbox.constants import ADVISORY_LOCK_KEYS
from utilities.api import get_serializer_for_model
from utilities.utils import count_related
from . import serializers
@ -45,19 +42,13 @@ class ASNRangeViewSet(NetBoxModelViewSet):
class ASNViewSet(NetBoxModelViewSet):
queryset = ASN.objects.annotate(
site_count=count_related(Site, 'asns'),
provider_count=count_related(Provider, 'asns')
)
queryset = ASN.objects.all()
serializer_class = serializers.ASNSerializer
filterset_class = filtersets.ASNFilterSet
class VRFViewSet(NetBoxModelViewSet):
queryset = VRF.objects.annotate(
ipaddress_count=count_related(IPAddress, 'vrf'),
prefix_count=count_related(Prefix, 'vrf')
)
queryset = VRF.objects.all()
serializer_class = serializers.VRFSerializer
filterset_class = filtersets.VRFFilterSet
@ -69,9 +60,7 @@ class RouteTargetViewSet(NetBoxModelViewSet):
class RIRViewSet(NetBoxModelViewSet):
queryset = RIR.objects.annotate(
aggregate_count=count_related(Aggregate, 'rir')
)
queryset = RIR.objects.all()
serializer_class = serializers.RIRSerializer
filterset_class = filtersets.RIRFilterSet
@ -83,10 +72,7 @@ class AggregateViewSet(NetBoxModelViewSet):
class RoleViewSet(NetBoxModelViewSet):
queryset = Role.objects.annotate(
prefix_count=count_related(Prefix, 'role'),
vlan_count=count_related(VLAN, 'role')
)
queryset = Role.objects.all()
serializer_class = serializers.RoleSerializer
filterset_class = filtersets.RoleFilterSet
@ -151,8 +137,6 @@ class VLANGroupViewSet(NetBoxModelViewSet):
class VLANViewSet(NetBoxModelViewSet):
queryset = VLAN.objects.prefetch_related(
'l2vpn_terminations', # Referenced by VLANSerializer.l2vpn_termination
).annotate(
prefix_count=count_related(Prefix, 'vlan')
)
serializer_class = serializers.VLANSerializer
filterset_class = filtersets.VLANFilterSet

View File

@ -1,6 +1,6 @@
from django.core.exceptions import ObjectDoesNotExist
from drf_spectacular.utils import extend_schema_field
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from netaddr import IPNetwork
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
@ -10,6 +10,7 @@ __all__ = (
'ChoiceField',
'ContentTypeField',
'IPNetworkSerializer',
'RelatedObjectCountField',
'SerializedPKRelatedField',
)
@ -135,3 +136,16 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField):
def to_representation(self, value):
return self.serializer(value, context={'request': self.context['request']}).data
@extend_schema_field(OpenApiTypes.INT64)
class RelatedObjectCountField(serializers.ReadOnlyField):
"""
Represents a read-only integer count of related objects (e.g. the number of racks assigned to a site). This field
is detected by get_annotations_for_serializer() when determining the annotations to be added to a queryset
depending on the serializer fields selected for inclusion in the response.
"""
def __init__(self, relation, **kwargs):
self.relation = relation
super().__init__(**kwargs)

View File

@ -10,7 +10,7 @@ from rest_framework import mixins as drf_mixins
from rest_framework.response import Response
from rest_framework.viewsets import GenericViewSet
from utilities.api import get_prefetches_for_serializer
from utilities.api import get_annotations_for_serializer, get_prefetches_for_serializer
from utilities.exceptions import AbortRequest
from . import mixins
@ -44,15 +44,16 @@ class BaseViewSet(GenericViewSet):
def get_queryset(self):
qs = super().get_queryset()
serializer_class = self.get_serializer_class()
# Dynamically resolve prefetches for included serializer fields and attach them to the queryset
prefetch = get_prefetches_for_serializer(
self.get_serializer_class(),
fields_to_include=self.requested_fields
)
if prefetch:
if prefetch := get_prefetches_for_serializer(serializer_class, fields_to_include=self.requested_fields):
qs = qs.prefetch_related(*prefetch)
# Dynamically resolve annotations for RelatedObjectCountFields on the serializer and attach them to the queryset
if annotations := get_annotations_for_serializer(serializer_class, fields_to_include=self.requested_fields):
qs = qs.annotate(**annotations)
return qs
def get_serializer(self, *args, **kwargs):

View File

@ -52,19 +52,6 @@ class BriefModeMixin:
return self.serializer_class
def get_queryset(self):
qs = super().get_queryset()
if self.brief:
serializer_class = self.get_serializer_class()
# Clear any annotations for fields not present on the nested serializer
for annotation in list(qs.query.annotations.keys()):
if annotation not in serializer_class().fields:
qs.query.annotations.pop(annotation)
return qs
class CustomFieldsMixin:
"""

View File

@ -3,7 +3,7 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from netbox.api.fields import ChoiceField, ContentTypeField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.choices import ContactPriorityChoices
@ -32,16 +32,18 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
class TenantSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
group = NestedTenantGroupSerializer(required=False, allow_null=True)
circuit_count = serializers.IntegerField(read_only=True)
device_count = serializers.IntegerField(read_only=True)
ipaddress_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
rack_count = serializers.IntegerField(read_only=True)
site_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
vrf_count = serializers.IntegerField(read_only=True)
cluster_count = serializers.IntegerField(read_only=True)
# Related object counts
circuit_count = RelatedObjectCountField('circuits')
device_count = RelatedObjectCountField('devices')
rack_count = RelatedObjectCountField('racks')
site_count = RelatedObjectCountField('sites')
ipaddress_count = RelatedObjectCountField('ip_addresses')
prefix_count = RelatedObjectCountField('prefixes')
vlan_count = RelatedObjectCountField('vlans')
vrf_count = RelatedObjectCountField('vrfs')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
cluster_count = RelatedObjectCountField('clusters')
class Meta:
model = Tenant

View File

@ -1,13 +1,8 @@
from rest_framework.routers import APIRootView
from circuits.models import Circuit
from dcim.models import Device, Rack, Site
from ipam.models import IPAddress, Prefix, VLAN, VRF
from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
from tenancy import filtersets
from tenancy.models import *
from utilities.utils import count_related
from virtualization.models import VirtualMachine, Cluster
from . import serializers
@ -36,18 +31,7 @@ class TenantGroupViewSet(MPTTLockedMixin, NetBoxModelViewSet):
class TenantViewSet(NetBoxModelViewSet):
queryset = Tenant.objects.annotate(
circuit_count=count_related(Circuit, 'tenant'),
device_count=count_related(Device, 'tenant'),
ipaddress_count=count_related(IPAddress, 'tenant'),
prefix_count=count_related(Prefix, 'tenant'),
rack_count=count_related(Rack, 'tenant'),
site_count=count_related(Site, 'tenant'),
virtualmachine_count=count_related(VirtualMachine, 'tenant'),
vlan_count=count_related(VLAN, 'tenant'),
vrf_count=count_related(VRF, 'tenant'),
cluster_count=count_related(Cluster, 'tenant')
)
queryset = Tenant.objects.all()
serializer_class = serializers.TenantSerializer
filterset_class = filtersets.TenantFilterSet

View File

@ -11,10 +11,13 @@ from rest_framework import status
from rest_framework.serializers import Serializer
from rest_framework.utils import formatting
from netbox.api.fields import RelatedObjectCountField
from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound
from utilities.utils import count_related
from .utils import dynamic_import
__all__ = (
'get_annotations_for_serializer',
'get_graphql_type_for_model',
'get_prefetches_for_serializer',
'get_serializer_for_model',
@ -131,6 +134,26 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None):
return prefetch_fields
def get_annotations_for_serializer(serializer_class, fields_to_include=None):
"""
Return a mapping of field names to annotations to be applied to the queryset for a serializer.
"""
annotations = {}
# If specific fields are not specified, default to all
if not fields_to_include:
fields_to_include = serializer_class.Meta.fields
model = serializer_class.Meta.model
for field_name, field in serializer_class._declared_fields.items():
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
related_field = model._meta.get_field(field.relation).field
annotations[field_name] = count_related(related_field.model, related_field.name)
return annotations
def rest_api_server_error(request, *args, **kwargs):
"""
Handle exceptions and return a useful error message for REST API requests.

View File

@ -1,6 +1,7 @@
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from virtualization.models import *
@ -23,7 +24,7 @@ __all__ = [
)
class NestedClusterTypeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True)
cluster_count = RelatedObjectCountField('clusters')
class Meta:
model = ClusterType
@ -35,7 +36,7 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
)
class NestedClusterGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True)
cluster_count = RelatedObjectCountField('clusters')
class Meta:
model = ClusterGroup
@ -47,7 +48,7 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
)
class NestedClusterSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
virtualmachine_count = serializers.IntegerField(read_only=True)
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Cluster

View File

@ -8,7 +8,7 @@ from dcim.choices import InterfaceModeChoices
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
from ipam.models import VLAN
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import *
@ -23,7 +23,9 @@ from .nested_serializers import *
class ClusterTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True)
# Related object counts
cluster_count = RelatedObjectCountField('clusters')
class Meta:
model = ClusterType
@ -35,7 +37,9 @@ class ClusterTypeSerializer(NetBoxModelSerializer):
class ClusterGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True)
# Related object counts
cluster_count = RelatedObjectCountField('clusters')
class Meta:
model = ClusterGroup
@ -52,8 +56,10 @@ class ClusterSerializer(NetBoxModelSerializer):
status = ChoiceField(choices=ClusterStatusChoices, required=False)
tenant = NestedTenantSerializer(required=False, allow_null=True)
site = NestedSiteSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
# Related object counts
device_count = RelatedObjectCountField('devices')
virtualmachine_count = RelatedObjectCountField('virtual_machines')
class Meta:
model = Cluster

View File

@ -1,10 +1,8 @@
from rest_framework.routers import APIRootView
from dcim.models import Device
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.query_functions import CollateAsChar
from utilities.utils import count_related
from virtualization import filtersets
from virtualization.models import *
from . import serializers
@ -23,26 +21,19 @@ class VirtualizationRootView(APIRootView):
#
class ClusterTypeViewSet(NetBoxModelViewSet):
queryset = ClusterType.objects.annotate(
cluster_count=count_related(Cluster, 'type')
)
queryset = ClusterType.objects.all()
serializer_class = serializers.ClusterTypeSerializer
filterset_class = filtersets.ClusterTypeFilterSet
class ClusterGroupViewSet(NetBoxModelViewSet):
queryset = ClusterGroup.objects.annotate(
cluster_count=count_related(Cluster, 'group')
)
queryset = ClusterGroup.objects.all()
serializer_class = serializers.ClusterGroupSerializer
filterset_class = filtersets.ClusterGroupFilterSet
class ClusterViewSet(NetBoxModelViewSet):
queryset = Cluster.objects.annotate(
device_count=count_related(Device, 'cluster'),
virtualmachine_count=count_related(VirtualMachine, 'cluster')
)
queryset = Cluster.objects.all()
serializer_class = serializers.ClusterSerializer
filterset_class = filtersets.ClusterFilterSet

View File

@ -1,6 +1,7 @@
from drf_spectacular.utils import extend_schema_serializer
from rest_framework import serializers
from netbox.api.fields import RelatedObjectCountField
from netbox.api.serializers import WritableNestedSerializer
from vpn import models
@ -23,7 +24,7 @@ __all__ = (
)
class NestedTunnelGroupSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True)
tunnel_count = RelatedObjectCountField('tunnels')
class Meta:
model = models.TunnelGroup

View File

@ -4,7 +4,7 @@ from rest_framework import serializers
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer
from ipam.models import RouteTarget
from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
from netbox.api.serializers import NetBoxModelSerializer
from netbox.constants import NESTED_SERIALIZER_PREFIX
from tenancy.api.nested_serializers import NestedTenantSerializer
@ -29,7 +29,9 @@ __all__ = (
class TunnelGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
tunnel_count = serializers.IntegerField(read_only=True)
# Related object counts
tunnel_count = RelatedObjectCountField('tunnels')
class Meta:
model = TunnelGroup
@ -59,11 +61,14 @@ class TunnelSerializer(NetBoxModelSerializer):
allow_null=True
)
# Related object counts
terminations_count = RelatedObjectCountField('terminations')
class Meta:
model = Tunnel
fields = (
'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count',
)

View File

@ -1,7 +1,6 @@
from rest_framework.routers import APIRootView
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from vpn import filtersets
from vpn.models import *
from . import serializers
@ -34,17 +33,13 @@ class VPNRootView(APIRootView):
#
class TunnelGroupViewSet(NetBoxModelViewSet):
queryset = TunnelGroup.objects.annotate(
tunnel_count=count_related(Tunnel, 'group')
)
queryset = TunnelGroup.objects.all()
serializer_class = serializers.TunnelGroupSerializer
filterset_class = filtersets.TunnelGroupFilterSet
class TunnelViewSet(NetBoxModelViewSet):
queryset = Tunnel.objects.annotate(
terminations_count=count_related(TunnelTermination, 'tunnel')
)
queryset = Tunnel.objects.all()
serializer_class = serializers.TunnelSerializer
filterset_class = filtersets.TunnelFilterSet