Fixes #20290: Prevent ObjectType queries when table doesn't exist

In v4.4.0, ObjectType became a concrete model with a database table. Signal
handlers query this table during migrations, but the table doesn't exist yet
during 3.7.x→4.4.0 upgrades.

The query failures poison the transaction and abort migrations.

Added objecttype_table_exists() helper and three table existence checks:
- has_feature() in netbox/models/features.py
- update_object_types() in core/signals.py
- Search backend cache() in search/backends.py

If core_objecttype table doesn't exist, operations return early instead of
querying. Once table exists, normal operation resumes.
This commit is contained in:
Jason Novinger 2025-10-02 10:02:10 -05:00
parent f23eb53312
commit 52d990463f
4 changed files with 28 additions and 0 deletions

View File

@ -13,6 +13,7 @@ from django_prometheus.models import model_deletes, model_inserts, model_updates
from core.choices import JobStatusChoices, ObjectChangeActionChoices from core.choices import JobStatusChoices, ObjectChangeActionChoices
from core.events import * from core.events import *
from core.models import ObjectType from core.models import ObjectType
from utilities.object_types import objecttype_table_exists
from extras.events import enqueue_event from extras.events import enqueue_event
from extras.models import Tag from extras.models import Tag
from extras.utils import run_validators from extras.utils import run_validators
@ -51,6 +52,10 @@ def update_object_types(sender, **kwargs):
""" """
Create or update the corresponding ObjectType for each model within the migrated app. Create or update the corresponding ObjectType for each model within the migrated app.
""" """
# Skip ObjectType operations if the table doesn't exist yet (during migrations)
if not objecttype_table_exists():
return
for model in sender.get_models(): for model in sender.get_models():
app_label, model_name = model._meta.label_lower.split('.') app_label, model_name = model._meta.label_lower.split('.')

View File

@ -24,6 +24,7 @@ from netbox.registry import registry
from netbox.signals import post_clean from netbox.signals import post_clean
from netbox.utils import register_model_feature from netbox.utils import register_model_feature
from utilities.json import CustomFieldJSONEncoder from utilities.json import CustomFieldJSONEncoder
from utilities.object_types import objecttype_table_exists
from utilities.serialization import serialize_object from utilities.serialization import serialize_object
__all__ = ( __all__ = (
@ -670,6 +671,10 @@ def has_feature(model_or_ct, feature):
""" """
Returns True if the model supports the specified feature. Returns True if the model supports the specified feature.
""" """
# Check if ObjectType table exists before attempting queries
if not objecttype_table_exists():
return False
# If an ObjectType was passed, we can use it directly # If an ObjectType was passed, we can use it directly
if type(model_or_ct) is ObjectType: if type(model_or_ct) is ObjectType:
ot = model_or_ct ot = model_or_ct

View File

@ -15,6 +15,7 @@ from netaddr.core import AddrFormatError
from core.models import ObjectType from core.models import ObjectType
from extras.models import CachedValue, CustomField from extras.models import CachedValue, CustomField
from netbox.registry import registry from netbox.registry import registry
from utilities.object_types import objecttype_table_exists
from utilities.object_types import object_type_identifier from utilities.object_types import object_type_identifier
from utilities.querysets import RestrictedPrefetch from utilities.querysets import RestrictedPrefetch
from utilities.string import title from utilities.string import title
@ -209,6 +210,10 @@ class CachedValueSearchBackend(SearchBackend):
break break
# Prefetch any associated custom fields # Prefetch any associated custom fields
# Skip if ObjectType table doesn't exist yet (during migrations)
if not objecttype_table_exists():
return
object_type = ObjectType.objects.get_for_model(indexer.model) object_type = ObjectType.objects.get_for_model(indexer.model)
custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0) custom_fields = CustomField.objects.filter(object_types=object_type).exclude(search_weight=0)

View File

@ -1,8 +1,11 @@
from django.db import connection
from .string import title from .string import title
__all__ = ( __all__ = (
'object_type_identifier', 'object_type_identifier',
'object_type_name', 'object_type_name',
'objecttype_table_exists',
) )
@ -27,3 +30,13 @@ def object_type_name(object_type, include_app=True):
except AttributeError: except AttributeError:
# Model does not exist # Model does not exist
return f'{object_type.app_label} > {object_type.model}' return f'{object_type.app_label} > {object_type.model}'
def objecttype_table_exists():
"""
Check if the core_objecttype table exists.
Returns True if the table exists, False otherwise.
Used to prevent ObjectType queries during migrations when the table doesn't exist yet.
"""
return 'core_objecttype' in connection.introspection.table_names()