diff --git a/netbox/core/migrations/0019_configrevision_active.py b/netbox/core/migrations/0019_configrevision_active.py new file mode 100644 index 00000000000..b911aaa53c5 --- /dev/null +++ b/netbox/core/migrations/0019_configrevision_active.py @@ -0,0 +1,48 @@ +# Generated by Django 5.2.5 on 2025-09-09 16:48 + +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 and get the latest version from cache + try: + version = cache.get('config_version') + except Exception: + pass + + # 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() + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_concrete_objecttype'), + ] + + operations = [ + migrations.AddField( + model_name='configrevision', + 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(('active', True)), fields=('active',), name='unique_active_config_revision' + ), + ), + ] diff --git a/netbox/core/models/config.py b/netbox/core/models/config.py index b2381ae401d..c9952153640 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(active=True), + name='unique_active_config_revision', + ) + ] def __str__(self): if not self.pk: @@ -59,8 +69,13 @@ 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 + ConfigRevision.objects.all().update(active=False) + ConfigRevision.objects.filter(pk=self.pk).update(active=True) + activate.alters_data = True @property def is_active(self): - return cache.get('config_version') == self.pk + return self.active diff --git a/netbox/netbox/config/__init__.py b/netbox/netbox/config/__init__.py index 23108f1d28c..f2fdecb3384 100644 --- a/netbox/netbox/config/__init__.py +++ b/netbox/netbox/config/__init__.py @@ -78,11 +78,16 @@ 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.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)")