forms.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. from django import forms
  2. from django.contrib.contenttypes.models import ContentType
  3. from django.core.exceptions import ValidationError
  4. from dcim.choices import InterfaceModeChoices
  5. from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
  6. from dcim.forms import INTERFACE_MODE_HELP_TEXT
  7. from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
  8. from extras.forms import (
  9. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  10. )
  11. from extras.models import Tag
  12. from ipam.models import IPAddress, VLAN
  13. from tenancy.forms import TenancyFilterForm, TenancyForm
  14. from tenancy.models import Tenant
  15. from utilities.forms import (
  16. add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, BulkRenameForm, CommentField,
  17. ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
  18. DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
  19. StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
  20. )
  21. from .choices import *
  22. from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
  23. #
  24. # Cluster types
  25. #
  26. class ClusterTypeForm(BootstrapMixin, forms.ModelForm):
  27. slug = SlugField()
  28. class Meta:
  29. model = ClusterType
  30. fields = [
  31. 'name', 'slug', 'description',
  32. ]
  33. class ClusterTypeCSVForm(CSVModelForm):
  34. slug = SlugField()
  35. class Meta:
  36. model = ClusterType
  37. fields = ClusterType.csv_headers
  38. #
  39. # Cluster groups
  40. #
  41. class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
  42. slug = SlugField()
  43. class Meta:
  44. model = ClusterGroup
  45. fields = [
  46. 'name', 'slug', 'description',
  47. ]
  48. class ClusterGroupCSVForm(CSVModelForm):
  49. slug = SlugField()
  50. class Meta:
  51. model = ClusterGroup
  52. fields = ClusterGroup.csv_headers
  53. #
  54. # Clusters
  55. #
  56. class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  57. type = DynamicModelChoiceField(
  58. queryset=ClusterType.objects.all()
  59. )
  60. group = DynamicModelChoiceField(
  61. queryset=ClusterGroup.objects.all(),
  62. required=False
  63. )
  64. site = DynamicModelChoiceField(
  65. queryset=Site.objects.all(),
  66. required=False
  67. )
  68. comments = CommentField()
  69. tags = DynamicModelMultipleChoiceField(
  70. queryset=Tag.objects.all(),
  71. required=False
  72. )
  73. class Meta:
  74. model = Cluster
  75. fields = (
  76. 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
  77. )
  78. class ClusterCSVForm(CustomFieldModelCSVForm):
  79. type = CSVModelChoiceField(
  80. queryset=ClusterType.objects.all(),
  81. to_field_name='name',
  82. help_text='Type of cluster'
  83. )
  84. group = CSVModelChoiceField(
  85. queryset=ClusterGroup.objects.all(),
  86. to_field_name='name',
  87. required=False,
  88. help_text='Assigned cluster group'
  89. )
  90. site = CSVModelChoiceField(
  91. queryset=Site.objects.all(),
  92. to_field_name='name',
  93. required=False,
  94. help_text='Assigned site'
  95. )
  96. tenant = CSVModelChoiceField(
  97. queryset=Tenant.objects.all(),
  98. to_field_name='name',
  99. required=False,
  100. help_text='Assigned tenant'
  101. )
  102. class Meta:
  103. model = Cluster
  104. fields = Cluster.csv_headers
  105. class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  106. pk = forms.ModelMultipleChoiceField(
  107. queryset=Cluster.objects.all(),
  108. widget=forms.MultipleHiddenInput()
  109. )
  110. type = DynamicModelChoiceField(
  111. queryset=ClusterType.objects.all(),
  112. required=False
  113. )
  114. group = DynamicModelChoiceField(
  115. queryset=ClusterGroup.objects.all(),
  116. required=False
  117. )
  118. tenant = DynamicModelChoiceField(
  119. queryset=Tenant.objects.all(),
  120. required=False
  121. )
  122. site = DynamicModelChoiceField(
  123. queryset=Site.objects.all(),
  124. required=False
  125. )
  126. comments = CommentField(
  127. widget=SmallTextarea,
  128. label='Comments'
  129. )
  130. class Meta:
  131. nullable_fields = [
  132. 'group', 'site', 'comments', 'tenant',
  133. ]
  134. class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  135. model = Cluster
  136. field_order = [
  137. 'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
  138. ]
  139. q = forms.CharField(required=False, label='Search')
  140. type = DynamicModelMultipleChoiceField(
  141. queryset=ClusterType.objects.all(),
  142. to_field_name='slug',
  143. required=False
  144. )
  145. region = DynamicModelMultipleChoiceField(
  146. queryset=Region.objects.all(),
  147. to_field_name='slug',
  148. required=False
  149. )
  150. site = DynamicModelMultipleChoiceField(
  151. queryset=Site.objects.all(),
  152. to_field_name='slug',
  153. required=False,
  154. null_option='None',
  155. query_params={
  156. 'region': '$region'
  157. }
  158. )
  159. group = DynamicModelMultipleChoiceField(
  160. queryset=ClusterGroup.objects.all(),
  161. to_field_name='slug',
  162. required=False,
  163. null_option='None'
  164. )
  165. tag = TagFilterField(model)
  166. class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
  167. region = DynamicModelChoiceField(
  168. queryset=Region.objects.all(),
  169. required=False,
  170. null_option='None'
  171. )
  172. site = DynamicModelChoiceField(
  173. queryset=Site.objects.all(),
  174. required=False,
  175. query_params={
  176. 'region_id': '$region'
  177. }
  178. )
  179. rack = DynamicModelChoiceField(
  180. queryset=Rack.objects.all(),
  181. required=False,
  182. null_option='None',
  183. display_field='display_name',
  184. query_params={
  185. 'site_id': '$site'
  186. }
  187. )
  188. devices = DynamicModelMultipleChoiceField(
  189. queryset=Device.objects.all(),
  190. display_field='display_name',
  191. query_params={
  192. 'site_id': '$site',
  193. 'rack_id': '$rack',
  194. 'cluster_id': 'null',
  195. }
  196. )
  197. class Meta:
  198. fields = [
  199. 'region', 'site', 'rack', 'devices',
  200. ]
  201. def __init__(self, cluster, *args, **kwargs):
  202. self.cluster = cluster
  203. super().__init__(*args, **kwargs)
  204. self.fields['devices'].choices = []
  205. def clean(self):
  206. super().clean()
  207. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  208. if self.cluster.site is not None:
  209. for device in self.cleaned_data.get('devices', []):
  210. if device.site != self.cluster.site:
  211. raise ValidationError({
  212. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  213. device, device.site, self.cluster.site
  214. )
  215. })
  216. class ClusterRemoveDevicesForm(ConfirmationForm):
  217. pk = forms.ModelMultipleChoiceField(
  218. queryset=Device.objects.all(),
  219. widget=forms.MultipleHiddenInput()
  220. )
  221. #
  222. # Virtual Machines
  223. #
  224. class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  225. cluster_group = DynamicModelChoiceField(
  226. queryset=ClusterGroup.objects.all(),
  227. required=False,
  228. null_option='None'
  229. )
  230. cluster = DynamicModelChoiceField(
  231. queryset=Cluster.objects.all(),
  232. query_params={
  233. 'group_id': '$cluster_group'
  234. }
  235. )
  236. role = DynamicModelChoiceField(
  237. queryset=DeviceRole.objects.all(),
  238. required=False,
  239. query_params={
  240. "vm_role": "True"
  241. }
  242. )
  243. platform = DynamicModelChoiceField(
  244. queryset=Platform.objects.all(),
  245. required=False
  246. )
  247. local_context_data = JSONField(
  248. required=False,
  249. label=''
  250. )
  251. tags = DynamicModelMultipleChoiceField(
  252. queryset=Tag.objects.all(),
  253. required=False
  254. )
  255. class Meta:
  256. model = VirtualMachine
  257. fields = [
  258. 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
  259. 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
  260. ]
  261. help_texts = {
  262. 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
  263. "config context",
  264. }
  265. widgets = {
  266. "status": StaticSelect2(),
  267. 'primary_ip4': StaticSelect2(),
  268. 'primary_ip6': StaticSelect2(),
  269. }
  270. def __init__(self, *args, **kwargs):
  271. # Initialize helper selector
  272. instance = kwargs.get('instance')
  273. if instance.pk and instance.cluster is not None:
  274. initial = kwargs.get('initial', {}).copy()
  275. initial['cluster_group'] = instance.cluster.group
  276. kwargs['initial'] = initial
  277. super().__init__(*args, **kwargs)
  278. if self.instance.pk:
  279. # Compile list of choices for primary IPv4 and IPv6 addresses
  280. for family in [4, 6]:
  281. ip_choices = [(None, '---------')]
  282. # Gather PKs of all interfaces belonging to this VM
  283. interface_ids = self.instance.interfaces.values_list('pk', flat=True)
  284. # Collect interface IPs
  285. interface_ips = IPAddress.objects.filter(
  286. address__family=family,
  287. assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  288. assigned_object_id__in=interface_ids
  289. )
  290. if interface_ips:
  291. ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
  292. ip_choices.append(('Interface IPs', ip_list))
  293. # Collect NAT IPs
  294. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  295. address__family=family,
  296. nat_inside__assigned_object_type=ContentType.objects.get_for_model(VMInterface),
  297. nat_inside__assigned_object_id__in=interface_ids
  298. )
  299. if nat_ips:
  300. ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
  301. ip_choices.append(('NAT IPs', ip_list))
  302. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  303. else:
  304. # An object that doesn't exist yet can't have any IPs assigned to it
  305. self.fields['primary_ip4'].choices = []
  306. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  307. self.fields['primary_ip6'].choices = []
  308. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  309. class VirtualMachineCSVForm(CustomFieldModelCSVForm):
  310. status = CSVChoiceField(
  311. choices=VirtualMachineStatusChoices,
  312. required=False,
  313. help_text='Operational status of device'
  314. )
  315. cluster = CSVModelChoiceField(
  316. queryset=Cluster.objects.all(),
  317. to_field_name='name',
  318. help_text='Assigned cluster'
  319. )
  320. role = CSVModelChoiceField(
  321. queryset=DeviceRole.objects.filter(
  322. vm_role=True
  323. ),
  324. required=False,
  325. to_field_name='name',
  326. help_text='Functional role'
  327. )
  328. tenant = CSVModelChoiceField(
  329. queryset=Tenant.objects.all(),
  330. required=False,
  331. to_field_name='name',
  332. help_text='Assigned tenant'
  333. )
  334. platform = CSVModelChoiceField(
  335. queryset=Platform.objects.all(),
  336. required=False,
  337. to_field_name='name',
  338. help_text='Assigned platform'
  339. )
  340. class Meta:
  341. model = VirtualMachine
  342. fields = VirtualMachine.csv_headers
  343. class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  344. pk = forms.ModelMultipleChoiceField(
  345. queryset=VirtualMachine.objects.all(),
  346. widget=forms.MultipleHiddenInput()
  347. )
  348. status = forms.ChoiceField(
  349. choices=add_blank_choice(VirtualMachineStatusChoices),
  350. required=False,
  351. initial='',
  352. widget=StaticSelect2(),
  353. )
  354. cluster = DynamicModelChoiceField(
  355. queryset=Cluster.objects.all(),
  356. required=False
  357. )
  358. role = DynamicModelChoiceField(
  359. queryset=DeviceRole.objects.filter(
  360. vm_role=True
  361. ),
  362. required=False,
  363. query_params={
  364. "vm_role": "True"
  365. }
  366. )
  367. tenant = DynamicModelChoiceField(
  368. queryset=Tenant.objects.all(),
  369. required=False
  370. )
  371. platform = DynamicModelChoiceField(
  372. queryset=Platform.objects.all(),
  373. required=False
  374. )
  375. vcpus = forms.IntegerField(
  376. required=False,
  377. label='vCPUs'
  378. )
  379. memory = forms.IntegerField(
  380. required=False,
  381. label='Memory (MB)'
  382. )
  383. disk = forms.IntegerField(
  384. required=False,
  385. label='Disk (GB)'
  386. )
  387. comments = CommentField(
  388. widget=SmallTextarea,
  389. label='Comments'
  390. )
  391. class Meta:
  392. nullable_fields = [
  393. 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
  394. ]
  395. class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  396. model = VirtualMachine
  397. field_order = [
  398. 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group',
  399. 'tenant', 'platform', 'mac_address',
  400. ]
  401. q = forms.CharField(
  402. required=False,
  403. label='Search'
  404. )
  405. cluster_group = DynamicModelMultipleChoiceField(
  406. queryset=ClusterGroup.objects.all(),
  407. to_field_name='slug',
  408. required=False,
  409. null_option='None'
  410. )
  411. cluster_type = DynamicModelMultipleChoiceField(
  412. queryset=ClusterType.objects.all(),
  413. to_field_name='slug',
  414. required=False,
  415. null_option='None'
  416. )
  417. cluster_id = DynamicModelMultipleChoiceField(
  418. queryset=Cluster.objects.all(),
  419. required=False,
  420. label='Cluster'
  421. )
  422. region = DynamicModelMultipleChoiceField(
  423. queryset=Region.objects.all(),
  424. to_field_name='slug',
  425. required=False
  426. )
  427. site = DynamicModelMultipleChoiceField(
  428. queryset=Site.objects.all(),
  429. to_field_name='slug',
  430. required=False,
  431. null_option='None',
  432. query_params={
  433. 'region': '$region'
  434. }
  435. )
  436. role = DynamicModelMultipleChoiceField(
  437. queryset=DeviceRole.objects.filter(vm_role=True),
  438. to_field_name='slug',
  439. required=False,
  440. null_option='None',
  441. query_params={
  442. 'vm_role': "True"
  443. }
  444. )
  445. status = forms.MultipleChoiceField(
  446. choices=VirtualMachineStatusChoices,
  447. required=False,
  448. widget=StaticSelect2Multiple()
  449. )
  450. platform = DynamicModelMultipleChoiceField(
  451. queryset=Platform.objects.all(),
  452. to_field_name='slug',
  453. required=False,
  454. null_option='None'
  455. )
  456. mac_address = forms.CharField(
  457. required=False,
  458. label='MAC address'
  459. )
  460. tag = TagFilterField(model)
  461. #
  462. # VM interfaces
  463. #
  464. class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
  465. untagged_vlan = DynamicModelChoiceField(
  466. queryset=VLAN.objects.all(),
  467. required=False,
  468. display_field='display_name',
  469. brief_mode=False,
  470. query_params={
  471. 'site_id': 'null',
  472. }
  473. )
  474. tagged_vlans = DynamicModelMultipleChoiceField(
  475. queryset=VLAN.objects.all(),
  476. required=False,
  477. display_field='display_name',
  478. brief_mode=False,
  479. query_params={
  480. 'site_id': 'null',
  481. }
  482. )
  483. tags = DynamicModelMultipleChoiceField(
  484. queryset=Tag.objects.all(),
  485. required=False
  486. )
  487. class Meta:
  488. model = VMInterface
  489. fields = [
  490. 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan',
  491. 'tagged_vlans',
  492. ]
  493. widgets = {
  494. 'virtual_machine': forms.HiddenInput(),
  495. 'mode': StaticSelect2()
  496. }
  497. labels = {
  498. 'mode': '802.1Q Mode',
  499. }
  500. help_texts = {
  501. 'mode': INTERFACE_MODE_HELP_TEXT,
  502. }
  503. def __init__(self, *args, **kwargs):
  504. super().__init__(*args, **kwargs)
  505. virtual_machine = VirtualMachine.objects.get(
  506. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  507. )
  508. # Add current site to VLANs query params
  509. site = virtual_machine.site
  510. if site:
  511. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  512. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  513. def clean(self):
  514. super().clean()
  515. # Validate VLAN assignments
  516. tagged_vlans = self.cleaned_data['tagged_vlans']
  517. # Untagged interfaces cannot be assigned tagged VLANs
  518. if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
  519. raise forms.ValidationError({
  520. 'mode': "An access interface cannot have tagged VLANs assigned."
  521. })
  522. # Remove all tagged VLAN assignments from "tagged all" interfaces
  523. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
  524. self.cleaned_data['tagged_vlans'] = []
  525. class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
  526. virtual_machine = DynamicModelChoiceField(
  527. queryset=VirtualMachine.objects.all()
  528. )
  529. name_pattern = ExpandableNameField(
  530. label='Name'
  531. )
  532. enabled = forms.BooleanField(
  533. required=False,
  534. initial=True
  535. )
  536. mtu = forms.IntegerField(
  537. required=False,
  538. min_value=INTERFACE_MTU_MIN,
  539. max_value=INTERFACE_MTU_MAX,
  540. label='MTU'
  541. )
  542. mac_address = forms.CharField(
  543. required=False,
  544. label='MAC Address'
  545. )
  546. description = forms.CharField(
  547. max_length=100,
  548. required=False
  549. )
  550. mode = forms.ChoiceField(
  551. choices=add_blank_choice(InterfaceModeChoices),
  552. required=False,
  553. widget=StaticSelect2(),
  554. )
  555. untagged_vlan = DynamicModelChoiceField(
  556. queryset=VLAN.objects.all(),
  557. required=False,
  558. display_field='display_name',
  559. brief_mode=False,
  560. query_params={
  561. 'site_id': 'null',
  562. }
  563. )
  564. tagged_vlans = DynamicModelMultipleChoiceField(
  565. queryset=VLAN.objects.all(),
  566. required=False,
  567. display_field='display_name',
  568. brief_mode=False,
  569. query_params={
  570. 'site_id': 'null',
  571. }
  572. )
  573. tags = DynamicModelMultipleChoiceField(
  574. queryset=Tag.objects.all(),
  575. required=False
  576. )
  577. def __init__(self, *args, **kwargs):
  578. super().__init__(*args, **kwargs)
  579. virtual_machine = VirtualMachine.objects.get(
  580. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  581. )
  582. # Add current site to VLANs query params
  583. site = virtual_machine.site
  584. if site:
  585. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  586. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  587. class VMInterfaceCSVForm(CSVModelForm):
  588. virtual_machine = CSVModelChoiceField(
  589. queryset=VirtualMachine.objects.all(),
  590. to_field_name='name'
  591. )
  592. mode = CSVChoiceField(
  593. choices=InterfaceModeChoices,
  594. required=False,
  595. help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
  596. )
  597. class Meta:
  598. model = VMInterface
  599. fields = VMInterface.csv_headers
  600. def clean_enabled(self):
  601. # Make sure enabled is True when it's not included in the uploaded data
  602. if 'enabled' not in self.data:
  603. return True
  604. else:
  605. return self.cleaned_data['enabled']
  606. class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  607. pk = forms.ModelMultipleChoiceField(
  608. queryset=VMInterface.objects.all(),
  609. widget=forms.MultipleHiddenInput()
  610. )
  611. virtual_machine = forms.ModelChoiceField(
  612. queryset=VirtualMachine.objects.all(),
  613. required=False,
  614. disabled=True,
  615. widget=forms.HiddenInput()
  616. )
  617. enabled = forms.NullBooleanField(
  618. required=False,
  619. widget=BulkEditNullBooleanSelect()
  620. )
  621. mtu = forms.IntegerField(
  622. required=False,
  623. min_value=INTERFACE_MTU_MIN,
  624. max_value=INTERFACE_MTU_MAX,
  625. label='MTU'
  626. )
  627. description = forms.CharField(
  628. max_length=100,
  629. required=False
  630. )
  631. mode = forms.ChoiceField(
  632. choices=add_blank_choice(InterfaceModeChoices),
  633. required=False,
  634. widget=StaticSelect2()
  635. )
  636. untagged_vlan = DynamicModelChoiceField(
  637. queryset=VLAN.objects.all(),
  638. required=False,
  639. display_field='display_name',
  640. brief_mode=False,
  641. query_params={
  642. 'site_id': 'null',
  643. }
  644. )
  645. tagged_vlans = DynamicModelMultipleChoiceField(
  646. queryset=VLAN.objects.all(),
  647. required=False,
  648. display_field='display_name',
  649. brief_mode=False,
  650. query_params={
  651. 'site_id': 'null',
  652. }
  653. )
  654. class Meta:
  655. nullable_fields = [
  656. 'mtu', 'description',
  657. ]
  658. def __init__(self, *args, **kwargs):
  659. super().__init__(*args, **kwargs)
  660. # Limit available VLANs based on the parent VirtualMachine
  661. if 'virtual_machine' in self.initial:
  662. parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
  663. site = getattr(parent_obj.cluster, 'site', None)
  664. if site is not None:
  665. # Add current site to VLANs query params
  666. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  667. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  668. class VMInterfaceBulkRenameForm(BulkRenameForm):
  669. pk = forms.ModelMultipleChoiceField(
  670. queryset=VMInterface.objects.all(),
  671. widget=forms.MultipleHiddenInput()
  672. )
  673. class VMInterfaceFilterForm(forms.Form):
  674. model = VMInterface
  675. cluster_id = DynamicModelMultipleChoiceField(
  676. queryset=Cluster.objects.all(),
  677. required=False,
  678. label='Cluster'
  679. )
  680. virtual_machine_id = DynamicModelMultipleChoiceField(
  681. queryset=VirtualMachine.objects.all(),
  682. required=False,
  683. label='Virtual machine',
  684. query_params={
  685. 'cluster_id': '$cluster_id'
  686. }
  687. )
  688. enabled = forms.NullBooleanField(
  689. required=False,
  690. widget=StaticSelect2(
  691. choices=BOOLEAN_WITH_BLANK_CHOICES
  692. )
  693. )
  694. tag = TagFilterField(model)
  695. #
  696. # Bulk VirtualMachine component creation
  697. #
  698. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  699. pk = forms.ModelMultipleChoiceField(
  700. queryset=VirtualMachine.objects.all(),
  701. widget=forms.MultipleHiddenInput()
  702. )
  703. name_pattern = ExpandableNameField(
  704. label='Name'
  705. )
  706. def clean_tags(self):
  707. # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
  708. # must first convert the list of tags to a string.
  709. return ','.join(self.cleaned_data.get('tags'))
  710. class VMInterfaceBulkCreateForm(
  711. form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
  712. VirtualMachineBulkAddComponentForm
  713. ):
  714. pass