Add denormalized relations

This commit is contained in:
Jeremy Stretch 2024-10-07 16:22:06 -04:00
parent f28f1646b0
commit f34818df71
4 changed files with 130 additions and 7 deletions

View File

@ -1,5 +1,7 @@
from django.apps import AppConfig from django.apps import AppConfig
from netbox import denormalized
class IPAMConfig(AppConfig): class IPAMConfig(AppConfig):
name = "ipam" name = "ipam"
@ -8,6 +10,16 @@ class IPAMConfig(AppConfig):
def ready(self): def ready(self):
from netbox.models.features import register_models from netbox.models.features import register_models
from . import signals, search # noqa: F401 from . import signals, search # noqa: F401
from .models import Prefix
# Register models # Register models
register_models(*self.get_models()) register_models(*self.get_models())
# Register denormalized fields
denormalized.register(Prefix, '_site', {
'_region': 'region',
'_sitegroup': 'group',
})
denormalized.register(Prefix, '_location', {
'_site': 'site',
})

View File

@ -23,7 +23,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
# Add the scope GenericForeignKey # Add the `scope` GenericForeignKey
migrations.AddField( migrations.AddField(
model_name='prefix', model_name='prefix',
name='scope_id', name='scope_id',
@ -46,10 +46,4 @@ class Migration(migrations.Migration):
code=copy_site_assignments, code=copy_site_assignments,
reverse_code=migrations.RunPython.noop reverse_code=migrations.RunPython.noop
), ),
# Delete the site ForeignKey
migrations.RemoveField(
model_name='prefix',
name='site',
),
] ]

View File

@ -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',
),
]

View File

@ -1,4 +1,5 @@
import netaddr import netaddr
from django.apps import apps
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
@ -270,6 +271,32 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
help_text=_("Treat as fully utilized") 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 # Cached depth & child counts
_depth = models.PositiveSmallIntegerField( _depth = models.PositiveSmallIntegerField(
default=0, default=0,
@ -331,8 +358,37 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
# Clear host bits from prefix # Clear host bits from prefix
self.prefix = self.prefix.cidr self.prefix = self.prefix.cidr
# Cache objects associated with the terminating object (for filtering)
self.cache_related_objects()
super().save(*args, **kwargs) 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 @property
def family(self): def family(self):
return self.prefix.version if self.prefix else None return self.prefix.version if self.prefix else None