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

1from django import forms 

2from django.conf import settings 

3from django.utils.translation import gettext_lazy as _ 

4 

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 

19 

20from ..choices import * 

21from ..models import * 

22 

23__all__ = ( 

24 'ClusterBulkEditForm', 

25 'ClusterGroupBulkEditForm', 

26 'ClusterTypeBulkEditForm', 

27 'VMInterfaceBulkEditForm', 

28 'VirtualDiskBulkEditForm', 

29 'VirtualMachineBulkEditForm', 

30 'VirtualMachineTypeBulkEditForm', 

31) 

32 

33 

34class ClusterTypeBulkEditForm(OrganizationalModelBulkEditForm): 

35 model = ClusterType 

36 fieldsets = ( 

37 FieldSet('description'), 

38 ) 

39 nullable_fields = ('description', 'comments') 

40 

41 

42class ClusterGroupBulkEditForm(OrganizationalModelBulkEditForm): 

43 model = ClusterGroup 

44 fieldsets = ( 

45 FieldSet('description'), 

46 ) 

47 nullable_fields = ('description', 'comments') 

48 

49 

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 ) 

72 

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 ) 

81 

82 

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 ) 

97 

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 ) 

106 

107 def __init__(self, *args, **kwargs): 

108 super().__init__(*args, **kwargs) 

109 

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 ) 

114 

115 

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 ) 

192 

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 ) 

204 

205 def __init__(self, *args, **kwargs): 

206 super().__init__(*args, **kwargs) 

207 

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) 

211 

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)) 

215 

216 

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 ) 

287 

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 ) 

300 

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') 

305 

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) 

309 

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) 

313 

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 ) 

325 

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 

341 

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) 

345 

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 

350 

351 

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 ) 

369 

370 model = VirtualDisk 

371 fieldsets = ( 

372 FieldSet('size', 'description'), 

373 ) 

374 nullable_fields = ('description',) 

375 

376 def __init__(self, *args, **kwargs): 

377 super().__init__(*args, **kwargs) 

378 

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))