model_forms.py 12 KB

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