Compare commits

...

2 Commits

Author SHA1 Message Date
Jeremy Stretch
78c56c2cb8 Update only the primary/OOB IP fields when saving the parent object
Some checks are pending
CI / build (20.x, 3.12) (push) Waiting to run
CI / build (20.x, 3.13) (push) Waiting to run
CI / build (20.x, 3.14) (push) Waiting to run
2026-01-19 11:57:35 -05:00
Jeremy Stretch
5dd5d65d74 Initial testing for #21203 2026-01-16 17:33:03 -05:00
6 changed files with 94 additions and 18 deletions

View File

@@ -29,6 +29,15 @@ class DCIMConfig(AppConfig):
denormalized.register(CableTermination, '_location', { denormalized.register(CableTermination, '_location', {
'_site': 'site', '_site': 'site',
}) })
denormalized.register(Device, 'virtual_chassis', {
'_virtual_chassis_name': 'name',
})
denormalized.register(Device, 'primary_ip4', {
'_primary_ip4_address': 'address',
})
denormalized.register(Device, 'primary_ip6', {
'_primary_ip6_address': 'address',
})
# Register counters # Register counters
connect_counters(Device, DeviceType, ModuleType, RackType, VirtualChassis) connect_counters(Device, DeviceType, ModuleType, RackType, VirtualChassis)

View File

@@ -1331,13 +1331,14 @@ class DeviceFilterSet(
return queryset return queryset
return queryset.filter( return queryset.filter(
Q(name__icontains=value) | Q(name__icontains=value) |
Q(virtual_chassis__name__icontains=value) |
Q(serial__icontains=value.strip()) | Q(serial__icontains=value.strip()) |
Q(asset_tag__icontains=value.strip()) | Q(asset_tag__icontains=value.strip()) |
Q(description__icontains=value.strip()) | Q(description__icontains=value.strip()) |
Q(comments__icontains=value) | Q(comments__icontains=value) |
Q(primary_ip4__address__startswith=value) | # Denormalized fields
Q(primary_ip6__address__startswith=value) Q(_virtual_chassis_name__icontains=value) |
Q(_primary_ip4_address__startswith=value) |
Q(_primary_ip6_address__startswith=value)
).distinct() ).distinct()
def _has_primary_ip(self, queryset, name, value): def _has_primary_ip(self, queryset, name, value):

View File

@@ -224,7 +224,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type( @strawberry_django.type(
models.Device, models.Device,
fields='__all__', exclude=['_virtual_chassis_name', '_primary_ip4_address', '_primary_ip6_address'],
filters=DeviceFilter, filters=DeviceFilter,
pagination=True pagination=True
) )

View File

@@ -0,0 +1,46 @@
from django.db import migrations, models
from django.db.models import Q
import ipam.fields
def backfill_denormalized_fields(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
# TODO: Optimize for bulk operations
for device in Device.objects.filter(
Q(virtual_chassis__isnull=False) | Q(primary_ip4__isnull=False) | Q(primary_ip6__isnull=False)
):
device._virtual_chassis_name = device.virtual_chassis.name if device.virtual_chassis else ''
device._primary_ip4_address = device.primary_ip4.address if device.primary_ip4 else None
device._primary_ip6_address = device.primary_ip6.address if device.primary_ip6 else None
device.save()
class Migration(migrations.Migration):
dependencies = [
('dcim', '0225_gfk_indexes'),
]
operations = [
migrations.AddField(
model_name='device',
name='_primary_ip4_address',
field=ipam.fields.IPAddressField(blank=True, null=True),
),
migrations.AddField(
model_name='device',
name='_primary_ip6_address',
field=ipam.fields.IPAddressField(blank=True, null=True),
),
migrations.AddField(
model_name='device',
name='_virtual_chassis_name',
field=models.CharField(blank=True),
),
migrations.RunPython(
code=backfill_denormalized_fields,
reverse_code=migrations.RunPython.noop
),
]

View File

@@ -21,6 +21,7 @@ from dcim.fields import MACAddressField
from dcim.utils import create_port_mappings, update_interface_bridges from dcim.utils import create_port_mappings, update_interface_bridges
from extras.models import ConfigContextModel, CustomField from extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet from extras.querysets import ConfigContextModelQuerySet
from ipam.fields import IPAddressField
from netbox.choices import ColorChoices from netbox.choices import ColorChoices
from netbox.config import ConfigItem from netbox.config import ConfigItem
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
@@ -673,6 +674,19 @@ class Device(
related_query_name='device', related_query_name='device',
) )
# Denormalized fields
_virtual_chassis_name = models.CharField(
blank=True,
)
_primary_ip4_address = IPAddressField(
blank=True,
null=True,
)
_primary_ip6_address = IPAddressField(
blank=True,
null=True,
)
# Counter fields # Counter fields
console_port_count = CounterCacheField( console_port_count = CounterCacheField(
to_model='dcim.ConsolePort', to_model='dcim.ConsolePort',

View File

@@ -424,34 +424,40 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
ipaddress = super().save(*args, **kwargs) ipaddress = super().save(*args, **kwargs)
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
interface = self.instance.assigned_object interface = self.instance.assigned_object
if type(interface) in (Interface, VMInterface): if type(interface) in (Interface, VMInterface):
parent = interface.parent_object parent = interface.parent_object
parent.snapshot() parent.snapshot()
update_fields = []
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
if self.cleaned_data['primary_for_parent']: if self.cleaned_data['primary_for_parent']:
if ipaddress.address.version == 4: if ipaddress.address.version == 4:
parent.primary_ip4 = ipaddress parent.primary_ip4 = ipaddress
update_fields.append('primary_ip4')
else: else:
parent.primary_ip6 = ipaddress parent.primary_ip6 = ipaddress
parent.save() update_fields.append('primary_ip6')
elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress: elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
parent.primary_ip4 = None parent.primary_ip4 = None
parent.save() update_fields.append('primary_ip4')
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress: elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
parent.primary_ip6 = None parent.primary_ip6 = None
parent.save() update_fields.append('primary_ip6')
# Assign/clear this IPAddress as the OOB for the associated Device # Assign/clear this IPAddress as the OOB for the associated Device
if type(interface) is Interface: if type(interface) is Interface:
parent = interface.parent_object if self.cleaned_data['oob_for_parent']:
parent.snapshot() parent.oob_ip = ipaddress
if self.cleaned_data['oob_for_parent']: update_fields.append('oob_ip')
parent.oob_ip = ipaddress elif parent.oob_ip == ipaddress:
parent.save() parent.oob_ip = None
elif parent.oob_ip == ipaddress: update_fields.append('oob_ip')
parent.oob_ip = None
parent.save() # Save the parent object if appropriate. Update only the relevant fields to avoid conflicts with e.g.
# denormalized data on the parent object.
if update_fields:
parent.save(update_fields=update_fields)
return ipaddress return ipaddress