From f34818df713e52767c73ccc44afd9a4d0f14dfee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 7 Oct 2024 16:22:06 -0400 Subject: [PATCH] Add denormalized relations --- netbox/ipam/apps.py | 12 ++++ netbox/ipam/migrations/0071_prefix_scope.py | 8 +-- .../0072_prefix_cached_relations.py | 61 +++++++++++++++++++ netbox/ipam/models/ip.py | 56 +++++++++++++++++ 4 files changed, 130 insertions(+), 7 deletions(-) create mode 100644 netbox/ipam/migrations/0072_prefix_cached_relations.py diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py index c118d5464..e0463dfce 100644 --- a/netbox/ipam/apps.py +++ b/netbox/ipam/apps.py @@ -1,5 +1,7 @@ from django.apps import AppConfig +from netbox import denormalized + class IPAMConfig(AppConfig): name = "ipam" @@ -8,6 +10,16 @@ class IPAMConfig(AppConfig): def ready(self): from netbox.models.features import register_models from . import signals, search # noqa: F401 + from .models import Prefix # Register models register_models(*self.get_models()) + + # Register denormalized fields + denormalized.register(Prefix, '_site', { + '_region': 'region', + '_sitegroup': 'group', + }) + denormalized.register(Prefix, '_location', { + '_site': 'site', + }) diff --git a/netbox/ipam/migrations/0071_prefix_scope.py b/netbox/ipam/migrations/0071_prefix_scope.py index e6516d8ed..2ad66a11d 100644 --- a/netbox/ipam/migrations/0071_prefix_scope.py +++ b/netbox/ipam/migrations/0071_prefix_scope.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ] operations = [ - # Add the scope GenericForeignKey + # Add the `scope` GenericForeignKey migrations.AddField( model_name='prefix', name='scope_id', @@ -46,10 +46,4 @@ class Migration(migrations.Migration): code=copy_site_assignments, reverse_code=migrations.RunPython.noop ), - - # Delete the site ForeignKey - migrations.RemoveField( - model_name='prefix', - name='site', - ), ] diff --git a/netbox/ipam/migrations/0072_prefix_cached_relations.py b/netbox/ipam/migrations/0072_prefix_cached_relations.py new file mode 100644 index 000000000..c97630221 --- /dev/null +++ b/netbox/ipam/migrations/0072_prefix_cached_relations.py @@ -0,0 +1,61 @@ +import django.db.models.deletion +from django.db import migrations, models + + +def populate_denormalized_fields(apps, schema_editor): + """ + Copy site ForeignKey values to the scope GFK. + """ + Prefix = apps.get_model('ipam', 'Prefix') + + prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site') + for prefix in prefixes: + prefix._region_id = prefix.site.region_id + prefix._sitegroup_id = prefix.site.group_id + prefix._site_id = prefix.site_id + # Note: Location cannot be set prior to migration + + Prefix.objects.bulk_update(prefixes, ['_region', '_sitegroup', '_site']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0192_poweroutlet_color'), + ('ipam', '0071_prefix_scope'), + ] + + operations = [ + migrations.AddField( + model_name='prefix', + name='_location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'), + ), + migrations.AddField( + model_name='prefix', + name='_region', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'), + ), + migrations.AddField( + model_name='prefix', + name='_site', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'), + ), + migrations.AddField( + model_name='prefix', + name='_sitegroup', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'), + ), + + # Populate denormalized FK values + migrations.RunPython( + code=populate_denormalized_fields, + reverse_code=migrations.RunPython.noop + ), + + # Delete the site ForeignKey + migrations.RemoveField( + model_name='prefix', + name='site', + ), + ] diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 303706b6f..e3b31825b 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -1,4 +1,5 @@ import netaddr +from django.apps import apps from django.contrib.contenttypes.fields import GenericForeignKey from django.core.exceptions import ValidationError from django.db import models @@ -270,6 +271,32 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel): help_text=_("Treat as fully utilized") ) + # Cached associations to enable efficient filtering + _location = models.ForeignKey( + to='dcim.Location', + on_delete=models.CASCADE, + blank=True, + null=True + ) + _site = models.ForeignKey( + to='dcim.Site', + on_delete=models.CASCADE, + blank=True, + null=True + ) + _region = models.ForeignKey( + to='dcim.Region', + on_delete=models.CASCADE, + blank=True, + null=True + ) + _sitegroup = models.ForeignKey( + to='dcim.SiteGroup', + on_delete=models.CASCADE, + blank=True, + null=True + ) + # Cached depth & child counts _depth = models.PositiveSmallIntegerField( default=0, @@ -331,8 +358,37 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel): # Clear host bits from prefix self.prefix = self.prefix.cidr + # Cache objects associated with the terminating object (for filtering) + self.cache_related_objects() + super().save(*args, **kwargs) + def cache_related_objects(self): + if self.scope is None: + return + scope_type = self.scope_type.model_class() + if scope_type == apps.get_model('dcim', 'region'): + self._region = self.scope + self._sitegroup = None + self._site = None + self._location = None + elif scope_type == apps.get_model('dcim', 'sitegroup'): + self._region = None + self._sitegroup = self.scope + self._site = None + self._location = None + elif scope_type == apps.get_model('dcim', 'site'): + self._region = self.scope.region + self._sitegroup = self.scope.group + self._site = self.scope + self._location = None + elif scope_type == apps.get_model('dcim', 'location'): + self._region = self.scope.site.region + self._sitegroup = self.scope.site.group + self._site = self.scope.site + self._location = self.scope + cache_related_objects.alters_data = True + @property def family(self): return self.prefix.version if self.prefix else None