From e54e70c7351e6c70aa0c9694cd06e0ad162053cf Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 27 Apr 2026 10:48:45 -0700 Subject: [PATCH 1/2] #21990 fix deletion of device from Virtual Machines --- netbox/dcim/forms/bulk_edit.py | 7 ++++++ netbox/virtualization/forms/bulk_edit.py | 4 ++++ netbox/virtualization/tests/test_views.py | 27 +++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index a3cc217c6..f9bfeb985 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1754,6 +1754,13 @@ class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm): ) nullable_fields = ('device', 'tenant', ) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Remove parent device passed as context to avoid conflicts with the actual device field + # on this form (see bug #21990) + self.initial.pop('device', None) + # # Addressing diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index f8868d8d3..887a89e9f 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -164,6 +164,10 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Remove parent device passed as context to avoid conflicts with the actual device field + # on this form (see bug #21990) + self.initial.pop('device', None) + # Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB) self.fields['memory'].label = _('Memory ({unit})').format(unit=get_capacity_unit_label(settings.RAM_BASE_UNIT)) self.fields['disk'].label = _('Disk ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT)) diff --git a/netbox/virtualization/tests/test_views.py b/netbox/virtualization/tests/test_views.py index 435e9579e..972505705 100644 --- a/netbox/virtualization/tests/test_views.py +++ b/netbox/virtualization/tests/test_views.py @@ -335,6 +335,33 @@ class VirtualMachineTestCase(ViewTestCases.PrimaryObjectViewTestCase): url = reverse('virtualization:virtualmachine_interfaces', kwargs={'pk': virtualmachine.pk}) self.assertHttpStatus(self.client.get(url), 200) + def test_bulk_edit_device_context_preserves_device(self): + """ + Regression test for #21990: Bulk editing VMs from the Device's VMs tab (URL contains + ?device=) must not clear the device field on those VMs. + """ + self.add_permissions('virtualization.view_virtualmachine', 'virtualization.change_virtualmachine') + + device = VirtualMachine.objects.filter(device__isnull=False).first().device + vms = list(VirtualMachine.objects.filter(device=device)[:3]) + pk_list = [vm.pk for vm in vms] + + data = { + 'pk': pk_list, + '_apply': True, + # Only change status — device is intentionally omitted + 'status': VirtualMachineStatusChoices.STATUS_STAGED, + } + + # Simulate navigation from Device -> Virtual Machines tab by passing ?device= as GET param + url = reverse('virtualization:virtualmachine_bulk_edit') + f'?device={device.pk}' + response = self.client.post(url, data) + self.assertHttpStatus(response, 302) + + for vm in VirtualMachine.objects.filter(pk__in=pk_list): + self.assertEqual(vm.device, device, msg=f"Device was unexpectedly cleared on VM '{vm.name}'") + self.assertEqual(vm.status, VirtualMachineStatusChoices.STATUS_STAGED) + def test_virtualmachine_renderconfig(self): configtemplate = ConfigTemplate.objects.create( name='Test Config Template', From be86c50204a1215d95b91b39c24676299c38b113 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 27 Apr 2026 11:06:03 -0700 Subject: [PATCH 2/2] cleanup --- netbox/dcim/forms/bulk_edit.py | 4 ++-- netbox/dcim/tests/test_views.py | 27 ++++++++++++++++++++++++ netbox/virtualization/forms/bulk_edit.py | 4 ++-- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index f9bfeb985..1282639fb 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -1757,8 +1757,8 @@ class VirtualDeviceContextBulkEditForm(PrimaryModelBulkEditForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Remove parent device passed as context to avoid conflicts with the actual device field - # on this form (see bug #21990) + # The ?device= GET param is navigation context (filter), not an intent to change the + # device field — drop it from initial so Django's changed_data doesn't treat it as an edit. self.initial.pop('device', None) diff --git a/netbox/dcim/tests/test_views.py b/netbox/dcim/tests/test_views.py index 78a315d29..2d41cc611 100644 --- a/netbox/dcim/tests/test_views.py +++ b/netbox/dcim/tests/test_views.py @@ -4027,6 +4027,33 @@ class VirtualDeviceContextTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'status': VirtualDeviceContextStatusChoices.STATUS_OFFLINE, } + def test_bulk_edit_device_context_preserves_device(self): + """ + Regression test: Bulk editing VDCs from the Device's VDCs tab (URL contains + ?device=) must not clear the device field on those VDCs. + """ + self.add_permissions('dcim.view_virtualdevicecontext', 'dcim.change_virtualdevicecontext') + + device = VirtualDeviceContext.objects.filter(device__isnull=False).first().device + vdcs = list(VirtualDeviceContext.objects.filter(device=device)[:3]) + pk_list = [vdc.pk for vdc in vdcs] + + data = { + 'pk': pk_list, + '_apply': True, + # Only change status — device is intentionally omitted + 'status': VirtualDeviceContextStatusChoices.STATUS_PLANNED, + } + + # Simulate navigation from Device -> VDCs tab by passing ?device= as GET param + url = reverse('dcim:virtualdevicecontext_bulk_edit') + f'?device={device.pk}' + response = self.client.post(url, data) + self.assertHttpStatus(response, 302) + + for vdc in VirtualDeviceContext.objects.filter(pk__in=pk_list): + self.assertEqual(vdc.device, device, msg=f"Device was unexpectedly cleared on VDC '{vdc.name}'") + self.assertEqual(vdc.status, VirtualDeviceContextStatusChoices.STATUS_PLANNED) + class MACAddressTestCase(ViewTestCases.PrimaryObjectViewTestCase): model = MACAddress diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 887a89e9f..ff84b537c 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -164,8 +164,8 @@ class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Remove parent device passed as context to avoid conflicts with the actual device field - # on this form (see bug #21990) + # The ?device= GET param is navigation context (filter), not an intent to change the + # device field — drop it from initial so Django's changed_data doesn't treat it as an edit. self.initial.pop('device', None) # Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB)