Fixes #18887: Allows VMInterface object custom field on Prefix (#18945)

This commit is contained in:
Jason Novinger 2025-03-20 15:55:12 -05:00 committed by GitHub
parent d332a0c0d7
commit b8cc2d7116
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 64 additions and 10 deletions

View File

@ -112,10 +112,27 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
def validate(self, data):
# Validate many-to-many VLAN assignments
virtual_machine = None
tagged_vlans = []
# #18887
# There seem to be multiple code paths coming through here. Previously, we might either get
# the VirtualMachine instance from self.instance or from incoming data. However, #18887
# illustrated that this is also being called when a custom field pointing to an object_type
# of VMInterface is on the right side of a custom-field assignment coming in from an API
# request. As such, we need to check a third way to access the VirtualMachine
# instance--where `data` is the VMInterface instance itself and we can get the associated
# VirtualMachine via attribute access.
if isinstance(data, dict):
virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
for vlan in data.get('tagged_vlans', []):
tagged_vlans = data.get('tagged_vlans', [])
elif isinstance(data, VMInterface):
virtual_machine = data.virtual_machine
tagged_vlans = data.tagged_vlans.all()
if virtual_machine:
for vlan in tagged_vlans:
if vlan.site not in [virtual_machine.site, None]:
raise serializers.ValidationError({
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "

View File

@ -1,11 +1,15 @@
from django.test import tag
from django.urls import reverse
from netaddr import IPNetwork
from rest_framework import status
from core.models import ObjectType
from dcim.choices import InterfaceModeChoices
from dcim.models import Site
from extras.models import ConfigTemplate
from extras.choices import CustomFieldTypeChoices
from extras.models import ConfigTemplate, CustomField
from ipam.choices import VLANQinQRoleChoices
from ipam.models import VLAN, VRF
from ipam.models import Prefix, VLAN, VRF
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine
from virtualization.choices import *
from virtualization.models import *
@ -350,6 +354,39 @@ class VMInterfaceTest(APIViewTestCases.APIViewTestCase):
},
]
@tag('regression')
def test_set_vminterface_as_object_in_custom_field(self):
cf = CustomField.objects.create(
name='associated_interface',
type=CustomFieldTypeChoices.TYPE_OBJECT,
related_object_type=ObjectType.objects.get_for_model(VMInterface),
required=False
)
cf.object_types.set([ObjectType.objects.get_for_model(Prefix)])
cf.save()
prefix = Prefix.objects.create(prefix=IPNetwork('10.0.0.0/12'))
vmi = VMInterface.objects.first()
url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
data = {
'custom_fields': {
'associated_interface': vmi.id,
},
}
self.add_permissions('ipam.change_prefix')
response = self.client.patch(url, data, format='json', **self.header)
self.assertEqual(response.status_code, 200)
prefix_data = response.json()
self.assertEqual(prefix_data['custom_fields']['associated_interface']['id'], vmi.id)
reloaded_prefix = Prefix.objects.get(pk=prefix.pk)
self.assertEqual(prefix.pk, reloaded_prefix.pk)
self.assertNotEqual(reloaded_prefix.cf['associated_interface'], None)
def test_bulk_delete_child_interfaces(self):
interface1 = VMInterface.objects.get(name='Interface 1')
virtual_machine = interface1.virtual_machine