model_forms.py 13 KB

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