Compare commits

..

1 Commits

Author SHA1 Message Date
Martin Hauser
a320df9926 fix(misc): Handle cache unpickling failure in release check
Guard `cache.get('latest_release')` during release checks to prevent a
500 when stale cached data can't be unpickled after dependency upgrades.
On failure, log at debug level and delete the affected cache key.

Fixes #21254
2026-01-23 18:02:02 +01:00
10 changed files with 22 additions and 94 deletions

View File

@@ -9,7 +9,6 @@ from django.db import connection, models
from django.db.models import Q
from django.utils.translation import gettext as _
from netbox.context import object_types_cache
from netbox.plugins import PluginConfig
from netbox.registry import registry
from utilities.string import title
@@ -71,12 +70,6 @@ class ObjectTypeManager(models.Manager):
"""
from netbox.models.features import get_model_features, model_is_public
# Check the request cache before hitting the database
cache = object_types_cache.get()
if cache is not None:
if ot := cache.get((model._meta.model, for_concrete_model)):
return ot
# TODO: Remove this in NetBox v5.0
# If the ObjectType table has not yet been provisioned (e.g. because we're in a pre-v4.4 migration),
# fall back to ContentType.
@@ -103,10 +96,6 @@ class ObjectTypeManager(models.Manager):
features=get_model_features(model),
)[0]
# Populate the request cache to avoid redundant lookups
if cache is not None:
cache[(model._meta.model, for_concrete_model)] = ot
return ot
def get_for_models(self, *models, for_concrete_models=True):

View File

@@ -20,9 +20,7 @@ from utilities.forms.fields import (
DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField,
)
from utilities.forms.rendering import FieldSet, InlineFields, TabbedGroups
from utilities.forms.widgets import (
APISelect, ClearableFileInput, ClearableSelect, HTMXSelect, NumberWithOptions, SelectWithPK,
)
from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, NumberWithOptions, SelectWithPK
from utilities.jsonschema import JSONSchemaProperty
from virtualization.models import Cluster, VMInterface
from wireless.models import WirelessLAN, WirelessLANGroup
@@ -594,14 +592,6 @@ class DeviceForm(TenancyForm, PrimaryModelForm):
},
)
)
face = forms.ChoiceField(
label=_('Face'),
choices=add_blank_choice(DeviceFaceChoices),
required=False,
widget=ClearableSelect(
requires_fields=['rack']
)
)
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),

View File

@@ -3,10 +3,8 @@ from contextvars import ContextVar
__all__ = (
'current_request',
'events_queue',
'object_types_cache',
)
current_request = ContextVar('current_request', default=None)
events_queue = ContextVar('events_queue', default=dict())
object_types_cache = ContextVar('object_types_cache', default=None)

View File

@@ -1,6 +1,6 @@
from contextlib import contextmanager
from netbox.context import current_request, events_queue, object_types_cache
from netbox.context import current_request, events_queue
from netbox.utils import register_request_processor
from extras.events import flush_events
@@ -16,7 +16,6 @@ def event_tracking(request):
"""
current_request.set(request)
events_queue.set({})
object_types_cache.set({})
yield
@@ -27,4 +26,3 @@ def event_tracking(request):
# Clear context vars
current_request.set(None)
events_queue.set({})
object_types_cache.set(None)

View File

@@ -1,5 +1,6 @@
import re
from collections import namedtuple
import logging
from django.conf import settings
from django.contrib import messages
@@ -28,6 +29,8 @@ __all__ = (
'SearchView',
)
logger = logging.getLogger(f'netbox.{__name__}')
Link = namedtuple('Link', ('label', 'viewname', 'permission', 'count'))
@@ -50,7 +53,14 @@ class HomeView(ConditionalLoginRequiredMixin, View):
# Check whether a new release is available. (Only for superusers.)
new_release = None
if request.user.is_superuser:
latest_release = cache.get('latest_release')
# cache.get() can raise if the cached value can't be unpickled after dependency upgrades
try:
latest_release = cache.get('latest_release')
except Exception:
logger.debug("Failed to read 'latest_release' from cache; deleting key", exc_info=True)
cache.delete('latest_release')
latest_release = None
if latest_release:
release_version, release_url = latest_release
if release_version > version.parse(settings.RELEASE.version):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,40 +0,0 @@
import TomSelect from 'tom-select';
import { getElements } from '../util';
/**
* Initialize clear-field dependencies.
* When a required field is cleared, dependent fields with data-requires-fields attribute will also be cleared.
*/
export function initClearField(): void {
// Find all fields with data-requires-fields attribute
for (const field of getElements<HTMLSelectElement>('[data-requires-fields]')) {
const requiredFieldsAttr = field.getAttribute('data-requires-fields');
if (!requiredFieldsAttr) continue;
// Parse the comma-separated list of required field names
const requiredFields = requiredFieldsAttr.split(',').map(name => name.trim());
// Set up listeners for each required field
for (const requiredFieldName of requiredFields) {
const requiredField = document.querySelector<HTMLSelectElement>(
`[name="${requiredFieldName}"]`,
);
if (!requiredField) continue;
// Listen for changes on the required field
requiredField.addEventListener('change', () => {
// If required field is cleared, also clear this dependent field
if (!requiredField.value || requiredField.value === '') {
// Check if this field uses TomSelect
const tomselect = (field as HTMLSelectElement & { tomselect?: TomSelect }).tomselect;
if (tomselect) {
tomselect.clear();
} else {
// Regular select field
field.value = '';
}
}
});
}
}
}

View File

@@ -1,10 +1,9 @@
import { initClearField } from './clearField';
import { initFormElements } from './elements';
import { initFilterModifiers } from './filterModifiers';
import { initSpeedSelector } from './speedSelector';
export function initForms(): void {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers, initClearField]) {
for (const func of [initFormElements, initSpeedSelector, initFilterModifiers]) {
func();
}
}

View File

@@ -5,7 +5,6 @@ from ..utils import add_blank_choice
__all__ = (
'BulkEditNullBooleanSelect',
'ClearableSelect',
'ColorSelect',
'HTMXSelect',
'SelectWithPK',
@@ -29,21 +28,6 @@ class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
)
class ClearableSelect(forms.Select):
"""
A Select widget that will be automatically cleared when one or more required fields are cleared.
Args:
requires_fields: A list of field names that this field depends on. When any of these fields
are cleared, this field will also be cleared automatically via JavaScript.
"""
def __init__(self, *args, requires_fields=None, **kwargs):
super().__init__(*args, **kwargs)
if requires_fields:
self.attrs['data-requires-fields'] = ','.join(requires_fields)
class ColorSelect(forms.Select):
"""
Extends the built-in Select widget to colorize each <option>.