model_forms.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ValidationError
  4. from django.utils.translation import gettext_lazy as _
  5. from dcim.forms.common import InterfaceCommonForm
  6. from dcim.forms.mixins import ScopedForm
  7. from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
  8. from extras.models import ConfigTemplate
  9. from ipam.choices import VLANQinQRoleChoices
  10. from ipam.models import IPAddress, VLAN, VLANGroup, VLANTranslationPolicy, VRF
  11. from netbox.forms import NetBoxModelForm
  12. from tenancy.forms import TenancyForm
  13. from utilities.forms import ConfirmationForm
  14. from utilities.forms.fields import (
  15. CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField,
  16. )
  17. from utilities.forms.rendering import FieldSet
  18. from utilities.forms.widgets import HTMXSelect
  19. from virtualization.models import *
  20. __all__ = (
  21. 'ClusterAddDevicesForm',
  22. 'ClusterForm',
  23. 'ClusterGroupForm',
  24. 'ClusterRemoveDevicesForm',
  25. 'ClusterTypeForm',
  26. 'VirtualDiskForm',
  27. 'VirtualMachineForm',
  28. 'VMInterfaceForm',
  29. )
  30. class ClusterTypeForm(NetBoxModelForm):
  31. slug = SlugField()
  32. fieldsets = (
  33. FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Type')),
  34. )
  35. class Meta:
  36. model = ClusterType
  37. fields = (
  38. 'name', 'slug', 'description', 'tags',
  39. )
  40. class ClusterGroupForm(NetBoxModelForm):
  41. slug = SlugField()
  42. fieldsets = (
  43. FieldSet('name', 'slug', 'description', 'tags', name=_('Cluster Group')),
  44. )
  45. class Meta:
  46. model = ClusterGroup
  47. fields = (
  48. 'name', 'slug', 'description', 'tags',
  49. )
  50. class ClusterForm(TenancyForm, ScopedForm, NetBoxModelForm):
  51. type = DynamicModelChoiceField(
  52. label=_('Type'),
  53. queryset=ClusterType.objects.all()
  54. )
  55. group = DynamicModelChoiceField(
  56. label=_('Group'),
  57. queryset=ClusterGroup.objects.all(),
  58. required=False
  59. )
  60. comments = CommentField()
  61. fieldsets = (
  62. FieldSet('name', 'type', 'group', 'status', 'description', 'tags', name=_('Cluster')),
  63. FieldSet('scope_type', 'scope', name=_('Scope')),
  64. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  65. )
  66. class Meta:
  67. model = Cluster
  68. fields = (
  69. 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'description', 'comments', 'tags',
  70. )
  71. class ClusterAddDevicesForm(forms.Form):
  72. region = DynamicModelChoiceField(
  73. label=_('Region'),
  74. queryset=Region.objects.all(),
  75. required=False,
  76. null_option='None'
  77. )
  78. site_group = DynamicModelChoiceField(
  79. label=_('Site group'),
  80. queryset=SiteGroup.objects.all(),
  81. required=False,
  82. null_option='None'
  83. )
  84. site = DynamicModelChoiceField(
  85. label=_('Site'),
  86. queryset=Site.objects.all(),
  87. required=False,
  88. query_params={
  89. 'region_id': '$region',
  90. 'group_id': '$site_group',
  91. }
  92. )
  93. rack = DynamicModelChoiceField(
  94. label=_('Rack'),
  95. queryset=Rack.objects.all(),
  96. required=False,
  97. null_option='None',
  98. query_params={
  99. 'site_id': '$site'
  100. }
  101. )
  102. devices = DynamicModelMultipleChoiceField(
  103. label=_('Devices'),
  104. queryset=Device.objects.all(),
  105. query_params={
  106. 'site_id': '$site',
  107. 'rack_id': '$rack',
  108. 'cluster_id': 'null',
  109. }
  110. )
  111. class Meta:
  112. fields = [
  113. 'region', 'site', 'rack', 'devices',
  114. ]
  115. def __init__(self, cluster, *args, **kwargs):
  116. self.cluster = cluster
  117. super().__init__(*args, **kwargs)
  118. self.fields['devices'].choices = []
  119. def clean(self):
  120. super().clean()
  121. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  122. if self.cluster.site is not None:
  123. for device in self.cleaned_data.get('devices', []):
  124. if device.site != self.cluster.site:
  125. raise ValidationError({
  126. 'devices': _(
  127. "{device} belongs to a different site ({device_site}) than the cluster ({cluster_site})"
  128. ).format(
  129. device=device,
  130. device_site=device.site,
  131. cluster_site=self.cluster.site
  132. )
  133. })
  134. class ClusterRemoveDevicesForm(ConfirmationForm):
  135. pk = forms.ModelMultipleChoiceField(
  136. queryset=Device.objects.all(),
  137. widget=forms.MultipleHiddenInput()
  138. )
  139. class VirtualMachineForm(TenancyForm, NetBoxModelForm):
  140. site = DynamicModelChoiceField(
  141. label=_('Site'),
  142. queryset=Site.objects.all(),
  143. required=False
  144. )
  145. cluster = DynamicModelChoiceField(
  146. label=_('Cluster'),
  147. queryset=Cluster.objects.all(),
  148. required=False,
  149. selector=True,
  150. query_params={
  151. 'site_id': ['$site', 'null']
  152. },
  153. )
  154. device = DynamicModelChoiceField(
  155. label=_('Device'),
  156. queryset=Device.objects.all(),
  157. required=False,
  158. query_params={
  159. 'cluster_id': '$cluster',
  160. 'site_id': '$site',
  161. },
  162. help_text=_("Optionally pin this VM to a specific host device within the cluster")
  163. )
  164. role = DynamicModelChoiceField(
  165. label=_('Role'),
  166. queryset=DeviceRole.objects.all(),
  167. required=False,
  168. query_params={
  169. "vm_role": "True"
  170. }
  171. )
  172. platform = DynamicModelChoiceField(
  173. label=_('Platform'),
  174. queryset=Platform.objects.all(),
  175. required=False,
  176. selector=True
  177. )
  178. local_context_data = JSONField(
  179. required=False,
  180. label=''
  181. )
  182. config_template = DynamicModelChoiceField(
  183. queryset=ConfigTemplate.objects.all(),
  184. required=False,
  185. label=_('Config template')
  186. )
  187. comments = CommentField()
  188. fieldsets = (
  189. FieldSet('name', 'role', 'status', 'description', 'serial', 'tags', name=_('Virtual Machine')),
  190. FieldSet('site', 'cluster', 'device', name=_('Site/Cluster')),
  191. FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
  192. FieldSet('platform', 'primary_ip4', 'primary_ip6', 'config_template', name=_('Management')),
  193. FieldSet('vcpus', 'memory', 'disk', name=_('Resources')),
  194. FieldSet('local_context_data', name=_('Config Context')),
  195. )
  196. class Meta:
  197. model = VirtualMachine
  198. fields = [
  199. 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
  200. 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'serial', 'comments', 'tags',
  201. 'local_context_data', 'config_template',
  202. ]
  203. def __init__(self, *args, **kwargs):
  204. super().__init__(*args, **kwargs)
  205. if self.instance.pk:
  206. # Disable the disk field if one or more VirtualDisks have been created
  207. if self.instance.virtualdisks.exists():
  208. self.fields['disk'].widget.attrs['disabled'] = True
  209. self.fields['disk'].help_text = _("Disk size is managed via the attachment of virtual disks.")
  210. # Compile list of choices for primary IPv4 and IPv6 addresses
  211. for family in [4, 6]:
  212. ip_choices = [(None, '---------')]
  213. # Gather PKs of all interfaces belonging to this VM
  214. interface_ids = self.instance.interfaces.values_list('pk', flat=True)
  215. # Collect interface IPs
  216. interface_ips = IPAddress.objects.filter(
  217. address__family=family,
  218. assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  219. assigned_object_id__in=interface_ids
  220. )
  221. if interface_ips:
  222. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  223. ip_choices.append(('Interface IPs', ip_list))
  224. # Collect NAT IPs
  225. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  226. address__family=family,
  227. nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  228. nat_inside__assigned_object_id__in=interface_ids
  229. )
  230. if nat_ips:
  231. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  232. ip_choices.append(('NAT IPs', ip_list))
  233. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  234. else:
  235. # An object that doesn't exist yet can't have any IPs assigned to it
  236. self.fields['primary_ip4'].choices = []
  237. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  238. self.fields['primary_ip6'].choices = []
  239. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  240. #
  241. # Virtual machine components
  242. #
  243. class VMComponentForm(NetBoxModelForm):
  244. virtual_machine = DynamicModelChoiceField(
  245. label=_('Virtual machine'),
  246. queryset=VirtualMachine.objects.all(),
  247. selector=True
  248. )
  249. def __init__(self, *args, **kwargs):
  250. super().__init__(*args, **kwargs)
  251. # Disable reassignment of VirtualMachine when editing an existing instance
  252. if self.instance.pk:
  253. self.fields['virtual_machine'].disabled = True
  254. class VMInterfaceForm(InterfaceCommonForm, VMComponentForm):
  255. parent = DynamicModelChoiceField(
  256. queryset=VMInterface.objects.all(),
  257. required=False,
  258. label=_('Parent interface'),
  259. query_params={
  260. 'virtual_machine_id': '$virtual_machine',
  261. }
  262. )
  263. bridge = DynamicModelChoiceField(
  264. queryset=VMInterface.objects.all(),
  265. required=False,
  266. label=_('Bridged interface'),
  267. query_params={
  268. 'virtual_machine_id': '$virtual_machine',
  269. }
  270. )
  271. vlan_group = DynamicModelChoiceField(
  272. queryset=VLANGroup.objects.all(),
  273. required=False,
  274. label=_('VLAN group')
  275. )
  276. untagged_vlan = DynamicModelChoiceField(
  277. queryset=VLAN.objects.all(),
  278. required=False,
  279. label=_('Untagged VLAN'),
  280. query_params={
  281. 'group_id': '$vlan_group',
  282. 'available_on_virtualmachine': '$virtual_machine',
  283. }
  284. )
  285. tagged_vlans = DynamicModelMultipleChoiceField(
  286. queryset=VLAN.objects.all(),
  287. required=False,
  288. label=_('Tagged VLANs'),
  289. query_params={
  290. 'group_id': '$vlan_group',
  291. 'available_on_virtualmachine': '$virtual_machine',
  292. }
  293. )
  294. qinq_svlan = DynamicModelChoiceField(
  295. queryset=VLAN.objects.all(),
  296. required=False,
  297. label=_('Q-in-Q Service VLAN'),
  298. query_params={
  299. 'group_id': '$vlan_group',
  300. 'available_on_virtualmachine': '$virtual_machine',
  301. 'qinq_role': VLANQinQRoleChoices.ROLE_SERVICE,
  302. }
  303. )
  304. vrf = DynamicModelChoiceField(
  305. queryset=VRF.objects.all(),
  306. required=False,
  307. label=_('VRF')
  308. )
  309. vlan_translation_policy = DynamicModelChoiceField(
  310. queryset=VLANTranslationPolicy.objects.all(),
  311. required=False,
  312. label=_('VLAN Translation Policy')
  313. )
  314. fieldsets = (
  315. FieldSet('virtual_machine', 'name', 'description', 'tags', name=_('Interface')),
  316. FieldSet('vrf', 'mac_address', name=_('Addressing')),
  317. FieldSet('mtu', 'enabled', name=_('Operation')),
  318. FieldSet('parent', 'bridge', name=_('Related Interfaces')),
  319. FieldSet(
  320. 'mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy',
  321. name=_('802.1Q Switching')
  322. ),
  323. )
  324. class Meta:
  325. model = VMInterface
  326. fields = [
  327. 'virtual_machine', 'name', 'parent', 'bridge', 'enabled', 'mac_address', 'mtu', 'description', 'mode',
  328. 'vlan_group', 'untagged_vlan', 'tagged_vlans', 'qinq_svlan', 'vlan_translation_policy', 'vrf', 'tags',
  329. ]
  330. labels = {
  331. 'mode': _('802.1Q Mode'),
  332. }
  333. widgets = {
  334. 'mode': HTMXSelect(),
  335. }
  336. class VirtualDiskForm(VMComponentForm):
  337. fieldsets = (
  338. FieldSet('virtual_machine', 'name', 'size', 'description', 'tags', name=_('Disk')),
  339. )
  340. class Meta:
  341. model = VirtualDisk
  342. fields = [
  343. 'virtual_machine', 'name', 'size', 'description', 'tags',
  344. ]