Преглед изворни кода

fix(virtualization): Validate primary IPs on unsaved VMs

Use an empty interface queryset instead of None when validating primary
IP addresses on an unsaved VM. Invalid assignments now raise the expected
validation error instead of a TypeError, returning HTTP 400 rather than
HTTP 500 for API create requests.

Add model and API regression tests for IPv4 and IPv6, with model coverage
confirming valid primary IP and NAT assignments remain accepted.

Fixes #23278
Martin Hauser пре 1 дан
родитељ
комит
6d191fe556

+ 1 - 1
netbox/virtualization/models/virtualmachines.py

@@ -341,7 +341,7 @@ class VirtualMachine(
                 })
                 })
 
 
         # Validate primary IP addresses
         # Validate primary IP addresses
-        interfaces = self.interfaces.all() if self.pk else None
+        interfaces = self.interfaces.all() if self.pk else VMInterface.objects.none()
         for family in (4, 6):
         for family in (4, 6):
             field = f'primary_ip{family}'
             field = f'primary_ip{family}'
             ip = getattr(self, field)
             ip = getattr(self, field)

+ 18 - 0
netbox/virtualization/tests/test_api.py

@@ -594,6 +594,24 @@ class VirtualMachineTestCase(APIViewTestCases.APIViewTestCase):
             ('vm4.example.com', 'vm6.example.com'),
             ('vm4.example.com', 'vm6.example.com'),
         )
         )
 
 
+    @tag('regression')  # Ref: #23278
+    def test_create_with_unassigned_primary_ip(self):
+        """Creating a VM with an unassigned primary IP returns HTTP 400."""
+        ip4 = IPAddress.objects.create(address='192.0.2.10/24')
+        ip6 = IPAddress.objects.create(address='2001:db8::10/64')
+
+        self.add_permissions('virtualization.add_virtualmachine')
+        for field, ip in (('primary_ip4', ip4), ('primary_ip6', ip6)):
+            with self.subTest(field=field):
+                data = {
+                    'name': 'vm-new',
+                    'site': self.sites[0].pk,
+                    field: ip.pk,
+                }
+                response = self.client.post(self._get_list_url(), data, format='json', **self.header)
+                self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+                self.assertIn(field, response.data)
+
     def test_render_config_with_config_template_id(self):
     def test_render_config_with_config_template_id(self):
         default_template = ConfigTemplate.objects.create(
         default_template = ConfigTemplate.objects.create(
             name='Default Template',
             name='Default Template',

+ 22 - 2
netbox/virtualization/tests/test_models.py

@@ -1,11 +1,13 @@
 from decimal import Decimal
 from decimal import Decimal
 
 
 from django.core.exceptions import ValidationError
 from django.core.exceptions import ValidationError
-from django.test import TestCase
+from django.test import TestCase, tag
+from netaddr import IPNetwork
 
 
 from dcim.models import Platform, Region, Site, SiteGroup
 from dcim.models import Platform, Region, Site, SiteGroup
+from ipam.models import IPAddress
 from tenancy.models import Tenant
 from tenancy.models import Tenant
-from utilities.testing import create_test_device
+from utilities.testing import create_test_device, create_test_virtualmachine
 from virtualization.models import *
 from virtualization.models import *
 
 
 
 
@@ -725,3 +727,21 @@ class VirtualMachineTestCase(TestCase):
 
 
         self.assertIsNone(vm.device)
         self.assertIsNone(vm.device)
         self.assertEqual(vm.cluster, self.cluster_with_site)
         self.assertEqual(vm.cluster, self.cluster_with_site)
+
+    @tag('regression')  # Ref: #23278
+    def test_unsaved_vm_rejects_primary_ip_of_other_vm(self):
+        """A primary IP valid for the VM that owns it, directly or via NAT, is rejected for an unsaved VM."""
+        vm = create_test_virtualmachine('vm1')
+        interface = VMInterface.objects.create(virtual_machine=vm, name='eth0')
+        ip4 = IPAddress.objects.create(address=IPNetwork('192.0.2.1/24'), assigned_object=interface)
+        ip6 = IPAddress.objects.create(address=IPNetwork('2001:db8::1/64'), assigned_object=interface)
+        nat_ip4 = IPAddress.objects.create(address=IPNetwork('198.51.100.1/24'), nat_inside=ip4)
+
+        for field, ip in (('primary_ip4', ip4), ('primary_ip4', nat_ip4), ('primary_ip6', ip6)):
+            with self.subTest(field=field, ip=ip):
+                setattr(vm, field, ip)
+                vm.full_clean()
+
+                with self.assertRaises(ValidationError) as context:
+                    VirtualMachine(name='vm2', cluster=vm.cluster, **{field: ip}).full_clean()
+                self.assertIn(field, context.exception.message_dict)