Coverage for virtualization/forms/bulk_edit.py: 66%
120 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django import forms
2from django.conf import settings
3from django.utils.translation import gettext_lazy as _
5from dcim.choices import InterfaceModeChoices
6from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
7from dcim.forms.mixins import ScopedBulkEditForm
8from dcim.models import Device, DeviceRole, Platform, Site
9from extras.models import ConfigTemplate
10from ipam.models import VLAN, VRF, VLANGroup, VLANTranslationPolicy
11from netbox.forms import NetBoxModelBulkEditForm, OrganizationalModelBulkEditForm, PrimaryModelBulkEditForm
12from netbox.forms.mixins import OwnerMixin
13from tenancy.models import Tenant
14from utilities.forms import add_blank_choice
15from utilities.forms.fields import ChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField
16from utilities.forms.rendering import FieldSet
17from utilities.forms.utils import get_capacity_unit_label
18from utilities.forms.widgets import BulkEditNullBooleanSelect
20from ..choices import *
21from ..models import *
23__all__ = (
24 'ClusterBulkEditForm',
25 'ClusterGroupBulkEditForm',
26 'ClusterTypeBulkEditForm',
27 'VMInterfaceBulkEditForm',
28 'VirtualDiskBulkEditForm',
29 'VirtualMachineBulkEditForm',
30 'VirtualMachineTypeBulkEditForm',
31)
34class ClusterTypeBulkEditForm(OrganizationalModelBulkEditForm):
35 model = ClusterType
36 fieldsets = (
37 FieldSet('description'),
38 )
39 nullable_fields = ('description', 'comments')
42class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm):
43 model = ClusterGroup
44 fieldsets = (
45 FieldSet('description'),
46 )
47 nullable_fields = ('description', 'comments')
50class ClusterBulkEditForm(ScopedBulkEditForm, PrimaryModelBulkEditForm):
51 type = DynamicModelChoiceField(
52 label=_('Type'),
53 queryset=ClusterType.objects.all(),
54 required=False
55 )
56 group = DynamicModelChoiceField(
57 label=_('Group'),
58 queryset=ClusterGroup.objects.all(),
59 required=False
60 )
61 status = ChoiceField(
62 label=_('Status'),
63 choices=add_blank_choice(ClusterStatusChoices),
64 required=False,
65 initial=''
66 )
67 tenant = DynamicModelChoiceField(
68 label=_('Tenant'),
69 queryset=Tenant.objects.all(),
70 required=False
71 )
73 model = Cluster
74 fieldsets = (
75 FieldSet('type', 'group', 'status', 'tenant', 'description'),
76 FieldSet('scope', name=_('Scope')),
77 )
78 nullable_fields = (
79 'group', 'scope', 'tenant', 'description', 'comments',
80 )
83class VirtualMachineTypeBulkEditForm(PrimaryModelBulkEditForm):
84 default_platform = DynamicModelChoiceField(
85 label=_('Default platform'),
86 queryset=Platform.objects.all(),
87 required=False
88 )
89 default_vcpus = forms.IntegerField(
90 label=_('Default vCPUs'),
91 required=False,
92 )
93 default_memory = forms.IntegerField(
94 label=_('Default memory'),
95 required=False,
96 )
98 model = VirtualMachineType
99 fieldsets = (
100 FieldSet('description', name=_('Virtual Machine Type')),
101 FieldSet('default_platform', 'default_vcpus', 'default_memory', name=_('Defaults')),
102 )
103 nullable_fields = (
104 'default_platform', 'default_vcpus', 'default_memory', 'description', 'comments',
105 )
107 def __init__(self, *args, **kwargs):
108 super().__init__(*args, **kwargs)
110 # Set unit label based on configured RAM_BASE_UNIT (MB vs MiB)
111 self.fields['default_memory'].label = _('Default memory ({unit})').format(
112 unit=get_capacity_unit_label(settings.RAM_BASE_UNIT)
113 )
116class VirtualMachineBulkEditForm(PrimaryModelBulkEditForm):
117 virtual_machine_type = DynamicModelChoiceField(
118 label=_('Virtual machine type'),
119 queryset=VirtualMachineType.objects.all(),
120 required=False
121 )
122 status = ChoiceField(
123 label=_('Status'),
124 choices=add_blank_choice(VirtualMachineStatusChoices),
125 required=False,
126 initial='',
127 )
128 start_on_boot = ChoiceField(
129 label=_('Start on boot'),
130 choices=add_blank_choice(VirtualMachineStartOnBootChoices),
131 required=False,
132 initial='',
133 )
134 site = DynamicModelChoiceField(
135 label=_('Site'),
136 queryset=Site.objects.all(),
137 required=False
138 )
139 cluster = DynamicModelChoiceField(
140 label=_('Cluster'),
141 queryset=Cluster.objects.all(),
142 required=False,
143 query_params={
144 'site_id': '$site'
145 }
146 )
147 device = DynamicModelChoiceField(
148 label=_('Device'),
149 queryset=Device.objects.all(),
150 required=False,
151 query_params={
152 'cluster_id': '$cluster',
153 'site_id': '$site'
154 }
155 )
156 role = DynamicModelChoiceField(
157 label=_('Role'),
158 queryset=DeviceRole.objects.filter(
159 vm_role=True
160 ),
161 required=False,
162 query_params={
163 "vm_role": "True"
164 }
165 )
166 tenant = DynamicModelChoiceField(
167 label=_('Tenant'),
168 queryset=Tenant.objects.all(),
169 required=False
170 )
171 platform = DynamicModelChoiceField(
172 label=_('Platform'),
173 queryset=Platform.objects.all(),
174 required=False
175 )
176 vcpus = forms.IntegerField(
177 required=False,
178 label=_('vCPUs')
179 )
180 memory = forms.IntegerField(
181 required=False,
182 label=_('Memory')
183 )
184 disk = forms.IntegerField(
185 required=False,
186 label=_('Disk')
187 )
188 config_template = DynamicModelChoiceField(
189 queryset=ConfigTemplate.objects.all(),
190 required=False
191 )
193 model = VirtualMachine
194 fieldsets = (
195 FieldSet('virtual_machine_type', 'status', 'start_on_boot', 'role', 'tenant', 'platform', 'description'),
196 FieldSet('site', 'cluster', 'device', name=_('Placement')),
197 FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
198 FieldSet('config_template', name=_('Configuration')),
199 )
200 nullable_fields = (
201 'virtual_machine_type', 'role', 'site', 'cluster', 'device', 'platform', 'vcpus', 'memory', 'disk', 'tenant',
202 'description', 'comments',
203 )
205 def __init__(self, *args, **kwargs):
206 super().__init__(*args, **kwargs)
208 # The ?device=<id> GET param is navigation context (filter), not an intent to change the
209 # device field — drop it from initial so Django's changed_data doesn't treat it as an edit.
210 self.initial.pop('device', None)
212 # Set unit labels based on configured RAM_BASE_UNIT / DISK_BASE_UNIT (MB vs MiB)
213 self.fields['memory'].label = _('Memory ({unit})').format(unit=get_capacity_unit_label(settings.RAM_BASE_UNIT))
214 self.fields['disk'].label = _('Disk ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))
217class VMInterfaceBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
218 virtual_machine = forms.ModelChoiceField(
219 label=_('Virtual machine'),
220 queryset=VirtualMachine.objects.all(),
221 required=False,
222 disabled=True,
223 widget=forms.HiddenInput()
224 )
225 parent = DynamicModelChoiceField(
226 label=_('Parent'),
227 queryset=VMInterface.objects.all(),
228 required=False
229 )
230 bridge = DynamicModelChoiceField(
231 label=_('Bridge'),
232 queryset=VMInterface.objects.all(),
233 required=False
234 )
235 enabled = forms.NullBooleanField(
236 label=_('Enabled'),
237 required=False,
238 widget=BulkEditNullBooleanSelect()
239 )
240 mtu = forms.IntegerField(
241 required=False,
242 min_value=INTERFACE_MTU_MIN,
243 max_value=INTERFACE_MTU_MAX,
244 label=_('MTU')
245 )
246 description = forms.CharField(
247 label=_('Description'),
248 max_length=100,
249 required=False
250 )
251 mode = ChoiceField(
252 label=_('Mode'),
253 choices=add_blank_choice(InterfaceModeChoices),
254 required=False
255 )
256 vlan_group = DynamicModelChoiceField(
257 queryset=VLANGroup.objects.all(),
258 required=False,
259 label=_('VLAN group')
260 )
261 untagged_vlan = DynamicModelChoiceField(
262 queryset=VLAN.objects.all(),
263 required=False,
264 query_params={
265 'group_id': '$vlan_group',
266 },
267 label=_('Untagged VLAN')
268 )
269 tagged_vlans = DynamicModelMultipleChoiceField(
270 queryset=VLAN.objects.all(),
271 required=False,
272 query_params={
273 'group_id': '$vlan_group',
274 },
275 label=_('Tagged VLANs')
276 )
277 vrf = DynamicModelChoiceField(
278 queryset=VRF.objects.all(),
279 required=False,
280 label=_('VRF')
281 )
282 vlan_translation_policy = DynamicModelChoiceField(
283 queryset=VLANTranslationPolicy.objects.all(),
284 required=False,
285 label=_('VLAN Translation Policy')
286 )
288 model = VMInterface
289 fieldsets = (
290 FieldSet('mtu', 'enabled', 'vrf', 'description'),
291 FieldSet('parent', 'bridge', name=_('Related Interfaces')),
292 FieldSet(
293 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'vlan_translation_policy',
294 name=_('802.1Q Switching')
295 ),
296 )
297 nullable_fields = (
298 'parent', 'bridge', 'mtu', 'vrf', 'description', 'vlan_translation_policy',
299 )
301 def __init__(self, *args, **kwargs):
302 super().__init__(*args, **kwargs)
303 if 'virtual_machine' in self.initial:
304 vm_id = self.initial.get('virtual_machine')
306 # Restrict parent/bridge interface assignment by VM
307 self.fields['parent'].widget.add_query_param('virtual_machine_id', vm_id)
308 self.fields['bridge'].widget.add_query_param('virtual_machine_id', vm_id)
310 # Limit VLAN choices by virtual machine
311 self.fields['untagged_vlan'].widget.add_query_param('available_on_virtualmachine', vm_id)
312 self.fields['tagged_vlans'].widget.add_query_param('available_on_virtualmachine', vm_id)
314 else:
315 # See 5643
316 if 'pk' in self.initial:
317 site = None
318 interfaces = VMInterface.objects.filter(
319 pk__in=self.initial['pk']
320 ).prefetch_related(
321 'virtual_machine__site',
322 'virtual_machine__cluster',
323 'virtual_machine__device',
324 )
326 # Determine the effective site for each interface's VM (from its site,
327 # cluster, or device). If all selected interfaces share the same site,
328 # use it to filter VLAN choices; otherwise leave unfiltered.
329 for interface in interfaces:
330 vm = interface.virtual_machine
331 vm_site = (
332 vm.site
333 or (vm.cluster and vm.cluster._site)
334 or (vm.device and vm.device.site)
335 )
336 if site is None:
337 site = vm_site
338 elif vm_site is not site:
339 site = None
340 break
342 if site is not None:
343 self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
344 self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
346 self.fields['parent'].choices = ()
347 self.fields['parent'].widget.attrs['disabled'] = True
348 self.fields['bridge'].choices = ()
349 self.fields['bridge'].widget.attrs['disabled'] = True
352class VirtualDiskBulkEditForm(OwnerMixin, NetBoxModelBulkEditForm):
353 virtual_machine = forms.ModelChoiceField(
354 label=_('Virtual machine'),
355 queryset=VirtualMachine.objects.all(),
356 required=False,
357 disabled=True,
358 widget=forms.HiddenInput()
359 )
360 size = forms.IntegerField(
361 required=False,
362 label=_('Size')
363 )
364 description = forms.CharField(
365 label=_('Description'),
366 max_length=100,
367 required=False
368 )
370 model = VirtualDisk
371 fieldsets = (
372 FieldSet('size', 'description'),
373 )
374 nullable_fields = ('description',)
376 def __init__(self, *args, **kwargs):
377 super().__init__(*args, **kwargs)
379 # Set unit label based on configured DISK_BASE_UNIT (MB vs MiB)
380 self.fields['size'].label = _('Size ({unit})').format(unit=get_capacity_unit_label(settings.DISK_BASE_UNIT))