From 663f9a223dd04e272ebd8b0f44c6fcc997be36ed Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 1 Sep 2025 10:30:57 -0500 Subject: [PATCH 1/8] Fixes: #19825 - Prevent cache for config revisions from being overwritten when in debug mode when not intended --- netbox/core/apps.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/netbox/core/apps.py b/netbox/core/apps.py index c081fb06449..aeb41bfe3cc 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -42,6 +42,13 @@ def ready(self): # Clear Redis cache on startup in development mode if settings.DEBUG: try: + config = cache.get('config') + config_version = cache.get('config_version') cache.clear() + if config_version: + # Activate the current config revision + # Do not query DB due to apps still initializing + cache.set('config', config, None) + cache.set('config_version', config_version, None) except Exception: pass From 9854c6619445087cd2cd87cb5b86d3512a4672a8 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 9 Sep 2025 09:42:23 -0500 Subject: [PATCH 2/8] Set ordering on the ConfigRevision to ensure consistency and usage of latest config. --- netbox/netbox/config/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index 23108f1d28c..0e6ee5b2d67 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -78,7 +78,8 @@ def _populate_from_db(self): from core.models import ConfigRevision try: - revision = ConfigRevision.objects.last() + # Enforce the creation date as the ordering parameter + revision = ConfigRevision.objects.order_by('-created').first() if revision is None: logger.debug("No previous configuration found in database; proceeding with default values") return From 42857b15b270974c1f4046797c0bf6047b489204 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Tue, 9 Sep 2025 11:49:13 -0500 Subject: [PATCH 3/8] Add active field on ConfigRevision model. --- .../migrations/0016_configrevision_active.py | 24 +++++++++++++++++++ netbox/core/models/config.py | 15 ++++++++++++ netbox/netbox/config/__init__.py | 3 ++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 netbox/core/migrations/0016_configrevision_active.py diff --git a/netbox/core/migrations/0016_configrevision_active.py b/netbox/core/migrations/0016_configrevision_active.py new file mode 100644 index 00000000000..12907289824 --- /dev/null +++ b/netbox/core/migrations/0016_configrevision_active.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.5 on 2025-09-09 16:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0015_remove_redundant_indexes'), + ] + + operations = [ + migrations.AddField( + model_name='configrevision', + name='active', + field=models.BooleanField(default=False), + ), + migrations.AddConstraint( + model_name='configrevision', + constraint=models.UniqueConstraint( + condition=models.Q(('actvive', True)), fields=('active',), name='unique_active_config_revision' + ), + ), + ] diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index b2381ae401d..6a0f922aa56 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -14,6 +14,9 @@ class ConfigRevision(models.Model): """ An atomic revision of NetBox's configuration. """ + active = models.BooleanField( + default=False + ) created = models.DateTimeField( verbose_name=_('created'), auto_now_add=True @@ -35,6 +38,13 @@ class Meta: ordering = ['-created'] verbose_name = _('config revision') verbose_name_plural = _('config revisions') + constraints = [ + models.UniqueConstraint( + fields=('active',), + condition=models.Q(actvive=True), + name='unique_active_config_revision', + ) + ] def __str__(self): if not self.pk: @@ -59,6 +69,11 @@ def activate(self): """ cache.set('config', self.data, None) cache.set('config_version', self.pk, None) + + # Set all instances of ConfigRevision to false and set this instance to true + self.objects.all().update(active=True) + self.objects.get(pk=self.pk).update(active=True) + activate.alters_data = True @property diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index 0e6ee5b2d67..0fd416ddcea 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -79,7 +79,8 @@ def _populate_from_db(self): try: # Enforce the creation date as the ordering parameter - revision = ConfigRevision.objects.order_by('-created').first() + if not (revision := ConfigRevision.objects.filter(active=True).first()): + revision = ConfigRevision.objects.order_by('-created').first() if revision is None: logger.debug("No previous configuration found in database; proceeding with default values") return From dfff41bea4f1b54a8c110470501d90789700e076 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 14 Sep 2025 11:11:15 -0500 Subject: [PATCH 4/8] Update migration and arrange for activation changes --- .../migrations/0016_configrevision_active.py | 22 +++++++++++++++++++ netbox/core/models/config.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/netbox/core/migrations/0016_configrevision_active.py b/netbox/core/migrations/0016_configrevision_active.py index 12907289824..a97bae761cc 100644 --- a/netbox/core/migrations/0016_configrevision_active.py +++ b/netbox/core/migrations/0016_configrevision_active.py @@ -3,6 +3,27 @@ from django.db import migrations, models +def get_active(apps, schema_editor): + from django.core.cache import cache + ConfigRevision = apps.get_model('core', 'ConfigRevision') + version = None + revision = None + + try: + version = cache.get('config_version') + except Exception: + pass + + if version: + revision = ConfigRevision.objects.filter(pk=version).first() + else: + revision = ConfigRevision.objects.order_by('-created').first() + + if revision: + revision.active = True + revision.save() + + class Migration(migrations.Migration): dependencies = [ @@ -15,6 +36,7 @@ class Migration(migrations.Migration): name='active', field=models.BooleanField(default=False), ), + migrations.RunPython(code=get_active, reverse_code=migrations.RunPython.noop), migrations.AddConstraint( model_name='configrevision', constraint=models.UniqueConstraint( diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index 6a0f922aa56..4a779e7673b 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -78,4 +78,4 @@ def activate(self): @property def is_active(self): - return cache.get('config_version') == self.pk + return self.active From 00741998450b5894c0980332dbcfe7c08b162f48 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 14 Sep 2025 11:11:15 -0500 Subject: [PATCH 5/8] Update migration and arrange for activation changes --- .../migrations/0016_configrevision_active.py | 24 ++++++++++++++++++- netbox/core/models/config.py | 4 ++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/netbox/core/migrations/0016_configrevision_active.py b/netbox/core/migrations/0016_configrevision_active.py index 12907289824..af04202ed41 100644 --- a/netbox/core/migrations/0016_configrevision_active.py +++ b/netbox/core/migrations/0016_configrevision_active.py @@ -3,6 +3,27 @@ from django.db import migrations, models +def get_active(apps, schema_editor): + from django.core.cache import cache + ConfigRevision = apps.get_model('core', 'ConfigRevision') + version = None + revision = None + + try: + version = cache.get('config_version') + except Exception: + pass + + if version: + revision = ConfigRevision.objects.filter(pk=version).first() + else: + revision = ConfigRevision.objects.order_by('-created').first() + + if revision: + revision.active = True + revision.save() + + class Migration(migrations.Migration): dependencies = [ @@ -15,10 +36,11 @@ class Migration(migrations.Migration): name='active', field=models.BooleanField(default=False), ), + migrations.RunPython(code=get_active, reverse_code=migrations.RunPython.noop), migrations.AddConstraint( model_name='configrevision', constraint=models.UniqueConstraint( - condition=models.Q(('actvive', True)), fields=('active',), name='unique_active_config_revision' + condition=models.Q(('active', True)), fields=('active',), name='unique_active_config_revision' ), ), ] diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index 6a0f922aa56..d97c6e53c5e 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -41,7 +41,7 @@ class Meta: constraints = [ models.UniqueConstraint( fields=('active',), - condition=models.Q(actvive=True), + condition=models.Q(active=True), name='unique_active_config_revision', ) ] @@ -78,4 +78,4 @@ def activate(self): @property def is_active(self): - return cache.get('config_version') == self.pk + return self.active From 72845c9a6867d3f083535f97ccb7e85162b16543 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 14 Sep 2025 12:07:57 -0500 Subject: [PATCH 6/8] Renumber mgiration --- ...configrevision_active.py => 0019_configrevision_active.py} | 2 +- netbox/core/models/config.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename netbox/core/migrations/{0016_configrevision_active.py => 0019_configrevision_active.py} (95%) diff --git a/netbox/core/migrations/0016_configrevision_active.py b/netbox/core/migrations/0019_configrevision_active.py similarity index 95% rename from netbox/core/migrations/0016_configrevision_active.py rename to netbox/core/migrations/0019_configrevision_active.py index af04202ed41..5bf25ed5c2d 100644 --- a/netbox/core/migrations/0016_configrevision_active.py +++ b/netbox/core/migrations/0019_configrevision_active.py @@ -27,7 +27,7 @@ def get_active(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('core', '0015_remove_redundant_indexes'), + ('core', '0018_concrete_objecttype'), ] operations = [ diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index d97c6e53c5e..2559f7ecac5 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -71,8 +71,8 @@ def activate(self): cache.set('config_version', self.pk, None) # Set all instances of ConfigRevision to false and set this instance to true - self.objects.all().update(active=True) - self.objects.get(pk=self.pk).update(active=True) + ConfigRevision.objects.all().update(active=True) + ConfigRevision.objects.get(pk=self.pk).update(active=True) activate.alters_data = True From 3fb8e8f2542c6aac3b71cba53c3b2c88a9a09a58 Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Sun, 14 Sep 2025 12:49:59 -0500 Subject: [PATCH 7/8] Correct error with activate --- netbox/core/models/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index 2559f7ecac5..c9952153640 100644 --- a/netbox/core/models/config.py +++ b/netbox/core/models/config.py @@ -71,8 +71,8 @@ def activate(self): cache.set('config_version', self.pk, None) # Set all instances of ConfigRevision to false and set this instance to true - ConfigRevision.objects.all().update(active=True) - ConfigRevision.objects.get(pk=self.pk).update(active=True) + ConfigRevision.objects.all().update(active=False) + ConfigRevision.objects.filter(pk=self.pk).update(active=True) activate.alters_data = True From 2b738f4129e39f4d097b3caa2086af345c5397dc Mon Sep 17 00:00:00 2001 From: Daniel Sheppard Date: Mon, 15 Sep 2025 16:31:04 -0500 Subject: [PATCH 8/8] Revise config initialization, remove unneeded cache setting, update migration logic and add comments * Revise config initialization to be more explicit * Remove old gating of cache.clear() * Add comments to migration * Clean up migration --- netbox/core/apps.py | 7 ------- netbox/core/migrations/0019_configrevision_active.py | 8 +++++--- netbox/netbox/config/__init__.py | 9 ++++++--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/netbox/core/apps.py b/netbox/core/apps.py index aeb41bfe3cc..c081fb06449 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -42,13 +42,6 @@ def ready(self): # Clear Redis cache on startup in development mode if settings.DEBUG: try: - config = cache.get('config') - config_version = cache.get('config_version') cache.clear() - if config_version: - # Activate the current config revision - # Do not query DB due to apps still initializing - cache.set('config', config, None) - cache.set('config_version', config_version, None) except Exception: pass diff --git a/netbox/core/migrations/0019_configrevision_active.py b/netbox/core/migrations/0019_configrevision_active.py index 5bf25ed5c2d..b911aaa53c5 100644 --- a/netbox/core/migrations/0019_configrevision_active.py +++ b/netbox/core/migrations/0019_configrevision_active.py @@ -9,16 +9,18 @@ def get_active(apps, schema_editor): version = None revision = None + # Try and get the latest version from cache try: version = cache.get('config_version') except Exception: pass - if version: - revision = ConfigRevision.objects.filter(pk=version).first() - else: + # If there is a version in cache, attempt to set revision to the current version from cache + # If the version in cache does not exist or there is no version, try the lastest revision in the database + if not version or (version and not (revision := ConfigRevision.objects.filter(pk=version).first())): revision = ConfigRevision.objects.order_by('-created').first() + # If there is a revision set, set the active revision if revision: revision.active = True revision.save() diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index 0fd416ddcea..f2fdecb3384 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -79,12 +79,15 @@ def _populate_from_db(self): try: # Enforce the creation date as the ordering parameter - if not (revision := ConfigRevision.objects.filter(active=True).first()): - revision = ConfigRevision.objects.order_by('-created').first() + revision = ConfigRevision.objects.get(active=True) + logger.debug(f"Loaded active configuration revision #{revision.pk}") + except (ConfigRevision.DoesNotExist, ConfigRevision.MultipleObjectsReturned): + logger.warning("No active configuration revision found - falling back to most recent") + revision = ConfigRevision.objects.order_by('-created').first() if revision is None: logger.debug("No previous configuration found in database; proceeding with default values") return - logger.debug("Loaded configuration data from database") + logger.debug(f"Using fallback configuration revision #{revision.pk}") except DatabaseError: # The database may not be available yet (e.g. when running a management command) logger.warning("Skipping config initialization (database unavailable)")