forms.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807
  1. from django import forms
  2. from django.core.exceptions import ValidationError
  3. from dcim.choices import InterfaceModeChoices
  4. from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
  5. from dcim.forms import INTERFACE_MODE_HELP_TEXT
  6. from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site
  7. from extras.forms import (
  8. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  9. )
  10. from extras.models import Tag
  11. from ipam.models import IPAddress, VLAN
  12. from tenancy.forms import TenancyFilterForm, TenancyForm
  13. from tenancy.models import Tenant
  14. from utilities.forms import (
  15. add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, BulkRenameForm, CommentField,
  16. ConfirmationForm, CSVChoiceField, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField,
  17. DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model, JSONField, SlugField, SmallTextarea,
  18. StaticSelect2, StaticSelect2Multiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
  19. )
  20. from .choices import *
  21. from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
  22. #
  23. # Cluster types
  24. #
  25. class ClusterTypeForm(BootstrapMixin, forms.ModelForm):
  26. slug = SlugField()
  27. class Meta:
  28. model = ClusterType
  29. fields = [
  30. 'name', 'slug', 'description',
  31. ]
  32. class ClusterTypeCSVForm(CSVModelForm):
  33. slug = SlugField()
  34. class Meta:
  35. model = ClusterType
  36. fields = ClusterType.csv_headers
  37. #
  38. # Cluster groups
  39. #
  40. class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
  41. slug = SlugField()
  42. class Meta:
  43. model = ClusterGroup
  44. fields = [
  45. 'name', 'slug', 'description',
  46. ]
  47. class ClusterGroupCSVForm(CSVModelForm):
  48. slug = SlugField()
  49. class Meta:
  50. model = ClusterGroup
  51. fields = ClusterGroup.csv_headers
  52. #
  53. # Clusters
  54. #
  55. class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  56. type = DynamicModelChoiceField(
  57. queryset=ClusterType.objects.all()
  58. )
  59. group = DynamicModelChoiceField(
  60. queryset=ClusterGroup.objects.all(),
  61. required=False
  62. )
  63. site = DynamicModelChoiceField(
  64. queryset=Site.objects.all(),
  65. required=False
  66. )
  67. comments = CommentField()
  68. tags = DynamicModelMultipleChoiceField(
  69. queryset=Tag.objects.all(),
  70. required=False
  71. )
  72. class Meta:
  73. model = Cluster
  74. fields = (
  75. 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
  76. )
  77. class ClusterCSVForm(CustomFieldModelCSVForm):
  78. type = CSVModelChoiceField(
  79. queryset=ClusterType.objects.all(),
  80. to_field_name='name',
  81. help_text='Type of cluster'
  82. )
  83. group = CSVModelChoiceField(
  84. queryset=ClusterGroup.objects.all(),
  85. to_field_name='name',
  86. required=False,
  87. help_text='Assigned cluster group'
  88. )
  89. site = CSVModelChoiceField(
  90. queryset=Site.objects.all(),
  91. to_field_name='name',
  92. required=False,
  93. help_text='Assigned site'
  94. )
  95. tenant = CSVModelChoiceField(
  96. queryset=Tenant.objects.all(),
  97. to_field_name='name',
  98. required=False,
  99. help_text='Assigned tenant'
  100. )
  101. class Meta:
  102. model = Cluster
  103. fields = Cluster.csv_headers
  104. class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  105. pk = forms.ModelMultipleChoiceField(
  106. queryset=Cluster.objects.all(),
  107. widget=forms.MultipleHiddenInput()
  108. )
  109. type = DynamicModelChoiceField(
  110. queryset=ClusterType.objects.all(),
  111. required=False
  112. )
  113. group = DynamicModelChoiceField(
  114. queryset=ClusterGroup.objects.all(),
  115. required=False
  116. )
  117. tenant = DynamicModelChoiceField(
  118. queryset=Tenant.objects.all(),
  119. required=False
  120. )
  121. site = DynamicModelChoiceField(
  122. queryset=Site.objects.all(),
  123. required=False
  124. )
  125. comments = CommentField(
  126. widget=SmallTextarea,
  127. label='Comments'
  128. )
  129. class Meta:
  130. nullable_fields = [
  131. 'group', 'site', 'comments', 'tenant',
  132. ]
  133. class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  134. model = Cluster
  135. field_order = [
  136. 'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
  137. ]
  138. q = forms.CharField(required=False, label='Search')
  139. type = DynamicModelMultipleChoiceField(
  140. queryset=ClusterType.objects.all(),
  141. to_field_name='slug',
  142. required=False
  143. )
  144. region = DynamicModelMultipleChoiceField(
  145. queryset=Region.objects.all(),
  146. to_field_name='slug',
  147. required=False
  148. )
  149. site = DynamicModelMultipleChoiceField(
  150. queryset=Site.objects.all(),
  151. to_field_name='slug',
  152. required=False,
  153. null_option='None',
  154. query_params={
  155. 'region': '$region'
  156. }
  157. )
  158. group = DynamicModelMultipleChoiceField(
  159. queryset=ClusterGroup.objects.all(),
  160. to_field_name='slug',
  161. required=False,
  162. null_option='None'
  163. )
  164. tag = TagFilterField(model)
  165. class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
  166. region = DynamicModelChoiceField(
  167. queryset=Region.objects.all(),
  168. required=False,
  169. null_option='None'
  170. )
  171. site = DynamicModelChoiceField(
  172. queryset=Site.objects.all(),
  173. required=False,
  174. query_params={
  175. 'region_id': '$region'
  176. }
  177. )
  178. rack = DynamicModelChoiceField(
  179. queryset=Rack.objects.all(),
  180. required=False,
  181. null_option='None',
  182. query_params={
  183. 'site_id': '$site'
  184. }
  185. )
  186. devices = DynamicModelMultipleChoiceField(
  187. queryset=Device.objects.all(),
  188. display_field='display_name',
  189. query_params={
  190. 'site_id': '$site',
  191. 'rack_id': '$rack',
  192. 'cluster_id': 'null',
  193. }
  194. )
  195. class Meta:
  196. fields = [
  197. 'region', 'site', 'rack', 'devices',
  198. ]
  199. def __init__(self, cluster, *args, **kwargs):
  200. self.cluster = cluster
  201. super().__init__(*args, **kwargs)
  202. self.fields['devices'].choices = []
  203. def clean(self):
  204. super().clean()
  205. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  206. if self.cluster.site is not None:
  207. for device in self.cleaned_data.get('devices', []):
  208. if device.site != self.cluster.site:
  209. raise ValidationError({
  210. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  211. device, device.site, self.cluster.site
  212. )
  213. })
  214. class ClusterRemoveDevicesForm(ConfirmationForm):
  215. pk = forms.ModelMultipleChoiceField(
  216. queryset=Device.objects.all(),
  217. widget=forms.MultipleHiddenInput()
  218. )
  219. #
  220. # Virtual Machines
  221. #
  222. class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  223. cluster_group = DynamicModelChoiceField(
  224. queryset=ClusterGroup.objects.all(),
  225. required=False,
  226. null_option='None'
  227. )
  228. cluster = DynamicModelChoiceField(
  229. queryset=Cluster.objects.all(),
  230. query_params={
  231. 'group_id': '$cluster_group'
  232. }
  233. )
  234. role = DynamicModelChoiceField(
  235. queryset=DeviceRole.objects.all(),
  236. required=False,
  237. query_params={
  238. "vm_role": "True"
  239. }
  240. )
  241. platform = DynamicModelChoiceField(
  242. queryset=Platform.objects.all(),
  243. required=False
  244. )
  245. local_context_data = JSONField(
  246. required=False,
  247. label=''
  248. )
  249. tags = DynamicModelMultipleChoiceField(
  250. queryset=Tag.objects.all(),
  251. required=False
  252. )
  253. class Meta:
  254. model = VirtualMachine
  255. fields = [
  256. 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
  257. 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
  258. ]
  259. help_texts = {
  260. 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
  261. "config context",
  262. }
  263. widgets = {
  264. "status": StaticSelect2(),
  265. 'primary_ip4': StaticSelect2(),
  266. 'primary_ip6': StaticSelect2(),
  267. }
  268. def __init__(self, *args, **kwargs):
  269. # Initialize helper selector
  270. instance = kwargs.get('instance')
  271. if instance.pk and instance.cluster is not None:
  272. initial = kwargs.get('initial', {}).copy()
  273. initial['cluster_group'] = instance.cluster.group
  274. kwargs['initial'] = initial
  275. super().__init__(*args, **kwargs)
  276. if self.instance.pk:
  277. # Compile list of choices for primary IPv4 and IPv6 addresses
  278. for family in [4, 6]:
  279. ip_choices = [(None, '---------')]
  280. # Collect interface IPs
  281. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  282. address__family=family,
  283. vminterface__in=self.instance.interfaces.values_list('id', flat=True)
  284. )
  285. if interface_ips:
  286. ip_choices.append(
  287. ('Interface IPs', [
  288. (ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
  289. ])
  290. )
  291. # Collect NAT IPs
  292. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  293. address__family=family,
  294. nat_inside__vminterface__in=self.instance.interfaces.values_list('id', flat=True)
  295. )
  296. if nat_ips:
  297. ip_choices.append(
  298. ('NAT IPs', [
  299. (ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
  300. ])
  301. )
  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, 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