views.py 21 KB

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