Procházet zdrojové kódy

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

Jeremy Stretch před 4 dny
rodič
revize
3bc04b56ae

+ 4 - 4
netbox/circuits/filtersets.py

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

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

@@ -4,6 +4,7 @@ import typing
 from collections import OrderedDict
 from collections import OrderedDict
 
 
 from django.core.exceptions import ImproperlyConfigured
 from django.core.exceptions import ImproperlyConfigured
+from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 from drf_spectacular.contrib.django_filters import DjangoFilterExtension
 from drf_spectacular.contrib.django_filters import DjangoFilterExtension
 from drf_spectacular.extensions import OpenApiSerializerExtension, OpenApiSerializerFieldExtension, _SchemaType
 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.types import OpenApiTypes
 from drf_spectacular.utils import Direction, OpenApiParameter, OpenApiResponse
 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 rest_framework.utils import model_meta
 
 
 from netbox.api.fields import ChoiceField
 from netbox.api.fields import ChoiceField
@@ -93,6 +94,16 @@ def viewset_handles_bulk_create(view):
     return isinstance(view, NetBoxModelViewSet)
     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):
 class NetBoxAutoSchema(AutoSchema):
     """
     """
     Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
     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
         3. bulk operations don't have filter params
         4. bulk operations don't have pagination
         4. bulk operations don't have pagination
         5. bulk delete should specify input
         5. bulk delete should specify input
+        6. 64-bit integer fields (including primary keys) should be documented as int64
     """
     """
 
 
     writable_serializers = {}
     writable_serializers = {}
@@ -454,6 +466,21 @@ class NetBoxAutoSchema(AutoSchema):
             ]
             ]
         return params
         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):
     def get_filter_backends(self):
         # bulk operations don't have filter params
         # bulk operations don't have filter params
         if self.is_bulk_action:
         if self.is_bulk_action:
@@ -544,7 +571,7 @@ class FixSerializedPKRelatedField(OpenApiSerializerFieldExtension):
             serializer = self.target.serializer(nested=self.target.nested)
             serializer = self.target.serializer(nested=self.target.nested)
             component = auto_schema.resolve_serializer(serializer, direction)
             component = auto_schema.resolve_serializer(serializer, direction)
             return component.ref if component else None
             return component.ref if component else None
-        return build_basic_type(OpenApiTypes.INT)
+        return build_basic_type(OpenApiTypes.INT64)
 
 
 
 
 class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
 class FixIntegerRangeSerializerSchema(OpenApiSerializerExtension):
@@ -577,7 +604,7 @@ class FixWritableNestedSerializerAllowPK(OpenApiSerializerFieldExtension):
         if direction == 'request' and self.target.nested:
         if direction == 'request' and self.target.nested:
             return {
             return {
                 'oneOf': [
                 'oneOf': [
-                    build_basic_type(OpenApiTypes.INT),
+                    build_basic_type(OpenApiTypes.INT64),
                     schema,
                     schema,
                 ]
                 ]
             }
             }

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

@@ -336,6 +336,64 @@ class OpenAPISchemaTestCase(TestCase):
             with self.subTest(component=component, field=field):
             with self.subTest(component=component, field=field):
                 self.assertEqual(components[component]['properties'][field]['items']['type'], 'integer')
                 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):
     def test_script_run_operation_exists(self):
         """
         """
         Encodes presence of extras_scripts_run operation in schema as expected.
         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')
         schema = FixSerializedPKRelatedField(field).map_serializer_field(auto_schema, 'request')
 
 
         self.assertEqual(schema['type'], 'integer')
         self.assertEqual(schema['type'], 'integer')
+        self.assertEqual(schema.get('format'), 'int64')
         self.assertEqual(auto_schema.resolved, [])
         self.assertEqual(auto_schema.resolved, [])

+ 34 - 34
netbox/dcim/filtersets.py

