views.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577
  1. import traceback
  2. from collections import defaultdict
  3. from django.contrib import messages
  4. from django.db import transaction
  5. from django.db.models import Prefetch, Sum
  6. from django.http import HttpResponse
  7. from django.shortcuts import get_object_or_404, redirect, render
  8. from django.urls import reverse
  9. from django.utils.translation import gettext as _
  10. from jinja2.exceptions import TemplateError
  11. from dcim.filtersets import DeviceFilterSet
  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.views import generic
  18. from tenancy.views import ObjectContactsView
  19. from utilities.utils import count_related
  20. from utilities.views import ViewTab, register_model_view
  21. from . import filtersets, forms, tables
  22. from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
  23. #
  24. # Cluster types
  25. #
  26. class ClusterTypeListView(generic.ObjectListView):
  27. queryset = ClusterType.objects.annotate(
  28. cluster_count=count_related(Cluster, 'type')
  29. )
  30. filterset = filtersets.ClusterTypeFilterSet
  31. filterset_form = forms.ClusterTypeFilterForm
  32. table = tables.ClusterTypeTable
  33. @register_model_view(ClusterType)
  34. class ClusterTypeView(generic.ObjectView):
  35. queryset = ClusterType.objects.all()
  36. def get_extra_context(self, request, instance):
  37. related_models = (
  38. (Cluster.objects.restrict(request.user, 'view').filter(type=instance), 'type_id'),
  39. )
  40. return {
  41. 'related_models': related_models,
  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(generic.ObjectView):
  78. queryset = ClusterGroup.objects.all()
  79. def get_extra_context(self, request, instance):
  80. related_models = (
  81. (Cluster.objects.restrict(request.user, 'view').filter(group=instance), 'group_id'),
  82. )
  83. return {
  84. 'related_models': related_models,
  85. }
  86. @register_model_view(ClusterGroup, 'edit')
  87. class ClusterGroupEditView(generic.ObjectEditView):
  88. queryset = ClusterGroup.objects.all()
  89. form = forms.ClusterGroupForm
  90. @register_model_view(ClusterGroup, 'delete')
  91. class ClusterGroupDeleteView(generic.ObjectDeleteView):
  92. queryset = ClusterGroup.objects.all()
  93. class ClusterGroupBulkImportView(generic.BulkImportView):
  94. queryset = ClusterGroup.objects.annotate(
  95. cluster_count=count_related(Cluster, 'group')
  96. )
  97. model_form = forms.ClusterGroupImportForm
  98. class ClusterGroupBulkEditView(generic.BulkEditView):
  99. queryset = ClusterGroup.objects.annotate(
  100. cluster_count=count_related(Cluster, 'group')
  101. )
  102. filterset = filtersets.ClusterGroupFilterSet
  103. table = tables.ClusterGroupTable
  104. form = forms.ClusterGroupBulkEditForm
  105. class ClusterGroupBulkDeleteView(generic.BulkDeleteView):
  106. queryset = ClusterGroup.objects.annotate(
  107. cluster_count=count_related(Cluster, 'group')
  108. )
  109. filterset = filtersets.ClusterGroupFilterSet
  110. table = tables.ClusterGroupTable
  111. @register_model_view(ClusterGroup, 'contacts')
  112. class ClusterGroupContactsView(ObjectContactsView):
  113. queryset = ClusterGroup.objects.all()
  114. #
  115. # Clusters
  116. #
  117. class ClusterListView(generic.ObjectListView):
  118. permission_required = 'virtualization.view_cluster'
  119. queryset = Cluster.objects.annotate(
  120. device_count=count_related(Device, 'cluster'),
  121. vm_count=count_related(VirtualMachine, 'cluster')
  122. )
  123. table = tables.ClusterTable
  124. filterset = filtersets.ClusterFilterSet
  125. filterset_form = forms.ClusterFilterForm
  126. @register_model_view(Cluster)
  127. class ClusterView(generic.ObjectView):
  128. queryset = Cluster.objects.all()
  129. def get_extra_context(self, request, instance):
  130. return instance.virtual_machines.aggregate(vcpus_sum=Sum('vcpus'), memory_sum=Sum('memory'), disk_sum=Sum('disk'))
  131. @register_model_view(Cluster, 'virtualmachines', path='virtual-machines')
  132. class ClusterVirtualMachinesView(generic.ObjectChildrenView):
  133. queryset = Cluster.objects.all()
  134. child_model = VirtualMachine
  135. table = tables.VirtualMachineTable
  136. filterset = filtersets.VirtualMachineFilterSet
  137. template_name = 'generic/object_children.html'
  138. tab = ViewTab(
  139. label=_('Virtual Machines'),
  140. badge=lambda obj: obj.virtual_machines.count(),
  141. permission='virtualization.view_virtualmachine',
  142. weight=500
  143. )
  144. def get_children(self, request, parent):
  145. return VirtualMachine.objects.restrict(request.user, 'view').filter(cluster=parent)
  146. @register_model_view(Cluster, 'devices')
  147. class ClusterDevicesView(generic.ObjectChildrenView):
  148. queryset = Cluster.objects.all()
  149. child_model = Device
  150. table = DeviceTable
  151. filterset = DeviceFilterSet
  152. template_name = 'virtualization/cluster/devices.html'
  153. actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_remove_devices')
  154. action_perms = defaultdict(set, **{
  155. 'add': {'add'},
  156. 'import': {'add'},
  157. 'bulk_edit': {'change'},
  158. 'bulk_remove_devices': {'change'},
  159. })
  160. tab = ViewTab(
  161. label=_('Devices'),
  162. badge=lambda obj: obj.devices.count(),
  163. permission='virtualization.view_virtualmachine',
  164. weight=600
  165. )
  166. def get_children(self, request, parent):
  167. return Device.objects.restrict(request.user, 'view').filter(cluster=parent)
  168. @register_model_view(Cluster, 'edit')
  169. class ClusterEditView(generic.ObjectEditView):
  170. queryset = Cluster.objects.all()
  171. form = forms.ClusterForm
  172. @register_model_view(Cluster, 'delete')
  173. class ClusterDeleteView(generic.ObjectDeleteView):
  174. queryset = Cluster.objects.all()
  175. class ClusterBulkImportView(generic.BulkImportView):
  176. queryset = Cluster.objects.all()
  177. model_form = forms.ClusterImportForm
  178. class ClusterBulkEditView(generic.BulkEditView):
  179. queryset = Cluster.objects.all()
  180. filterset = filtersets.ClusterFilterSet
  181. table = tables.ClusterTable
  182. form = forms.ClusterBulkEditForm
  183. class ClusterBulkDeleteView(generic.BulkDeleteView):
  184. queryset = Cluster.objects.all()
  185. filterset = filtersets.ClusterFilterSet
  186. table = tables.ClusterTable
  187. @register_model_view(Cluster, 'add_devices', path='devices/add')
  188. class ClusterAddDevicesView(generic.ObjectEditView):
  189. queryset = Cluster.objects.all()
  190. form = forms.ClusterAddDevicesForm
  191. template_name = 'virtualization/cluster_add_devices.html'
  192. def get(self, request, pk):
  193. cluster = get_object_or_404(self.queryset, pk=pk)
  194. form = self.form(cluster, initial=request.GET)
  195. return render(request, self.template_name, {
  196. 'cluster': cluster,
  197. 'form': form,
  198. 'return_url': reverse('virtualization:cluster', kwargs={'pk': pk}),
  199. })
  200. def post(self, request, pk):
  201. cluster = get_object_or_404(self.queryset, pk=pk)
  202. form = self.form(cluster, request.POST)
  203. if form.is_valid():
  204. device_pks = form.cleaned_data['devices']
  205. with transaction.atomic():
  206. # Assign the selected Devices to the Cluster
  207. for device in Device.objects.filter(pk__in=device_pks):
  208. device.cluster = cluster
  209. device.save()
  210. messages.success(request, "Added {} devices to cluster {}".format(
  211. len(device_pks), cluster
  212. ))
  213. return redirect(cluster.get_absolute_url())
  214. return render(request, self.template_name, {
  215. 'cluster': cluster,
  216. 'form': form,
  217. 'return_url': cluster.get_absolute_url(),
  218. })
  219. @register_model_view(Cluster, 'remove_devices', path='devices/remove')
  220. class ClusterRemoveDevicesView(generic.ObjectEditView):
  221. queryset = Cluster.objects.all()
  222. form = forms.ClusterRemoveDevicesForm
  223. template_name = 'generic/bulk_remove.html'
  224. def post(self, request, pk):
  225. cluster = get_object_or_404(self.queryset, pk=pk)
  226. if '_confirm' in request.POST:
  227. form = self.form(request.POST)
  228. if form.is_valid():
  229. device_pks = form.cleaned_data['pk']
  230. with transaction.atomic():
  231. # Remove the selected Devices from the Cluster
  232. for device in Device.objects.filter(pk__in=device_pks):
  233. device.cluster = None
  234. device.save()
  235. messages.success(request, "Removed {} devices from cluster {}".format(
  236. len(device_pks), 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. template_name = 'virtualization/virtualmachine/interfaces.html'
  272. tab = ViewTab(
  273. label=_('Interfaces'),
  274. badge=lambda obj: obj.interface_count,
  275. permission='virtualization.view_vminterface',
  276. weight=500
  277. )
  278. actions = ('add', 'import', 'export', 'bulk_edit', 'bulk_delete', 'bulk_rename')
  279. action_perms = defaultdict(set, **{
  280. 'add': {'add'},
  281. 'import': {'add'},
  282. 'bulk_edit': {'change'},
  283. 'bulk_delete': {'delete'},
  284. 'bulk_rename': {'change'},
  285. })
  286. def get_children(self, request, parent):
  287. return parent.interfaces.restrict(request.user, 'view').prefetch_related(
  288. Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
  289. 'tags',
  290. )
  291. @register_model_view(VirtualMachine, 'configcontext', path='config-context')
  292. class VirtualMachineConfigContextView(ObjectConfigContextView):
  293. queryset = VirtualMachine.objects.annotate_config_context_data()
  294. base_template = 'virtualization/virtualmachine.html'
  295. tab = ViewTab(
  296. label=_('Config Context'),
  297. weight=2000
  298. )
  299. @register_model_view(VirtualMachine, 'render-config')
  300. class VirtualMachineRenderConfigView(generic.ObjectView):
  301. queryset = VirtualMachine.objects.all()
  302. template_name = 'virtualization/virtualmachine/render_config.html'
  303. tab = ViewTab(
  304. label=_('Render Config'),
  305. permission='extras.view_configtemplate',
  306. weight=2100
  307. )
  308. def get(self, request, **kwargs):
  309. instance = self.get_object(**kwargs)
  310. context = self.get_extra_context(request, instance)
  311. # If a direct export has been requested, return the rendered template content as a
  312. # downloadable file.
  313. if request.GET.get('export'):
  314. response = HttpResponse(context['rendered_config'], content_type='text')
  315. filename = f"{instance.name or 'config'}.txt"
  316. response['Content-Disposition'] = f'attachment; filename="{filename}"'
  317. return response
  318. return render(request, self.get_template_name(), {
  319. 'object': instance,
  320. 'tab': self.tab,
  321. **context,
  322. })
  323. def get_extra_context(self, request, instance):
  324. # Compile context data
  325. context_data = instance.get_config_context()
  326. context_data.update({'virtualmachine': instance})
  327. # Render the config template
  328. rendered_config = None
  329. if config_template := instance.get_config_template():
  330. try:
  331. rendered_config = config_template.render(context=context_data)
  332. except TemplateError as e:
  333. messages.error(request, f"An error occurred while rendering the template: {e}")
  334. rendered_config = traceback.format_exc()
  335. return {
  336. 'config_template': config_template,
  337. 'context_data': context_data,
  338. 'rendered_config': rendered_config,
  339. }
  340. @register_model_view(VirtualMachine, 'edit')
  341. class VirtualMachineEditView(generic.ObjectEditView):
  342. queryset = VirtualMachine.objects.all()
  343. form = forms.VirtualMachineForm
  344. @register_model_view(VirtualMachine, 'delete')
  345. class VirtualMachineDeleteView(generic.ObjectDeleteView):
  346. queryset = VirtualMachine.objects.all()
  347. class VirtualMachineBulkImportView(generic.BulkImportView):
  348. queryset = VirtualMachine.objects.all()
  349. model_form = forms.VirtualMachineImportForm
  350. class VirtualMachineBulkEditView(generic.BulkEditView):
  351. queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
  352. filterset = filtersets.VirtualMachineFilterSet
  353. table = tables.VirtualMachineTable
  354. form = forms.VirtualMachineBulkEditForm
  355. class VirtualMachineBulkDeleteView(generic.BulkDeleteView):
  356. queryset = VirtualMachine.objects.prefetch_related('primary_ip4', 'primary_ip6')
  357. filterset = filtersets.VirtualMachineFilterSet
  358. table = tables.VirtualMachineTable
  359. @register_model_view(VirtualMachine, 'contacts')
  360. class VirtualMachineContactsView(ObjectContactsView):
  361. queryset = VirtualMachine.objects.all()
  362. #
  363. # VM interfaces
  364. #
  365. class VMInterfaceListView(generic.ObjectListView):
  366. queryset = VMInterface.objects.all()
  367. filterset = filtersets.VMInterfaceFilterSet
  368. filterset_form = forms.VMInterfaceFilterForm
  369. table = tables.VMInterfaceTable
  370. @register_model_view(VMInterface)
  371. class VMInterfaceView(generic.ObjectView):
  372. queryset = VMInterface.objects.all()
  373. def get_extra_context(self, request, instance):
  374. # Get child interfaces
  375. child_interfaces = VMInterface.objects.restrict(request.user, 'view').filter(parent=instance)
  376. child_interfaces_tables = tables.VMInterfaceTable(
  377. child_interfaces,
  378. exclude=('virtual_machine',),
  379. orderable=False
  380. )
  381. # Get assigned VLANs and annotate whether each is tagged or untagged
  382. vlans = []
  383. if instance.untagged_vlan is not None:
  384. vlans.append(instance.untagged_vlan)
  385. vlans[0].tagged = False
  386. for vlan in instance.tagged_vlans.restrict(request.user).prefetch_related('site', 'group', 'tenant', 'role'):
  387. vlan.tagged = True
  388. vlans.append(vlan)
  389. vlan_table = InterfaceVLANTable(
  390. interface=instance,
  391. data=vlans,
  392. orderable=False
  393. )
  394. return {
  395. 'child_interfaces_table': child_interfaces_tables,
  396. 'vlan_table': vlan_table,
  397. }
  398. class VMInterfaceCreateView(generic.ComponentCreateView):
  399. queryset = VMInterface.objects.all()
  400. form = forms.VMInterfaceCreateForm
  401. model_form = forms.VMInterfaceForm
  402. @register_model_view(VMInterface, 'edit')
  403. class VMInterfaceEditView(generic.ObjectEditView):
  404. queryset = VMInterface.objects.all()
  405. form = forms.VMInterfaceForm
  406. @register_model_view(VMInterface, 'delete')
  407. class VMInterfaceDeleteView(generic.ObjectDeleteView):
  408. queryset = VMInterface.objects.all()
  409. class VMInterfaceBulkImportView(generic.BulkImportView):
  410. queryset = VMInterface.objects.all()
  411. model_form = forms.VMInterfaceImportForm
  412. class VMInterfaceBulkEditView(generic.BulkEditView):
  413. queryset = VMInterface.objects.all()
  414. filterset = filtersets.VMInterfaceFilterSet
  415. table = tables.VMInterfaceTable
  416. form = forms.VMInterfaceBulkEditForm
  417. class VMInterfaceBulkRenameView(generic.BulkRenameView):
  418. queryset = VMInterface.objects.all()
  419. form = forms.VMInterfaceBulkRenameForm
  420. class VMInterfaceBulkDeleteView(generic.BulkDeleteView):
  421. queryset = VMInterface.objects.all()
  422. filterset = filtersets.VMInterfaceFilterSet
  423. table = tables.VMInterfaceTable
  424. #
  425. # Bulk Device component creation
  426. #
  427. class VirtualMachineBulkAddInterfaceView(generic.BulkComponentCreateView):
  428. parent_model = VirtualMachine
  429. parent_field = 'virtual_machine'
  430. form = forms.VMInterfaceBulkCreateForm
  431. queryset = VMInterface.objects.all()
  432. model_form = forms.VMInterfaceForm
  433. filterset = filtersets.VirtualMachineFilterSet
  434. table = tables.VirtualMachineTable
  435. default_return_url = 'virtualization:virtualmachine_list'
  436. def get_required_permission(self):
  437. return f'virtualization.add_vminterface'