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', {
'_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
connect_counters(Device, DeviceType, ModuleType, RackType, VirtualChassis)

View File

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

View File

@@ -224,7 +224,7 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
@strawberry_django.type(
models.Device,
fields='__all__',
exclude=['_virtual_chassis_name', '_primary_ip4_address', '_primary_ip6_address'],
filters=DeviceFilter,
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 extras.models import ConfigContextModel, CustomField
from extras.querysets import ConfigContextModelQuerySet
from ipam.fields import IPAddressField
from netbox.choices import ColorChoices
from netbox.config import ConfigItem
from netbox.models import NestedGroupModel, OrganizationalModel, PrimaryModel
@@ -673,6 +674,19 @@ class 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
console_port_count = CounterCacheField(
to_model='dcim.ConsolePort',

View File

@@ -424,34 +424,40 @@ class IPAddressForm(TenancyForm, PrimaryModelForm):
def save(self, *args, **kwargs):
ipaddress = super().save(*args, **kwargs)
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
interface = self.instance.assigned_object
if type(interface) in (Interface, VMInterface):
parent = interface.parent_object
parent.snapshot()
update_fields = []
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
if self.cleaned_data['primary_for_parent']:
if ipaddress.address.version == 4:
parent.primary_ip4 = ipaddress
update_fields.append('primary_ip4')
else:
parent.primary_ip6 = ipaddress
parent.save()
update_fields.append('primary_ip6')
elif ipaddress.address.version == 4 and parent.primary_ip4 == ipaddress:
parent.primary_ip4 = None
parent.save()
update_fields.append('primary_ip4')
elif ipaddress.address.version == 6 and parent.primary_ip6 == ipaddress:
parent.primary_ip6 = None
parent.save()
update_fields.append('primary_ip6')
# Assign/clear this IPAddress as the OOB for the associated Device
if type(interface) is Interface:
parent = interface.parent_object
parent.snapshot()
if self.cleaned_data['oob_for_parent']:
parent.oob_ip = ipaddress
parent.save()
elif parent.oob_ip == ipaddress:
parent.oob_ip = None
parent.save()
# Assign/clear this IPAddress as the OOB for the associated Device
if type(interface) is Interface:
if self.cleaned_data['oob_for_parent']:
parent.oob_ip = ipaddress
update_fields.append('oob_ip')
elif parent.oob_ip == ipaddress:
parent.oob_ip = None
update_fields.append('oob_ip')
# 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