@@ -31,7 +31,6 @@ from utilities.filters import (
     MultiValueCharFilter,
     MultiValueCharFilter,
     MultiValueContentTypeFilter,
     MultiValueContentTypeFilter,
     MultiValueMACAddressFilter,
     MultiValueMACAddressFilter,
-    MultiValueNumberFilter,
     MultiValueTimeZoneFilter,
     MultiValueTimeZoneFilter,
     MultiValueWWNFilter,
     MultiValueWWNFilter,
     NumericArrayFilter,
     NumericArrayFilter,
@@ -1256,7 +1255,7 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo
         label=_('Role (slug)'),
         label=_('Role (slug)'),
     )
     )
     component_type = MultiValueContentTypeFilter()
     component_type = MultiValueContentTypeFilter()
-    component_id = MultiValueNumberFilter()
+    component_id = MultiValueBigNumberFilter()
 
 
     class Meta:
     class Meta:
         model = InventoryItemTemplate
         model = InventoryItemTemplate
@@ -1365,7 +1364,7 @@ class PlatformFilterSet(NestedGroupModelFilterSet):
         model = Platform
         model = Platform
         fields = ('id', 'name', 'slug', 'description')
         fields = ('id', 'name', 'slug', 'description')
 
 
-    @extend_schema_field(OpenApiTypes.STR)
+    @extend_schema_field(OpenApiTypes.INT64)
     def get_for_device_type(self, queryset, name, value):
     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
         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',
         field_name='name',
         label=_('Device (name)'),
         label=_('Device (name)'),
     )
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_device',
         method='filter_device',
         field_name='pk',
         field_name='pk',
         label=_('Device (ID)'),
         label=_('Device (ID)'),
@@ -2251,7 +2250,7 @@ class MACAddressFilterSet(PrimaryModelFilterSet):
         field_name='name',
         field_name='name',
         label=_('Virtual machine (name)'),
         label=_('Virtual machine (name)'),
     )
     )
-    virtual_machine_id = MultiValueNumberFilter(
+    virtual_machine_id = MultiValueBigNumberFilter(
         method='filter_virtual_machine',
         method='filter_virtual_machine',
         field_name='pk',
         field_name='pk',
         label=_('Virtual machine (ID)'),
         label=_('Virtual machine (ID)'),
@@ -2398,6 +2397,7 @@ class CommonInterfaceFilterSet(django_filters.FilterSet):
         label=_('VLAN Translation Policy'),
         label=_('VLAN Translation Policy'),
     )
     )
 
 
+    @extend_schema_field(OpenApiTypes.INT64)
     def filter_vlan_id(self, queryset, name, value):
     def filter_vlan_id(self, queryset, name, value):
         return self._filter_assigned_vlan(queryset, 'id', value)
         return self._filter_assigned_vlan(queryset, 'id', value)
 
 
@@ -2430,7 +2430,7 @@ class InterfaceFilterSet(
         field_name='name',
         field_name='name',
         label=_('Virtual Chassis Interfaces for Device when device is master')
         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',
         method='filter_virtual_chassis_member_or_master',
         field_name='pk',
         field_name='pk',
         label=_('Virtual Chassis Interfaces for Device when device is master (ID)')
         label=_('Virtual Chassis Interfaces for Device when device is master (ID)')
@@ -2440,7 +2440,7 @@ class InterfaceFilterSet(
         field_name='name',
         field_name='name',
         label=_('Virtual Chassis Interfaces for Device')
         label=_('Virtual Chassis Interfaces for Device')
     )
     )
-    virtual_chassis_member_id = MultiValueNumberFilter(
+    virtual_chassis_member_id = MultiValueBigNumberFilter(
         method='filter_virtual_chassis_member',
         method='filter_virtual_chassis_member',
         field_name='pk',
         field_name='pk',
         label=_('Virtual Chassis Interfaces for Device (ID)')
         label=_('Virtual Chassis Interfaces for Device (ID)')
@@ -2719,7 +2719,7 @@ class InventoryItemFilterSet(DeviceComponentFilterSet):
         label=_('Role (slug)'),
         label=_('Role (slug)'),
     )
     )
     component_type = MultiValueContentTypeFilter()
     component_type = MultiValueContentTypeFilter()
