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

Fixes #23424: Document object IDs as int64 in the OpenAPI schema (#23428)

Jeremy Stretch 5 дней назад
Родитель
Сommit
3bc04b56ae

+ 4 - 4
netbox/circuits/filtersets.py

@@ -9,9 +9,9 @@ from ipam.models import ASN
 from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from utilities.filters import (
+    MultiValueBigNumberFilter,
     MultiValueCharFilter,
     MultiValueContentTypeFilter,
-    MultiValueNumberFilter,
     TreeNodeMultipleChoiceFilter,
 )
 from utilities.filtersets import register_filterset
@@ -415,7 +415,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
         field_name='cid',
         label=_('Circuit (CID)'),
     )
-    circuit_id = MultiValueNumberFilter(
+    circuit_id = MultiValueBigNumberFilter(
         method='filter_circuit',
         field_name='pk',
         label=_('Circuit (ID)'),
@@ -425,7 +425,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
         field_name='cid',
         label=_('Virtual circuit (CID)'),
     )
-    virtual_circuit_id = MultiValueNumberFilter(
+    virtual_circuit_id = MultiValueBigNumberFilter(
         method='filter_virtual_circuit',
         field_name='pk',
         label=_('Virtual circuit (ID)'),
@@ -435,7 +435,7 @@ class CircuitGroupAssignmentFilterSet(NetBoxModelFilterSet):
         field_name='slug',
         label=_('Provider (name)'),
     )
-    provider_id = MultiValueNumberFilter(
+    provider_id = MultiValueBigNumberFilter(
         method='filter_provider',
         field_name='pk',
         label=_('Provider (ID)'),

+ 30 - 3
netbox/core/api/schema.py

@@ -4,6 +4,7 @@ import typing
 from collections import OrderedDict
 
 from django.core.exceptions import ImproperlyConfigured
+from django.db import models
 from django.utils.translation import gettext_lazy as _
 from drf_spectacular.contrib.django_filters import DjangoFilterExtension
 from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
@@ -18,7 +19,7 @@ from drf_spectacular.plumbing import (
 )
 from drf_spectacular.types import OpenApiTypes
 from drf_spectacular.utils import Direction, OpenApiParameter, OpenApiResponse
-from rest_framework.fields import ReadOnlyField
+from rest_framework.fields import IntegerField, ReadOnlyField
 from rest_framework.utils import model_meta
 
 from netbox.api.fields import ChoiceField
@@ -93,6 +94,16 @@ def viewset_handles_bulk_create(view):
     return isinstance(view, NetBoxModelViewSet)
 
 
+def int64_for_bigint(schema, model_field):
+    """
+    Document an integer schema as int64 if the model field behind it is a 64-bit integer. drf-spectacular infers
+    int64 only from a field's validator bounds, which primary keys and many other 64-bit fields lack.
+    """
+    if isinstance(model_field, models.BigIntegerField) and isinstance(schema, dict) and schema.get('type') == 'integer':
+        return {**schema, 'format': 'int64'}
+    return schema
+
+
 class NetBoxAutoSchema(AutoSchema):
     """
     Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
@@ -101,6 +112,7 @@ class NetBoxAutoSchema(AutoSchema):
         3. bulk operations don't have filter params
         4. bulk operations don't have pagination
         5. bulk delete should specify input
+        6. 64-bit integer fields (including primary keys) should be documented as int64
     """
 
     writable_serializers = {}
@@ -454,6 +466,21 @@ class NetBoxAutoSchema(AutoSchema):
             ]
         return params
 
+    def _map_model_field(self, model_field, direction):
+        # Covers path parameters, related object filters, and references to related objects
+        schema = super()._map_model_field(model_field, direction)
+        return int64_for_bigint(schema, model_field)
+
+    def _map_serializer_field(self, field, direction, bypass_extensions=False):
+        schema = super()._map_serializer_field(field, direction, bypass_extensions)
+        # Covers integer serializer fields backed by a model field, such as `id`
+        if isinstance(field, IntegerField):
+            model = getattr(getattr(getattr(field, 'parent', None), 'Meta', None), 'model', None)
+            if model is not None:
+                model_field = follow_field_source(model, field.source_attrs, emit_warnings=False)
+                schema = int64_for_bigint(schema, model_field)
+        return schema
+
     def get_filter_backends(self):
         # bulk operations don't have filter params
         if self.is_bulk_action:
@@ -544,7 +571,7 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
             serializer = self.target.serializer(nested=self.target.nested)
             component = auto_schema.resolve_serializer(serializer, direction)
             return component.ref if component else None
-        return build_basic_type(OpenApiTypes.INT)
+        return build_basic_type(OpenApiTypes.INT64)
 
 
 class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
@@ -577,7 +604,7 @@ class FixWritableNestedSerializerAllowPK(OpenApiSerializerFieldExtension):
         if direction == 'request' and self.target.nested:
             return {
                 'oneOf': [
-                    build_basic_type(OpenApiTypes.INT),
+                    build_basic_type(OpenApiTypes.INT64),
                     schema,
                 ]
             }

+ 59 - 0
netbox/core/tests/test_openapi_schema.py

@@ -336,6 +336,64 @@ class OpenAPISchemaTestCase(TestCase):
             with self.subTest(component=component, field=field):
                 self.assertEqual(components[component]['properties'][field]['items']['type'], 'integer')
 
+    def get_parameter(self, path, name):
+        parameters = self.schema['paths'][path]['get']['parameters']
+        return {parameter['name']: parameter['schema'] for parameter in parameters}[name]
+
+    def test_object_ids_are_int64(self):
+        """
+        Object IDs, and every reference to one, must be documented as 64-bit integers.
+
+        Refs: #23424
+        """
+        components = self.schema['components']['schemas']
+        get_parameter = self.get_parameter
+
+        for label, schema in (
+            ('object ID', components['Site']['properties']['id']),
+            ('bulk update ID', components['BulkSiteRequest']['properties']['id']),
+            ('related object', components['DeviceRequest']['properties']['site']['oneOf'][0]),
+            ('related objects', components['SiteRequest']['properties']['asns']['items']),
+            ('generic object ID', components['GenericObject']['properties']['object_id']),
+            ('generic object ID on a model', components['Prefix']['properties']['scope_id']),
+            ('path parameter', get_parameter('/api/dcim/sites/{id}/', 'id')),
+            ('path parameter from a URL pattern', get_parameter('/api/ipam/prefixes/{id}/available-ips/', 'id')),
+            ('ID filter', get_parameter('/api/dcim/sites/', 'id')['items']),
+            ('related object filter', get_parameter('/api/dcim/devices/', 'site_id')['items']),
+            ('tree node filter', get_parameter('/api/dcim/sites/', 'region_id')['items']),
+            ('number filter on an object ID', get_parameter('/api/ipam/ip-addresses/', 'device_id')['items']),
+            ('generic object ID filter', get_parameter('/api/core/jobs/', 'object_id')['items']),
+            ('scope filter', get_parameter('/api/ipam/vlan-groups/', 'site')['items']),
+            ('filter method on an object ID', get_parameter('/api/ipam/vlans/', 'related_to_site')['items']),
+            ('filter method on a related object', get_parameter('/api/ipam/vlans/', 'available_at_site')),
+            ('filter method on related objects', get_parameter('/api/ipam/fhrp-groups/', 'related_ip')['items']),
+            ('number filter method on an object ID', get_parameter('/api/dcim/interfaces/', 'vlan_id')),
+            ('filter method shared with a non-ID filter', get_parameter('/api/ipam/prefixes/', 'present_in_vrf_id')),
+        ):
+            with self.subTest(label):
+                self.assertEqual(schema['type'], 'integer')
+                self.assertEqual(schema.get('format'), 'int64')
+
+    def test_non_id_filters_keep_their_types(self):
+        """
+        Filters which don't reference an object ID must not be documented as 64-bit integers.
+
+        Refs: #23424
+        """
+        get_parameter = self.get_parameter
+
+        for label, schema, expected_type, expected_format in (
+            ('number filter', get_parameter('/api/ipam/vlans/', 'vid')['items'], 'integer', 'int32'),
+            ('number filter lookup', get_parameter('/api/dcim/interfaces/', 'mtu__gte')['items'], 'integer', 'int32'),
+            ('content type filter', get_parameter('/api/extras/tags/', 'content_type_id')['items'], 'integer', 'int32'),
+            ('VRF RD filter', get_parameter('/api/ipam/prefixes/', 'present_in_vrf'), 'string', None),
+            # Refs: #13459
+            ('tree node slug filter', get_parameter('/api/tenancy/tenants/', 'group')['items'], 'string', None),
+        ):
+            with self.subTest(label):
+                self.assertEqual(schema['type'], expected_type)
+                self.assertEqual(schema.get('format'), expected_format)
+
     def test_script_run_operation_exists(self):
         """
         Encodes presence of extras_scripts_run operation in schema as expected.
@@ -448,4 +506,5 @@ class SerializedPKRelatedFieldSchemaTestCase(SimpleTestCase):
         schema = FixSerializedPKRelatedField(field).map_serializer_field(auto_schema, 'request')
 
         self.assertEqual(schema['type'], 'integer')
+        self.assertEqual(schema.get('format'), 'int64')
         self.assertEqual(auto_schema.resolved, [])

+ 34 - 34
netbox/dcim/filtersets.py

@@ -31,7 +31,6 @@ from utilities.filters import (
     MultiValueCharFilter,
     MultiValueContentTypeFilter,
     MultiValueMACAddressFilter,
-    MultiValueNumberFilter,
     MultiValueTimeZoneFilter,
     MultiValueWWNFilter,
     NumericArrayFilter,
@@ -1256,7 +1255,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
         label=_('Role (slug)'),
     )
     component_type = MultiValueContentTypeFilter()
-    component_id = MultiValueNumberFilter()
+    component_id = MultiValueBigNumberFilter()
 
     class Meta:
         model = InventoryItemTemplate
@@ -1365,7 +1364,7 @@ class PlatformFilterSet(NestedGroupModelFilterSet):
         model = Platform
         fields = ('id', 'name', 'slug', 'description')
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_device_type(self, queryset, name, value):
         """
         Return all Platforms available for a specific manufacturer based on device type and Platforms not assigned any
@@ -2241,7 +2240,7 @@ class MACAddressFilterSet(PrimaryModelFilterSet):
         field_name='name',
         label=_('Device (name)'),
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_device',
         field_name='pk',
         label=_('Device (ID)'),
@@ -2251,7 +2250,7 @@ class MACAddressFilterSet(PrimaryModelFilterSet):
         field_name='name',
         label=_('Virtual machine (name)'),
     )
-    virtual_machine_id = MultiValueNumberFilter(
+    virtual_machine_id = MultiValueBigNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
         label=_('Virtual machine (ID)'),
@@ -2398,6 +2397,7 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
         label=_('VLAN Translation Policy'),
     )
 
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_vlan_id(self, queryset, name, value):
         return self._filter_assigned_vlan(queryset, 'id', value)
 
@@ -2430,7 +2430,7 @@ class InterfaceFilterSet(
         field_name='name',
         label=_('Virtual Chassis Interfaces for Device when device is master')
     )
-    virtual_chassis_member_or_master_id = MultiValueNumberFilter(
+    virtual_chassis_member_or_master_id = MultiValueBigNumberFilter(
         method='filter_virtual_chassis_member_or_master',
         field_name='pk',
         label=_('Virtual Chassis Interfaces for Device when device is master (ID)')
@@ -2440,7 +2440,7 @@ class InterfaceFilterSet(
         field_name='name',
         label=_('Virtual Chassis Interfaces for Device')
     )
-    virtual_chassis_member_id = MultiValueNumberFilter(
+    virtual_chassis_member_id = MultiValueBigNumberFilter(
         method='filter_virtual_chassis_member',
         field_name='pk',
         label=_('Virtual Chassis Interfaces for Device (ID)')
@@ -2719,7 +2719,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
         label=_('Role (slug)'),
     )
     component_type = MultiValueContentTypeFilter()
-    component_id = MultiValueNumberFilter()
+    component_id = MultiValueBigNumberFilter()
     serial = MultiValueCharFilter(
         lookup_expr='iexact'
     )
@@ -2859,14 +2859,14 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
     termination_a_type = MultiValueContentTypeFilter(
         field_name='terminations__termination_type'
     )
-    termination_a_id = MultiValueNumberFilter(
+    termination_a_id = MultiValueBigNumberFilter(
         method='filter_by_cable_end_a',
         field_name='terminations__termination_id'
     )
     termination_b_type = MultiValueContentTypeFilter(
         field_name='terminations__termination_type'
     )
-    termination_b_id = MultiValueNumberFilter(
+    termination_b_id = MultiValueBigNumberFilter(
         method='filter_by_cable_end_b',
         field_name='terminations__termination_id'
     )
@@ -2900,14 +2900,14 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         choices=ColorChoices,
         distinct=False,
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_by_termination'
     )
     device = MultiValueCharFilter(
         method='filter_by_termination',
         field_name='device__name'
     )
-    rack_id = MultiValueNumberFilter(
+    rack_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         field_name='rack_id'
     )
@@ -2915,7 +2915,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         method='filter_by_termination',
         field_name='rack__name'
     )
-    location_id = MultiValueNumberFilter(
+    location_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         field_name='location_id'
     )
@@ -2923,7 +2923,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         method='filter_by_termination',
         field_name='location__name'
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         field_name='site_id'
     )
@@ -2933,31 +2933,31 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
     )
 
     # Termination object filters
-    consoleport_id = MultiValueNumberFilter(
+    consoleport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleport'
     )
-    consoleserverport_id = MultiValueNumberFilter(
+    consoleserverport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleserverport'
     )
-    powerport_id = MultiValueNumberFilter(
+    powerport_id = MultiValueBigNumberFilter(
         method='filter_by_powerport'
     )
-    poweroutlet_id = MultiValueNumberFilter(
+    poweroutlet_id = MultiValueBigNumberFilter(
         method='filter_by_poweroutlet'
     )
-    interface_id = MultiValueNumberFilter(
+    interface_id = MultiValueBigNumberFilter(
         method='filter_by_interface'
     )
-    frontport_id = MultiValueNumberFilter(
+    frontport_id = MultiValueBigNumberFilter(
         method='filter_by_frontport'
     )
-    rearport_id = MultiValueNumberFilter(
+    rearport_id = MultiValueBigNumberFilter(
         method='filter_by_rearport'
     )
-    powerfeed_id = MultiValueNumberFilter(
+    powerfeed_id = MultiValueBigNumberFilter(
         method='filter_by_powerfeed'
     )
-    circuittermination_id = MultiValueNumberFilter(
+    circuittermination_id = MultiValueBigNumberFilter(
         method='filter_by_circuittermination'
     )
 
@@ -3050,31 +3050,31 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
     termination_type = MultiValueContentTypeFilter()
 
     # Termination object filters
-    consoleport_id = MultiValueNumberFilter(
+    consoleport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleport'
     )
-    consoleserverport_id = MultiValueNumberFilter(
+    consoleserverport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleserverport'
     )
-    powerport_id = MultiValueNumberFilter(
+    powerport_id = MultiValueBigNumberFilter(
         method='filter_by_powerport'
     )
-    poweroutlet_id = MultiValueNumberFilter(
+    poweroutlet_id = MultiValueBigNumberFilter(
         method='filter_by_poweroutlet'
     )
-    interface_id = MultiValueNumberFilter(
+    interface_id = MultiValueBigNumberFilter(
         method='filter_by_interface'
     )
-    frontport_id = MultiValueNumberFilter(
+    frontport_id = MultiValueBigNumberFilter(
         method='filter_by_frontport'
     )
-    rearport_id = MultiValueNumberFilter(
+    rearport_id = MultiValueBigNumberFilter(
         method='filter_by_rearport'
     )
-    powerfeed_id = MultiValueNumberFilter(
+    powerfeed_id = MultiValueBigNumberFilter(
         method='filter_by_powerfeed'
     )
-    circuittermination_id = MultiValueNumberFilter(
+    circuittermination_id = MultiValueBigNumberFilter(
         method='filter_by_circuittermination'
     )
 
@@ -3441,7 +3441,7 @@ class ConnectionFilterSet(BaseFilterSet):
         method='search',
         label=_('Search'),
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_connections',
         field_name='device__site_id'
     )
@@ -3449,7 +3449,7 @@ class ConnectionFilterSet(BaseFilterSet):
         method='filter_connections',
         field_name='device__site__slug'
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_connections',
         field_name='device_id'
     )

+ 2 - 0
netbox/extras/api/serializers_/configtemplates.py

@@ -1,3 +1,4 @@
+from django.db import models
 from django.utils.translation import gettext_lazy as _
 from drf_spectacular.extensions import OpenApiSerializerExtension
 from rest_framework import serializers
@@ -49,6 +50,7 @@ class RenderConfigInputSerializer(serializers.Serializer):
     """
     config_template_id = serializers.IntegerField(
         required=False,
+        max_value=models.BigIntegerField.MAX_BIGINT,
         help_text=_(
             "Optional ID of the ConfigTemplate to render. If omitted, the object's assigned "
             "config template is used."

+ 7 - 2
netbox/extras/filtersets.py

@@ -10,7 +10,12 @@ from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxM
 from tenancy.models import Tenant, TenantGroup
 from users.filterset_mixins import OwnerFilterMixin
 from users.models import Group, User
-from utilities.filters import MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter
+from utilities.filters import (
+    MultiValueBigNumberFilter,
+    MultiValueCharFilter,
+    MultiValueContentTypeFilter,
+    MultiValueNumberFilter,
+)
 from utilities.filtersets import register_filterset
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
@@ -121,7 +126,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
         label=_('Action available'),
     )
     action_object_type = MultiValueContentTypeFilter()
-    action_object_id = MultiValueNumberFilter()
+    action_object_id = MultiValueBigNumberFilter()
 
     class Meta:
         model = EventRule

+ 37 - 26
netbox/ipam/filtersets.py

@@ -18,6 +18,7 @@ from netbox.filtersets import (
 )
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from utilities.filters import (
+    MultiValueBigNumberFilter,
     MultiValueCharFilter,
     MultiValueContentTypeFilter,
     MultiValueNumberFilter,
@@ -380,7 +381,7 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
     )
     present_in_vrf_id = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
-        method='filter_present_in_vrf',
+        method='filter_present_in_vrf_id',
         label=_('VRF')
     )
     present_in_vrf = django_filters.ModelChoiceFilter(
@@ -498,6 +499,11 @@ class PrefixFilterSet(PrimaryModelFilterSet, ScopedFilterSet, TenancyFilterSet,
             Q(vrf__export_targets__in=vrf.import_targets.all())
         ).distinct()
 
+    # Distinct from filter_present_in_vrf() (which takes an RD) so that the VRF ID can be documented as int64
+    @extend_schema_field(OpenApiTypes.INT64)
+    def filter_present_in_vrf_id(self, queryset, name, vrf):
+        return self.filter_present_in_vrf(queryset, name, vrf)
+
 
 @register_filterset
 class IPRangeFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
@@ -639,7 +645,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
     )
     present_in_vrf_id = django_filters.ModelChoiceFilter(
         queryset=VRF.objects.all(),
-        method='filter_present_in_vrf',
+        method='filter_present_in_vrf_id',
         label=_('VRF')
     )
     present_in_vrf = django_filters.ModelChoiceFilter(
@@ -654,7 +660,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
         field_name='name',
         label=_('Device (name)'),
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_device',
         field_name='pk',
         label=_('Device (ID)'),
@@ -664,7 +670,7 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
         field_name='name',
         label=_('Virtual machine (name)'),
     )
-    virtual_machine_id = MultiValueNumberFilter(
+    virtual_machine_id = MultiValueBigNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
         label=_('Virtual machine (ID)'),
@@ -774,6 +780,11 @@ class IPAddressFilterSet(PrimaryModelFilterSet, TenancyFilterSet, ContactModelFi
             Q(vrf__export_targets__in=vrf.import_targets.all())
         ).distinct()
 
+    # Distinct from filter_present_in_vrf() (which takes an RD) so that the VRF ID can be documented as int64
+    @extend_schema_field(OpenApiTypes.INT64)
+    def filter_present_in_vrf_id(self, queryset, name, vrf):
+        return self.filter_present_in_vrf(queryset, name, vrf)
+
     def filter_device(self, queryset, name, value):
         devices = Device.objects.filter(**{'{}__in'.format(name): value})
         if not devices.exists():
@@ -848,7 +859,7 @@ class FHRPGroupFilterSet(PrimaryModelFilterSet):
             Q(name__icontains=value)
         )
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_related_ip(self, queryset, name, value):
         """
         Filter by VRF & prefix of assigned IP addresses.
@@ -883,7 +894,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
         field_name='name',
         label=_('Device (name)'),
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_device',
         field_name='pk',
         label=_('Device (ID)'),
@@ -893,7 +904,7 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
         field_name='name',
         label=_('Virtual machine (name)'),
     )
-    virtual_machine_id = MultiValueNumberFilter(
+    virtual_machine_id = MultiValueBigNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
         label=_('Virtual machine (ID)'),
@@ -929,28 +940,28 @@ class FHRPGroupAssignmentFilterSet(ChangeLoggedModelFilterSet):
 @register_filterset
 class VLANGroupFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
     scope_type = MultiValueContentTypeFilter()
-    region = MultiValueNumberFilter(
+    region = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    site_group = MultiValueNumberFilter(
+    site_group = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    site = MultiValueNumberFilter(
+    site = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    location = MultiValueNumberFilter(
+    location = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    rack_group = MultiValueNumberFilter(
+    rack_group = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    rack = MultiValueNumberFilter(
+    rack = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    cluster_group = MultiValueNumberFilter(
+    cluster_group = MultiValueBigNumberFilter(
         method='filter_scope'
     )
-    cluster = MultiValueNumberFilter(
+    cluster = MultiValueBigNumberFilter(
         method='filter_scope'
     )
     contains_vid = django_filters.NumberFilter(
@@ -1124,35 +1135,35 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
             pass
         return queryset.filter(qs_filter)
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_site(self, queryset, name, value):
         return queryset.get_for_site(value)
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_site_group(self, queryset, name, value):
         return queryset.get_for_site_group(value)
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_device(self, queryset, name, value):
         return queryset.get_for_device(value)
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_virtualmachine(self, queryset, name, value):
         return queryset.get_for_virtualmachine(value)
 
-    @extend_schema_field(OpenApiTypes.INT)
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_related_to_site(self, queryset, name, value):
         if not value:
             return queryset
         return queryset.get_related_to_sites(value)
 
-    @extend_schema_field(OpenApiTypes.INT)
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_related_to_site_negated(self, queryset, name, value):
         if not value:
             return queryset
         return queryset.get_related_to_sites(value, negate=True)
 
-    @extend_schema_field(OpenApiTypes.INT)
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_interface_id(self, queryset, name, value):
         if value is None:
             return queryset.none()
@@ -1160,7 +1171,7 @@ class VLANFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
         untagged = queryset.filter(interfaces_as_untagged=value)
         return queryset.filter(pk__in=tagged.union(untagged).values('pk'))
 
-    @extend_schema_field(OpenApiTypes.INT)
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_vminterface_id(self, queryset, name, value):
         if value is None:
             return queryset.none()
@@ -1379,7 +1390,7 @@ class ServiceFilterSet(ServicePortMappingFilterMixin, ContactModelFilterSet, Pri
         field_name='name',
         label=_('Device (name)'),
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_device',
         field_name='pk',
         label=_('Device (ID)'),
@@ -1389,7 +1400,7 @@ class ServiceFilterSet(ServicePortMappingFilterMixin, ContactModelFilterSet, Pri
         field_name='name',
         label=_('Virtual machine (name)'),
     )
-    virtual_machine_id = MultiValueNumberFilter(
+    virtual_machine_id = MultiValueBigNumberFilter(
         method='filter_virtual_machine',
         field_name='pk',
         label=_('Virtual machine (ID)'),
@@ -1399,7 +1410,7 @@ class ServiceFilterSet(ServicePortMappingFilterMixin, ContactModelFilterSet, Pri
         field_name='name',
         label=_('FHRP Group (name)'),
     )
-    fhrpgroup_id = MultiValueNumberFilter(
+    fhrpgroup_id = MultiValueBigNumberFilter(
         method='filter_fhrp_group',
         field_name='pk',
         label=_('FHRP Group (ID)'),

+ 3 - 1
netbox/netbox/api/serializers/bulk.py

@@ -1,6 +1,7 @@
 import copy
 import functools
 
+from django.db import models
 from django.utils.translation import gettext_lazy as _
 from rest_framework import serializers
 
@@ -17,7 +18,7 @@ __all__ = (
 
 
 class BulkOperationSerializer(ChangeLogMessageSerializer):
-    id = serializers.IntegerField()
+    id = serializers.IntegerField(max_value=models.BigIntegerField.MAX_BIGINT)
 
 
 # The two serializers below are schema-only: they are never used to validate or render data. The
@@ -31,6 +32,7 @@ class BulkOperationEntryErrorSerializer(serializers.Serializer):
     """
     id = serializers.IntegerField(
         required=False,
+        max_value=models.BigIntegerField.MAX_BIGINT,
         help_text=_(
             "The ID of the object which failed. Present once the entry has been matched to an "
             "object; mutually exclusive with `index`."

+ 2 - 1
netbox/netbox/api/serializers/generic.py

@@ -1,4 +1,5 @@
 from django.contrib.contenttypes.models import ContentType
+from django.db import models
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 
@@ -19,7 +20,7 @@ class GenericObjectSerializer(serializers.Serializer):
     object_type = ContentTypeField(
         queryset=ContentType.objects.all()
     )
-    object_id = serializers.IntegerField()
+    object_id = serializers.IntegerField(max_value=models.BigIntegerField.MAX_BIGINT)
     object = serializers.SerializerMethodField(read_only=True)
 
     def __init__(self, *args, **kwargs):

+ 3 - 0
netbox/netbox/filtersets.py

@@ -64,6 +64,9 @@ class BaseFilterSet(django_filters.FilterSet):
         models.AutoField: {
             'filter_class': filters.MultiValueNumberFilter
         },
+        models.BigIntegerField: {
+            'filter_class': filters.MultiValueBigNumberFilter
+        },
         models.CharField: {
             'filter_class': filters.MultiValueCharFilter
         },

+ 4 - 0
netbox/netbox/settings.py

@@ -846,6 +846,10 @@ SPECTACULAR_SETTINGS = {
     'SWAGGER_UI_DIST': 'SIDECAR',
     'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
     'POSTPROCESSING_HOOKS': [],
+    # Integer path parameters (e.g. <int:pk>) carry 64-bit object IDs
+    'PATH_CONVERTER_OVERRIDES': {
+        'int': {'type': 'integer', 'format': 'int64'},
+    },
 }
 
 #

+ 0 - 1
netbox/utilities/filters.py

@@ -162,7 +162,6 @@ class MultiValueWWNFilter(InvalidValueFilterMixin, MultiValueCharFilter):
     pass
 
 
-@extend_schema_field(OpenApiTypes.STR)
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]

+ 3 - 3
netbox/vpn/filtersets.py

@@ -7,7 +7,7 @@ from dcim.models import Device, Interface
 from ipam.models import VLAN, IPAddress, RouteTarget
 from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
-from utilities.filters import MultiValueCharFilter, MultiValueContentTypeFilter, MultiValueNumberFilter
+from utilities.filters import MultiValueBigNumberFilter, MultiValueCharFilter, MultiValueContentTypeFilter
 from utilities.filtersets import register_filterset
 from virtualization.models import VirtualMachine, VMInterface
 
@@ -401,7 +401,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='slug',
         label=_('Region (slug)'),
     )
-    region_id = MultiValueNumberFilter(
+    region_id = MultiValueBigNumberFilter(
         method='filter_region',
         field_name='pk',
         label=_('Region (ID)'),
@@ -411,7 +411,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='slug',
         label=_('Site (slug)'),
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_site',
         field_name='pk',
         label=_('Site (ID)'),