Skip to content

Commit b8cc2d7

Browse files
authored
Fixes #18887: Allows VMInterface object custom field on Prefix (#18945)
1 parent d332a0c commit b8cc2d7

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

netbox/virtualization/api/serializers_/virtualmachines.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -112,15 +112,32 @@ class Meta:
112112
brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
113113

114114
def validate(self, data):
115-
116115
# Validate many-to-many VLAN assignments
117-
virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
118-
for vlan in data.get('tagged_vlans', []):
119-
if vlan.site not in [virtual_machine.site, None]:
120-
raise serializers.ValidationError({
121-
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "
122-
f"machine, or it must be global."
123-
})
116+
virtual_machine = None
117+
tagged_vlans = []
118+
119+
# #18887
120+
# There seem to be multiple code paths coming through here. Previously, we might either get
121+
# the VirtualMachine instance from self.instance or from incoming data. However, #18887
122+
# illustrated that this is also being called when a custom field pointing to an object_type
123+
# of VMInterface is on the right side of a custom-field assignment coming in from an API
124+
# request. As such, we need to check a third way to access the VirtualMachine
125+
# instance--where `data` is the VMInterface instance itself and we can get the associated
126+
# VirtualMachine via attribute access.
127+
if isinstance(data, dict):
128+
virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
129+
tagged_vlans = data.get('tagged_vlans', [])
130+
elif isinstance(data, VMInterface):
131+
virtual_machine = data.virtual_machine
132+
tagged_vlans = data.tagged_vlans.all()
133+
134+
if virtual_machine:
135+
for vlan in tagged_vlans:
136+
if vlan.site not in [virtual_machine.site, None]:
137+
raise serializers.ValidationError({
138+
'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "
139+
f"machine, or it must be global."
140+
})
124141

125142
return super().validate(data)
126143

netbox/virtualization/tests/test_api.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
from django.test import tag
12
from django.urls import reverse
3+
from netaddr import IPNetwork
24
from rest_framework import status
35

6+
from core.models import ObjectType
47
from dcim.choices import InterfaceModeChoices
58
from dcim.models import Site
6-
from extras.models import ConfigTemplate
9+
from extras.choices import CustomFieldTypeChoices
10+
from extras.models import ConfigTemplate, CustomField
711
from ipam.choices import VLANQinQRoleChoices
8-
from ipam.models import VLAN, VRF
12+
from ipam.models import Prefix, VLAN, VRF
913
from utilities.testing import APITestCase, APIViewTestCases, create_test_device, create_test_virtualmachine
1014
from virtualization.choices import *
1115
from virtualization.models import *
@@ -350,6 +354,39 @@ def setUpTestData(cls):
350354
},
351355
]
352356

357+
@tag('regression')
358+
def test_set_vminterface_as_object_in_custom_field(self):
359+
cf = CustomField.objects.create(
360+
name='associated_interface',
361+
type=CustomFieldTypeChoices.TYPE_OBJECT,
362+
related_object_type=ObjectType.objects.get_for_model(VMInterface),
363+
required=False
364+
)
365+
cf.object_types.set([ObjectType.objects.get_for_model(Prefix)])
366+
cf.save()
367+
368+
prefix = Prefix.objects.create(prefix=IPNetwork('10.0.0.0/12'))
369+
vmi = VMInterface.objects.first()
370+
371+
url = reverse('ipam-api:prefix-detail', kwargs={'pk': prefix.pk})
372+
data = {
373+
'custom_fields': {
374+
'associated_interface': vmi.id,
375+
},
376+
}
377+
378+
self.add_permissions('ipam.change_prefix')
379+
380+
response = self.client.patch(url, data, format='json', **self.header)
381+
self.assertEqual(response.status_code, 200)
382+
383+
prefix_data = response.json()
384+
self.assertEqual(prefix_data['custom_fields']['associated_interface']['id'], vmi.id)
385+
386+
reloaded_prefix = Prefix.objects.get(pk=prefix.pk)
387+
self.assertEqual(prefix.pk, reloaded_prefix.pk)
388+
self.assertNotEqual(reloaded_prefix.cf['associated_interface'], None)
389+
353390
def test_bulk_delete_child_interfaces(self):
354391
interface1 = VMInterface.objects.get(name='Interface 1')
355392
virtual_machine = interface1.virtual_machine

0 commit comments

Comments
 (0)