-    component_id = MultiValueNumberFilter()
+    component_id = MultiValueBigNumberFilter()
     serial = MultiValueCharFilter(
     serial = MultiValueCharFilter(
         lookup_expr='iexact'
         lookup_expr='iexact'
     )
     )
@@ -2859,14 +2859,14 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
     termination_a_type = MultiValueContentTypeFilter(
     termination_a_type = MultiValueContentTypeFilter(
         field_name='terminations__termination_type'
         field_name='terminations__termination_type'
     )
     )
-    termination_a_id = MultiValueNumberFilter(
+    termination_a_id = MultiValueBigNumberFilter(
         method='filter_by_cable_end_a',
         method='filter_by_cable_end_a',
         field_name='terminations__termination_id'
         field_name='terminations__termination_id'
     )
     )
     termination_b_type = MultiValueContentTypeFilter(
     termination_b_type = MultiValueContentTypeFilter(
         field_name='terminations__termination_type'
         field_name='terminations__termination_type'
     )
     )
-    termination_b_id = MultiValueNumberFilter(
+    termination_b_id = MultiValueBigNumberFilter(
         method='filter_by_cable_end_b',
         method='filter_by_cable_end_b',
         field_name='terminations__termination_id'
         field_name='terminations__termination_id'
     )
     )
@@ -2900,14 +2900,14 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         choices=ColorChoices,
         choices=ColorChoices,
         distinct=False,
         distinct=False,
     )
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_by_termination'
         method='filter_by_termination'
     )
     )
     device = MultiValueCharFilter(
     device = MultiValueCharFilter(
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='device__name'
         field_name='device__name'
     )
     )
-    rack_id = MultiValueNumberFilter(
+    rack_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='rack_id'
         field_name='rack_id'
     )
     )
@@ -2915,7 +2915,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='rack__name'
         field_name='rack__name'
     )
     )
-    location_id = MultiValueNumberFilter(
+    location_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='location_id'
         field_name='location_id'
     )
     )
@@ -2923,7 +2923,7 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='location__name'
         field_name='location__name'
     )
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_by_termination',
         method='filter_by_termination',
         field_name='site_id'
         field_name='site_id'
     )
     )
@@ -2933,31 +2933,31 @@ class CableFilterSet(TenancyFilterSet, PrimaryModelFilterSet):
     )
     )
 
 
     # Termination object filters
     # Termination object filters
-    consoleport_id = MultiValueNumberFilter(
+    consoleport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleport'
         method='filter_by_consoleport'
     )
     )
-    consoleserverport_id = MultiValueNumberFilter(
+    consoleserverport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleserverport'
         method='filter_by_consoleserverport'
     )
     )
-    powerport_id = MultiValueNumberFilter(
+    powerport_id = MultiValueBigNumberFilter(
         method='filter_by_powerport'
         method='filter_by_powerport'
     )
     )
-    poweroutlet_id = MultiValueNumberFilter(
+    poweroutlet_id = MultiValueBigNumberFilter(
         method='filter_by_poweroutlet'
         method='filter_by_poweroutlet'
     )
     )
-    interface_id = MultiValueNumberFilter(
+    interface_id = MultiValueBigNumberFilter(
         method='filter_by_interface'
         method='filter_by_interface'
     )
     )
-    frontport_id = MultiValueNumberFilter(
+    frontport_id = MultiValueBigNumberFilter(
         method='filter_by_frontport'
         method='filter_by_frontport'
     )
     )
-    rearport_id = MultiValueNumberFilter(
+    rearport_id = MultiValueBigNumberFilter(
         method='filter_by_rearport'
         method='filter_by_rearport'
     )
     )
-    powerfeed_id = MultiValueNumberFilter(
+    powerfeed_id = MultiValueBigNumberFilter(
         method='filter_by_powerfeed'
         method='filter_by_powerfeed'
     )
     )
-    circuittermination_id = MultiValueNumberFilter(
+    circuittermination_id = MultiValueBigNumberFilter(
         method='filter_by_circuittermination'
         method='filter_by_circuittermination'
     )
     )
 
 
