views.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. import traceback
  2. from django.contrib import messages
  3. from django.db import transaction
  4. from django.db.models import Prefetch, Sum
  5. from django.http import HttpResponse
  6. from django.shortcuts import get_object_or_404, redirect, render
  7. from django.urls import reverse
  8. from django.utils.translation import gettext as _
  9. from jinja2.exceptions import TemplateError
  10. from dcim.filtersets import DeviceFilterSet
  11. from dcim.forms import DeviceFilterForm
  12. from dcim.models import Device
  13. from dcim.tables import DeviceTable
  14. from extras.views import ObjectConfigContextView
  15. from ipam.models import IPAddress
  16. from ipam.tables import InterfaceVLANTable
  17. from netbox.constants import DEFAULT_ACTION_PERMISSIONS
  18. from netbox.views import generic
  19. from tenancy.views import ObjectContactsView
  20. from utilities.query import count_related
  21. from utilities.query_functions import CollateAsChar
  22. from utilities.views import GetRelatedModelsMixin, ViewTab, register_model_view
  23. from . import filtersets, forms, tables
  24. from .models import *
  25. #
  26. # Cluster types
  27. #
  28. class ClusterTypeListView(generic.ObjectListView):
  29. queryset = ClusterType.objects.annotate(
  30. cluster_count=count_related(Cluster, 'type')
  31. )
  32. filterset = filtersets.ClusterTypeFilterSet
  33. filterset_form = forms.ClusterTypeFilterForm
  34. table = tables.ClusterTypeTable
  35. @register_model_view(ClusterType)
  36. class ClusterTypeView(GetRelatedModelsMixin, generic.ObjectView):
  37. queryset = ClusterType.objects.all()
  38. def get_extra_context(self, request, instance):
  39. return {
  40. 'related_models': self.get_related_models(request, instance),
  41. }
  42. @register_model_view(ClusterType, 'edit')
  43. class ClusterTypeEditView(generic.ObjectEditView):
  44. queryset = ClusterType.objects.all()
  45. form = forms.ClusterTypeForm
  46. @register_model_view(ClusterType, 'delete')
  47. class ClusterTypeDeleteView(generic.ObjectDeleteView):
  48. queryset = ClusterType.objects.all()
  49. class ClusterTypeBulkImportView(generic.BulkImportView):
  50. queryset = ClusterType.objects.all()
  51. model_form = forms.ClusterTypeImportForm
  52. class ClusterTypeBulkEditView(generic.BulkEditView):
  53. queryset = ClusterType.objects.annotate(
  54. cluster_count=count_related(Cluster, 'type')
  55. )
  56. filterset = filtersets.ClusterTypeFilterSet
  57. table = tables.ClusterTypeTable
  58. form = forms.ClusterTypeBulkEditForm
  59. class ClusterTypeBulkDeleteView(generic.BulkDeleteView):
  60. queryset = ClusterType.objects.annotate(
  61. cluster_count=count_related(Cluster, 'type')
  62. )
  63. filterset = filtersets.ClusterTypeFilterSet
  64. table = tables.ClusterTypeTable
  65. #
  66. # Cluster groups
  67. #
  68. class ClusterGroupListView(generic.ObjectListView):
  69. queryset = ClusterGroup.objects.annotate(
  70. cluster_count=count_related(Cluster, 'group')
  71. )
  72. filterset = filtersets.ClusterGroupFilterSet
  73. filterset_form = forms.ClusterGroupFilterForm
  74. table = tables.ClusterGroupTable
  75. @register_model_view(ClusterGroup)
  76. class ClusterGroupView(GetRelatedModelsMixin, generic.ObjectView):
  77. queryset = ClusterGroup.objects.all()
  78. def get_extra_context(self, request, instance):
  79. return {
  80. 'related_models': self.get_related_models(request, instance),
  81. }
  82. @register_model_view(ClusterGroup, 'edit')
  83. class ClusterGroupEditView(generic.ObjectEditView):
  84. queryset = ClusterGroup.objects.all()
  85. form = forms.ClusterGroupForm
  86. @register_model_view(ClusterGroup, 'delete')
  87. class ClusterGroupDeleteView(generic.ObjectDeleteView):
  88. queryset = ClusterGroup.objects.all()
  89. class ClusterGroupBulkImportView(generic.BulkImportView):
  90. queryset = ClusterGroup.objects.annotate(
  91. cluster_count=count_related(Cluster, 'group')
  92. )
  93. model_form = forms.ClusterGroupImportForm
  94. class ClusterGroupBulkEditView(generic.BulkEditView):
  95. queryset = ClusterGroup.objects.annotate(
  96. cluster_count=count_related(Cluster, 'group')
  97. )
  98. filterset = filtersets.ClusterGroupFilterSet
  99. table = tables.ClusterGroupTable
  100. form = forms.ClusterGroupBulkEditForm
  101. class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
  102. queryset = ClusterGroup.objects.annotate(
  103. cluster_count=count_related(Cluster, 'group')
  104. )
  105. filterset = filtersets.ClusterGroupFilterSet
  106. table = tables.ClusterGroupTable
  107. @register_model_view(ClusterGroup, 'contacts')
  108. class ClusterGroupContactsView(ObjectContactsView):
  109. queryset = ClusterGroup.objects.all()
  110. #
  111. # Clusters
  112. #
  113. class ClusterListView(generic.ObjectListView):
  114. permission_required = 'virtualization.view_cluster'
  115. queryset = Cluster.objects.annotate(
  116. device_count=count_related(Device, 'cluster'),
  117. vm_count=count_related(VirtualMachine, 'cluster')
  118. )
  119. table = tables.ClusterTable
  120. filterset = filtersets.ClusterFilterSet
  121. filterset_form = forms.ClusterFilterForm
  122. @register_model_view(Cluster)
  123. class ClusterView(generic.ObjectView):
  124. queryset = Cluster.objects.all()
  125. def get_extra_context(self, request, instance):
  126. return instance.virtual_machines.aggregate(vcpus_sum=Sum('vcpus'), memory_sum=Sum('memory'), disk_sum=Sum('disk'))
  127. @register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
  128. class ClusterVirtualMachinesView(generic.ObjectChildrenView):
  129. queryset = Cluster.objects.all()
  130. child_model = VirtualMachine
  131. table = tables.VirtualMachineTable
  132. filterset = filtersets.VirtualMachineFilterSet
  133. filterset_form = forms.VirtualMachineFilterForm
  134. tab = ViewTab(
  135. label=_('Virtual Machines'),
  136. badge=lambda obj: obj.virtual_machines.count(),
  137. permission='virtualization.view_virtualmachine',
  138. weight=500
  139. )
  140. def get_children(self, request, parent):
  141. return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent)
  142. @register_model_view(Cluster, 'devices')
  143. class ClusterDevicesView(generic.ObjectChildrenView):
  144. queryset = Cluster.objects.all()
  145. child_model = Device
  146. table = DeviceTable
  147. filterset = DeviceFilterSet
  148. filterset_form = DeviceFilterForm
  149. template_name = 'virtualization/cluster/devices.html'
  150. actions = {
  151. 'add': {'add'},
  152. 'import': {'add'},
  153. 'export': {'view'},
  154. 'bulk_edit': {'change'},
  155. 'bulk_remove_devices': {'change'},
  156. }
  157. tab = ViewTab(
  158. label=_('Devices'),
  159. badge=lambda obj: obj.devices.count(),
  160. permission='virtualization.view_virtualmachine',
  161. weight=600
  162. )
  163. def get_children(self, request, parent):
  164. return Device.objects.restrict(request.user, 'view').filter(cluster=parent)
  165. @register_model_view(Cluster, 'edit')
  166. class ClusterEditView(generic.ObjectEditView):
  167. queryset = Cluster.objects.all()
  168. form = forms.ClusterForm
  169. @register_model_view(Cluster, 'delete')
  170. class ClusterDeleteView(generic.ObjectDeleteView):
  171. queryset = Cluster.objects.all()
  172. class ClusterBulkImportView(generic.BulkImportView):
  173. queryset = Cluster.objects.all()
  174. model_form = forms.ClusterImportForm
  175. class ClusterBulkEditView(generic.BulkEditView):
  176. queryset = Cluster.objects.all()
  177. filterset = filtersets.ClusterFilterSet
  178. table = tables.ClusterTable
  179. form = forms.ClusterBulkEditForm
  180. class ClusterBulkDeleteView(generic.BulkDeleteView):
  181. queryset = Cluster.objects.all()
  182. filterset = filtersets.ClusterFilterSet
  183. table = tables.ClusterTable
  184. @register_model_view(Cluster, 'add_devices', path='devices/add')
  185. class ClusterAddDevicesView(generic.ObjectEditView):
  186. queryset = Cluster.objects.all()
  187. form = forms.ClusterAddDevicesForm
  188. template_name = 'virtualization/cluster_add_devices.html'
  189. def get(self, request, pk):
  190. cluster = get_object_or_404(self.queryset, pk=pk)
  191. form = self.form(cluster, initial=request.GET)
  192. return render(request, self.template_name, {
  193. 'cluster': cluster,
  194. 'form': form,
  195. 'return_url': reverse('virtualization:cluster', kwargs={'pk': pk}),
  196. })
  197. def post(self, request, pk):
  198. cluster = get_object_or_404(self.queryset, pk=pk)
  199. form = self.form(cluster, request.POST)
  200. if form.is_valid():
  201. device_pks = form.cleaned_data['devices']
  202. with transaction.atomic():
  203. # Assign the selected Devices to the Cluster
  204. for device in Device.objects.filter(pk__in=device_pks):
  205. device.cluster = cluster
  206. device.save()
  207. messages.success(request, "Added {} devices to cluster {}".format(
  208. len(device_pks), cluster
  209. ))
  210. return redirect(cluster.get_absolute_url())
  211. return render(request, self.template_name, {
  212. 'cluster': cluster,
  213. 'form': form,
  214. 'return_url': cluster.get_absolute_url(),
  215. })
  216. @register_model_view(Cluster, 'remove_devices', path='devices/remove')
  217. class ClusterRemoveDevicesView(generic.ObjectEditView):
  218. queryset = Cluster.objects.all()
  219. form = forms.ClusterRemoveDevicesForm
  220. template_name = 'generic/bulk_remove.html'
  221. def post(self, request, pk):
  222. cluster = get_object_or_404(self.queryset, pk=pk)
  223. if '_confirm' in request.POST:
  224. form = self.form(request.POST)
  225. if form.is_valid():
  226. device_pks = form.cleaned_data['pk']
  227. with transaction.atomic():
  228. # Remove the selected Devices from the Cluster
  229. for device in Device.objects.filter(pk__in=device_pks):
  230. device.cluster = None
  231. device.save()
  232. messages.success(request, "Removed {} devices from cluster {}".format(
  233. len(device_pks), cluster
  234. ))
  235. return redirect(cluster.get_absolute_url())
  236. else:
  237. form = self.form(initial={'pk': request.POST.getlist('pk')})
  238. selected_objects = Device.objects.filter(pk__in=form.initial['pk'])
  239. device_table = DeviceTable(list(selected_objects), orderable=False)
  240. return render(request, self.template_name, {
  241. 'form': form,
  242. 'parent_obj': cluster,
  243. 'table': device_table,
  244. 'obj_type_plural': 'devices',
  245. 'return_url': cluster.get_absolute_url(),
  246. })
  247. @register_model_view(Cluster, 'contacts')
  248. class ClusterContactsView(ObjectContactsView):
  249. queryset = Cluster.objects.all()
  250. #
  251. # Virtual machines
  252. #
  253. class VirtualMachineListView(generic.ObjectListView):
  254. queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
  255. filterset = filtersets.VirtualMachineFilterSet
  256. filterset_form = forms.VirtualMachineFilterForm
  257. table = tables.VirtualMachineTable
  258. template_name = 'virtualization/virtualmachine_list.html'
  259. @register_model_view(VirtualMachine)
  260. class VirtualMachineView(generic.ObjectView):
  261. queryset = VirtualMachine.objects.all()
  262. @register_model_view(VirtualMachine, 'interfaces')
  263. class VirtualMachineInterfacesView(generic.ObjectChildrenView):
  264. queryset = VirtualMachine.objects.all()
  265. child_model = VMInterface
  266. table = tables.VirtualMachineVMInterfaceTable
  267. filterset = filtersets.VMInterfaceFilterSet
  268. filterset_form = forms.VMInterfaceFilterForm
  269. template_name = 'virtualization/virtualmachine/interfaces.html'
  270. actions = {
  271. **DEFAULT_ACTION_PERMISSIONS,
  272. 'bulk_rename': {'change'},
  273. }
  274. tab = ViewTab(
  275. label=_('Interfaces'),
  276. badge=lambda obj: obj.interface_count,
  277. permission='virtualization.view_vminterface',
  278. weight=500
  279. )
  280. def get_children(self, request, parent):
  281. return parent.interfaces.restrict(request.user, 'view').prefetch_related(
  282. Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
  283. 'tags',
  284. )
  285. @register_model_view(VirtualMachine, 'disks')
  286. class VirtualMachineVirtualDisksView(generic.ObjectChildrenView):
  287. queryset = VirtualMachine.objects.all()
  288. child_model = VirtualDisk
  289. table = tables.VirtualMachineVirtualDiskTable
  290. filterset = filtersets.VirtualDiskFilterSet
  291. filterset_form = forms.VirtualDiskFilterForm
  292. template_name = 'virtualization/virtualmachine/virtual_disks.html'
  293. tab = ViewTab(
  294. label=_('Virtual Disks'),
  295. badge=lambda obj: obj.virtual_disk_count,
  296. permission='virtualization.view_virtualdisk',
  297. weight=500
  298. )
  299. actions = {
  300. **DEFAULT_ACTION_PERMISSIONS,
  301. 'bulk_rename': {'change'},
  302. }
  303. def get_children(self, request, parent):
  304. return parent.virtualdisks.restrict(request.user, 'view').prefetch_related('tags')
  305. @register_model_view(VirtualMachine, 'configcontext', path='config-context')
  306. class VirtualMachineConfigContextView(ObjectConfigContextView):
  307. queryset = VirtualMachine.objects.annotate_config_context_data()
  308. base_template = 'virtualization/virtualmachine.html'
  309. tab = ViewTab(
  310. label=_('Config Context'),
  311. weight=2000
  312. )
  313. @register_model_view(VirtualMachine, 'render-config')
  314. class VirtualMachineRenderConfigView(generic.ObjectView):
  315. queryset = VirtualMachine.objects.all()
  316. template_name = 'virtualization/virtualmachine/render_config.html'
  317. tab = ViewTab(
  318. label=_('Render Config'),
  319. weight=2100
  320. )
  321. def get(self, request, **kwargs):
  322. instance = self.get_object(**kwargs)
  323. context = self.get_extra_context(request, instance)
  324. # If a direct export has been requested, return the rendered template content as a
  325. # downloadable file.
  326. if request.GET.get('export'):
  327. response = HttpResponse(context['rendered_config'], content_type='text')
  328. filename = f"{instance.name or 'config'}.txt"
  329. response['Content-Disposition'] = f'attachment; filename="{filename}"'
  330. return response
  331. return render(request, self.get_template_name(), {
  332. 'object': instance,
  333. 'tab': self.tab,
  334. **context,
  335. })
  336. def get_extra_context(self, request, instance):
  337. # Compile context data
  338. context_data = instance.get_config_context()
  339. context_data.update({'virtualmachine': instance})
  340. # Render the config template
  341. rendered_config = None
  342. if config_template := instance.get_config_template():
  343. try:
  344. rendered_config = config_template.render(context=context_data)
  345. except TemplateError as e:
  346. messages.error(request, f"An error occurred while rendering the template: {e}")
  347. rendered_config = traceback.format_exc()
  348. return {
  349. 'config_template': config_template,
  350. 'context_data': context_data,
  351. 'rendered_config': rendered_config,
  352. }
  353. @register_model_view(VirtualMachine, 'edit')
  354. class VirtualMachineEditView(generic.ObjectEditView):
  355. queryset = VirtualMachine.objects.all()
  356. form = forms.VirtualMachineForm
  357. @register_model_view(VirtualMachine, 'delete')
  358. class VirtualMachineDeleteView(generic.ObjectDeleteView):
  359. queryset = VirtualMachine.objects.all()
  360. class VirtualMachineBulkImportView(generic.BulkImportView):
  361. queryset = VirtualMachine.objects.all()
  362. model_form = forms.VirtualMachineImportForm
  363. class VirtualMachineBulkEditView(generic.BulkEditView):
  364. queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
  365. filterset = filtersets.VirtualMachineFilterSet
  366. table = tables.VirtualMachineTable
  367. form = forms.VirtualMachineBulkEditForm
  368. class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
  369. queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
  370. filterset = filtersets.VirtualMachineFilterSet
  371. table = tables.VirtualMachineTable
  372. @register_model_view(VirtualMachine, 'contacts')
  373. class VirtualMachineContactsView(ObjectContactsView):
  374. queryset = VirtualMachine.objects.all()
  375. #
  376. # VM interfaces
  377. #
  378. class VMInterfaceListView(generic.ObjectListView):
  379. queryset = VMInterface.objects.all()
  380. filterset = filtersets.VMInterfaceFilterSet
  381. filterset_form = forms.VMInterfaceFilterForm
  382. table = tables.VMInterfaceTable
  383. @register_model_view(VMInterface)
  384. class VMInterfaceView(generic.ObjectView):
  385. queryset = VMInterface.objects.all()
  386. def get_extra_context(self, request, instance):
  387. # Get child interfaces
  388. child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
  389. child_interfaces_tables = tables.VMInterfaceTable(
  390. child_interfaces,
  391. exclude=('virtual_machine',),
  392. orderable=False
  393. )
  394. # Get assigned VLANs and annotate whether each is tagged or untagged
  395. vlans = []
  396. if instance.untagged_vlan is not None:
  397. vlans.append(instance.untagged_vlan)
  398. vlans[0].tagged = False
  399. for vlan in instance.tagged_vlans.restrict(request.user).prefetch_related('site', 'group', 'tenant', 'role'):
  400. vlan.tagged = True
  401. vlans.append(vlan)
  402. vlan_table = InterfaceVLANTable(
  403. interface=instance,
  404. data=vlans,
  405. orderable=False
  406. )
  407. return {
  408. 'child_interfaces_table': child_interfaces_tables,
  409. 'vlan_table': vlan_table,
  410. }
  411. class VMInterfaceCreateView(generic.ComponentCreateView):
  412. queryset = VMInterface.objects.all()
  413. form = forms.VMInterfaceCreateForm
  414. model_form = forms.VMInterfaceForm
  415. @register_model_view(VMInterface, 'edit')
  416. class VMInterfaceEditView(generic.ObjectEditView):
  417. queryset = VMInterface.objects.all()
  418. form = forms.VMInterfaceForm
  419. @register_model_view(VMInterface, 'delete')
  420. class VMInterfaceDeleteView(generic.ObjectDeleteView):
  421. queryset = VMInterface.objects.all()
  422. class VMInterfaceBulkImportView(generic.BulkImportView):
  423. queryset = VMInterface.objects.all()
  424. model_form = forms.VMInterfaceImportForm
  425. class VMInterfaceBulkEditView(generic.BulkEditView):
  426. queryset = VMInterface.objects.all()
  427. filterset = filtersets.VMInterfaceFilterSet
  428. table = tables.VMInterfaceTable
  429. form = forms.VMInterfaceBulkEditForm
  430. class VMInterfaceBulkRenameView(generic.BulkRenameView):
  431. queryset = VMInterface.objects.all()
  432. form = forms.VMInterfaceBulkRenameForm
  433. class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
  434. # Ensure child interfaces are deleted prior to their parents
  435. queryset = VMInterface.objects.order_by('virtual_machine', 'parent', CollateAsChar('_name'))
  436. filterset = filtersets.VMInterfaceFilterSet
  437. table = tables.VMInterfaceTable
  438. #
  439. # Virtual disks
  440. #
  441. class VirtualDiskListView(generic.ObjectListView):
  442. queryset = VirtualDisk.objects.all()
  443. filterset = filtersets.VirtualDiskFilterSet
  444. filterset_form = forms.VirtualDiskFilterForm
  445. table = tables.VirtualDiskTable
  446. @register_model_view(VirtualDisk)
  447. class VirtualDiskView(generic.ObjectView):
  448. queryset = VirtualDisk.objects.all()
  449. class VirtualDiskCreateView(generic.ComponentCreateView):
  450. queryset = VirtualDisk.objects.all()
  451. form = forms.VirtualDiskCreateForm
  452. model_form = forms.VirtualDiskForm
  453. @register_model_view(VirtualDisk, 'edit')
  454. class VirtualDiskEditView(generic.ObjectEditView):
  455. queryset = VirtualDisk.objects.all()
  456. form = forms.VirtualDiskForm
  457. @register_model_view(VirtualDisk, 'delete')
  458. class VirtualDiskDeleteView(generic.ObjectDeleteView):
  459. queryset = VirtualDisk.objects.all()
  460. class VirtualDiskBulkImportView(generic.BulkImportView):
  461. queryset = VirtualDisk.objects.all()
  462. model_form = forms.VirtualDiskImportForm
  463. class VirtualDiskBulkEditView(generic.BulkEditView):
  464. queryset = VirtualDisk.objects.all()
  465. filterset = filtersets.VirtualDiskFilterSet
  466. table = tables.VirtualDiskTable
  467. form = forms.VirtualDiskBulkEditForm
  468. class VirtualDiskBulkRenameView(generic.BulkRenameView):
  469. queryset = VirtualDisk.objects.all()
  470. form = forms.VirtualDiskBulkRenameForm
  471. class VirtualDiskBulkDeleteView(generic.BulkDeleteView):
  472. queryset = VirtualDisk.objects.all()
  473. filterset = filtersets.VirtualDiskFilterSet
  474. table = tables.VirtualDiskTable
  475. #
  476. # Bulk Device component creation
  477. #
  478. class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
  479. parent_model = VirtualMachine
  480. parent_field = 'virtual_machine'
  481. form = forms.VMInterfaceBulkCreateForm
  482. queryset = VMInterface.objects.all()
  483. model_form = forms.VMInterfaceForm
  484. filterset = filtersets.VirtualMachineFilterSet
  485. table = tables.VirtualMachineTable
  486. default_return_url = 'virtualization:virtualmachine_list'
  487. def get_required_permission(self):
  488. return f'virtualization.add_vminterface'
  489. class VirtualMachineBulkAddVirtualDiskView(generic.BulkComponentCreateView):
  490. parent_model = VirtualMachine
  491. parent_field = 'virtual_machine'
  492. form = forms.VirtualDiskBulkCreateForm
  493. queryset = VirtualDisk.objects.all()
  494. model_form = forms.VirtualDiskForm
  495. filterset = filtersets.VirtualMachineFilterSet
  496. table = tables.VirtualMachineTable
  497. default_return_url = 'virtualization:virtualmachine_list'
  498. def get_required_permission(self):
  499. return f'virtualization.add_virtualdisk'