@@ -3050,31 +3050,31 @@ class CableTerminationFilterSet(ChangeLoggedModelFilterSet):
     termination_type = MultiValueContentTypeFilter()
     termination_type = MultiValueContentTypeFilter()
 
 
     # Termination object filters
     # Termination object filters
-    consoleport_id = MultiValueNumberFilter(
+    consoleport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleport'
         method='filter_by_consoleport'
     )
     )
-    consoleserverport_id = MultiValueNumberFilter(
+    consoleserverport_id = MultiValueBigNumberFilter(
         method='filter_by_consoleserverport'
         method='filter_by_consoleserverport'
     )
     )
-    powerport_id = MultiValueNumberFilter(
+    powerport_id = MultiValueBigNumberFilter(
         method='filter_by_powerport'
         method='filter_by_powerport'
     )
     )
-    poweroutlet_id = MultiValueNumberFilter(
+    poweroutlet_id = MultiValueBigNumberFilter(
         method='filter_by_poweroutlet'
         method='filter_by_poweroutlet'
     )
     )
-    interface_id = MultiValueNumberFilter(
+    interface_id = MultiValueBigNumberFilter(
         method='filter_by_interface'
         method='filter_by_interface'
     )
     )
-    frontport_id = MultiValueNumberFilter(
+    frontport_id = MultiValueBigNumberFilter(
         method='filter_by_frontport'
         method='filter_by_frontport'
     )
     )
-    rearport_id = MultiValueNumberFilter(
+    rearport_id = MultiValueBigNumberFilter(
         method='filter_by_rearport'
         method='filter_by_rearport'
     )
     )
-    powerfeed_id = MultiValueNumberFilter(
+    powerfeed_id = MultiValueBigNumberFilter(
         method='filter_by_powerfeed'
         method='filter_by_powerfeed'
     )
     )
-    circuittermination_id = MultiValueNumberFilter(
+    circuittermination_id = MultiValueBigNumberFilter(
         method='filter_by_circuittermination'
         method='filter_by_circuittermination'
     )
     )
 
 
@@ -3441,7 +3441,7 @@ class ConnectionFilterSet(BaseFilterSet):
         method='search',
         method='search',
         label=_('Search'),
         label=_('Search'),
     )
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_connections',
         method='filter_connections',
         field_name='device__site_id'
         field_name='device__site_id'
     )
     )
@@ -3449,7 +3449,7 @@ class ConnectionFilterSet(BaseFilterSet):
         method='filter_connections',
         method='filter_connections',
         field_name='device__site__slug'
         field_name='device__site__slug'
     )
     )
-    device_id = MultiValueNumberFilter(
+    device_id = MultiValueBigNumberFilter(
         method='filter_connections',
         method='filter_connections',
         field_name='device_id'
         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 django.utils.translation import gettext_lazy as _
 from drf_spectacular.extensions import OpenApiSerializerExtension
 from drf_spectacular.extensions import OpenApiSerializerExtension
 from rest_framework import serializers
 from rest_framework import serializers
@@ -49,6 +50,7 @@ class RenderConfigInputSerializer(serializers.Serializer):
     """
     """
     config_template_id = serializers.IntegerField(
     config_template_id = serializers.IntegerField(
         required=False,
         required=False,
+        max_value=models.BigIntegerField.MAX_BIGINT,
         help_text=_(
         help_text=_(
             "Optional ID of the ConfigTemplate to render. If omitted, the object's assigned "
             "Optional ID of the ConfigTemplate to render. If omitted, the object's assigned "
             "config template is used."
             "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 tenancy.models import Tenant, TenantGroup
 from users.filterset_mixins import OwnerFilterMixin
 from users.filterset_mixins import OwnerFilterMixin
 from users.models import Group, User
 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 utilities.filtersets import register_filterset
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
@@ -121,7 +126,7 @@ class EventRuleFilterSet(OwnerFilterMixin, NetBoxModelFilterSet):
         label=_('Action available'),
         label=_('Action available'),
     )
     )
     action_object_type = MultiValueContentTypeFilter()
     action_object_type = MultiValueContentTypeFilter()
-    action_object_id = MultiValueNumberFilter()
+    action_object_id = MultiValueBigNumberFilter()
 
 
     class Meta:
     class Meta:
         model = EventRule
         model = EventRule

+ 37 - 26
netbox/ipam/filtersets.py

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

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

@@ -1,6 +1,7 @@
 import copy
 import copy
 import functools
 import functools
 
 
+from django.db import models
 from django.utils.translation import gettext_lazy as _
 from django.utils.translation import gettext_lazy as _
 from rest_framework import serializers
 from rest_framework import serializers
 
 
@@ -17,7 +18,7 @@ __all__ = (
 
 
 
 
 class BulkOperationSerializer(ChangeLogMessageSerializer):
 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
 # 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(
     id = serializers.IntegerField(
         required=False,
         required=False,
+        max_value=models.BigIntegerField.MAX_BIGINT,
         help_text=_(
         help_text=_(
             "The ID of the object which failed. Present once the entry has been matched to an "
             "The ID of the object which failed. Present once the entry has been matched to an "
             "object; mutually exclusive with `index`."
             "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.contrib.contenttypes.models import ContentType
+from django.db import models
 from drf_spectacular.utils import extend_schema_field
 from drf_spectacular.utils import extend_schema_field
 from rest_framework import serializers
 from rest_framework import serializers
 
 
@@ -19,7 +20,7 @@ class GenericObjectSerializer(serializers.Serializer):
     object_type = ContentTypeField(
     object_type = ContentTypeField(
         queryset=ContentType.objects.all()
         queryset=ContentType.objects.all()
     )
     )
-    object_id = serializers.IntegerField()
+    object_id = serializers.IntegerField(max_value=models.BigIntegerField.MAX_BIGINT)
     object = serializers.SerializerMethodField(read_only=True)
     object = serializers.SerializerMethodField(read_only=True)
 
 
     def __init__(self, *args, **kwargs):
     def __init__(self, *args, **kwargs):

+ 3 - 0
netbox/netbox/filtersets.py

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

+ 4 - 0
netbox/netbox/settings.py

@@ -846,6 +846,10 @@ SPECTACULAR_SETTINGS = {
     'SWAGGER_UI_DIST': 'SIDECAR',
     'SWAGGER_UI_DIST': 'SIDECAR',
     'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
     'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
     'POSTPROCESSING_HOOKS': [],
     '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
     pass
 
 
 
 
-@extend_schema_field(OpenApiTypes.STR)
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
 class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
     """
     """
     Filters for a set of Models, including all descendant models within a Tree.  Example: [<Region: R1>,<Region: R2>]
     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 ipam.models import VLAN, IPAddress, RouteTarget
 from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
 from netbox.filtersets import NetBoxModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 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 utilities.filtersets import register_filterset
 from virtualization.models import VirtualMachine, VMInterface
 from virtualization.models import VirtualMachine, VMInterface
 
 
@@ -401,7 +401,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='slug',
         field_name='slug',
         label=_('Region (slug)'),
         label=_('Region (slug)'),
     )
     )
-    region_id = MultiValueNumberFilter(
+    region_id = MultiValueBigNumberFilter(
         method='filter_region',
         method='filter_region',
         field_name='pk',
         field_name='pk',
         label=_('Region (ID)'),
         label=_('Region (ID)'),
@@ -411,7 +411,7 @@ class L2VPNTerminationFilterSet(NetBoxModelFilterSet):
         field_name='slug',
         field_name='slug',
         label=_('Site (slug)'),
         label=_('Site (slug)'),
     )
     )
-    site_id = MultiValueNumberFilter(
+    site_id = MultiValueBigNumberFilter(
         method='filter_site',
         method='filter_site',
         field_name='pk',
         field_name='pk',
         label=_('Site (ID)'),
         label=_('Site (ID)'),