forms.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847
  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, InterfaceCommonForm
  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, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
  16. CommentField, 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. widget=APISelectMultiple(
  144. value_field='slug',
  145. )
  146. )
  147. region = DynamicModelMultipleChoiceField(
  148. queryset=Region.objects.all(),
  149. to_field_name='slug',
  150. required=False,
  151. widget=APISelectMultiple(
  152. value_field="slug",
  153. filter_for={
  154. 'site': 'region'
  155. }
  156. )
  157. )
  158. site = DynamicModelMultipleChoiceField(
  159. queryset=Site.objects.all(),
  160. to_field_name='slug',
  161. required=False,
  162. widget=APISelectMultiple(
  163. value_field='slug',
  164. null_option=True,
  165. )
  166. )
  167. group = DynamicModelMultipleChoiceField(
  168. queryset=ClusterGroup.objects.all(),
  169. to_field_name='slug',
  170. required=False,
  171. widget=APISelectMultiple(
  172. value_field='slug',
  173. null_option=True,
  174. )
  175. )
  176. tag = TagFilterField(model)
  177. class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
  178. region = DynamicModelChoiceField(
  179. queryset=Region.objects.all(),
  180. required=False,
  181. widget=APISelect(
  182. filter_for={
  183. "site": "region_id",
  184. },
  185. attrs={
  186. 'nullable': 'true',
  187. }
  188. )
  189. )
  190. site = DynamicModelChoiceField(
  191. queryset=Site.objects.all(),
  192. required=False,
  193. widget=APISelect(
  194. filter_for={
  195. "rack": "site_id",
  196. "devices": "site_id",
  197. }
  198. )
  199. )
  200. rack = DynamicModelChoiceField(
  201. queryset=Rack.objects.all(),
  202. required=False,
  203. widget=APISelect(
  204. filter_for={
  205. "devices": "rack_id"
  206. },
  207. attrs={
  208. 'nullable': 'true',
  209. }
  210. )
  211. )
  212. devices = DynamicModelMultipleChoiceField(
  213. queryset=Device.objects.filter(cluster__isnull=True),
  214. widget=APISelectMultiple(
  215. display_field='display_name',
  216. disabled_indicator='cluster'
  217. )
  218. )
  219. class Meta:
  220. fields = [
  221. 'region', 'site', 'rack', 'devices',
  222. ]
  223. def __init__(self, cluster, *args, **kwargs):
  224. self.cluster = cluster
  225. super().__init__(*args, **kwargs)
  226. self.fields['devices'].choices = []
  227. def clean(self):
  228. super().clean()
  229. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  230. if self.cluster.site is not None:
  231. for device in self.cleaned_data.get('devices', []):
  232. if device.site != self.cluster.site:
  233. raise ValidationError({
  234. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  235. device, device.site, self.cluster.site
  236. )
  237. })
  238. class ClusterRemoveDevicesForm(ConfirmationForm):
  239. pk = forms.ModelMultipleChoiceField(
  240. queryset=Device.objects.all(),
  241. widget=forms.MultipleHiddenInput()
  242. )
  243. #
  244. # Virtual Machines
  245. #
  246. class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  247. cluster_group = DynamicModelChoiceField(
  248. queryset=ClusterGroup.objects.all(),
  249. required=False,
  250. widget=APISelect(
  251. filter_for={
  252. "cluster": "group_id",
  253. },
  254. attrs={
  255. 'nullable': 'true',
  256. }
  257. )
  258. )
  259. cluster = DynamicModelChoiceField(
  260. queryset=Cluster.objects.all()
  261. )
  262. role = DynamicModelChoiceField(
  263. queryset=DeviceRole.objects.all(),
  264. required=False,
  265. widget=APISelect(
  266. additional_query_params={
  267. "vm_role": "True"
  268. }
  269. )
  270. )
  271. platform = DynamicModelChoiceField(
  272. queryset=Platform.objects.all(),
  273. required=False
  274. )
  275. local_context_data = JSONField(
  276. required=False,
  277. label=''
  278. )
  279. tags = DynamicModelMultipleChoiceField(
  280. queryset=Tag.objects.all(),
  281. required=False
  282. )
  283. class Meta:
  284. model = VirtualMachine
  285. fields = [
  286. 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
  287. 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
  288. ]
  289. help_texts = {
  290. 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
  291. "config context",
  292. }
  293. widgets = {
  294. "status": StaticSelect2(),
  295. 'primary_ip4': StaticSelect2(),
  296. 'primary_ip6': StaticSelect2(),
  297. }
  298. def __init__(self, *args, **kwargs):
  299. # Initialize helper selector
  300. instance = kwargs.get('instance')
  301. if instance.pk and instance.cluster is not None:
  302. initial = kwargs.get('initial', {}).copy()
  303. initial['cluster_group'] = instance.cluster.group
  304. kwargs['initial'] = initial
  305. super().__init__(*args, **kwargs)
  306. if self.instance.pk:
  307. # Compile list of choices for primary IPv4 and IPv6 addresses
  308. for family in [4, 6]:
  309. ip_choices = [(None, '---------')]
  310. # Collect interface IPs
  311. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  312. address__family=family,
  313. vm_interface__in=self.instance.interfaces.values_list('id', flat=True)
  314. )
  315. if interface_ips:
  316. ip_choices.append(
  317. ('Interface IPs', [
  318. (ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
  319. ])
  320. )
  321. # Collect NAT IPs
  322. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  323. address__family=family,
  324. nat_inside__vm_interface__in=self.instance.interfaces.values_list('id', flat=True)
  325. )
  326. if nat_ips:
  327. ip_choices.append(
  328. ('NAT IPs', [
  329. (ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
  330. ])
  331. )
  332. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  333. else:
  334. # An object that doesn't exist yet can't have any IPs assigned to it
  335. self.fields['primary_ip4'].choices = []
  336. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  337. self.fields['primary_ip6'].choices = []
  338. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  339. class VirtualMachineCSVForm(CustomFieldModelCSVForm):
  340. status = CSVChoiceField(
  341. choices=VirtualMachineStatusChoices,
  342. required=False,
  343. help_text='Operational status of device'
  344. )
  345. cluster = CSVModelChoiceField(
  346. queryset=Cluster.objects.all(),
  347. to_field_name='name',
  348. help_text='Assigned cluster'
  349. )
  350. role = CSVModelChoiceField(
  351. queryset=DeviceRole.objects.filter(
  352. vm_role=True
  353. ),
  354. required=False,
  355. to_field_name='name',
  356. help_text='Functional role'
  357. )
  358. tenant = CSVModelChoiceField(
  359. queryset=Tenant.objects.all(),
  360. required=False,
  361. to_field_name='name',
  362. help_text='Assigned tenant'
  363. )
  364. platform = CSVModelChoiceField(
  365. queryset=Platform.objects.all(),
  366. required=False,
  367. to_field_name='name',
  368. help_text='Assigned platform'
  369. )
  370. class Meta:
  371. model = VirtualMachine
  372. fields = VirtualMachine.csv_headers
  373. class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  374. pk = forms.ModelMultipleChoiceField(
  375. queryset=VirtualMachine.objects.all(),
  376. widget=forms.MultipleHiddenInput()
  377. )
  378. status = forms.ChoiceField(
  379. choices=add_blank_choice(VirtualMachineStatusChoices),
  380. required=False,
  381. initial='',
  382. widget=StaticSelect2(),
  383. )
  384. cluster = DynamicModelChoiceField(
  385. queryset=Cluster.objects.all(),
  386. required=False
  387. )
  388. role = DynamicModelChoiceField(
  389. queryset=DeviceRole.objects.filter(
  390. vm_role=True
  391. ),
  392. required=False,
  393. widget=APISelect(
  394. additional_query_params={
  395. "vm_role": "True"
  396. }
  397. )
  398. )
  399. tenant = DynamicModelChoiceField(
  400. queryset=Tenant.objects.all(),
  401. required=False
  402. )
  403. platform = DynamicModelChoiceField(
  404. queryset=Platform.objects.all(),
  405. required=False
  406. )
  407. vcpus = forms.IntegerField(
  408. required=False,
  409. label='vCPUs'
  410. )
  411. memory = forms.IntegerField(
  412. required=False,
  413. label='Memory (MB)'
  414. )
  415. disk = forms.IntegerField(
  416. required=False,
  417. label='Disk (GB)'
  418. )
  419. comments = CommentField(
  420. widget=SmallTextarea,
  421. label='Comments'
  422. )
  423. class Meta:
  424. nullable_fields = [
  425. 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
  426. ]
  427. class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  428. model = VirtualMachine
  429. field_order = [
  430. 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group',
  431. 'tenant', 'platform', 'mac_address',
  432. ]
  433. q = forms.CharField(
  434. required=False,
  435. label='Search'
  436. )
  437. cluster_group = DynamicModelMultipleChoiceField(
  438. queryset=ClusterGroup.objects.all(),
  439. to_field_name='slug',
  440. required=False,
  441. widget=APISelectMultiple(
  442. value_field="slug",
  443. null_option=True,
  444. )
  445. )
  446. cluster_type = DynamicModelMultipleChoiceField(
  447. queryset=ClusterType.objects.all(),
  448. to_field_name='slug',
  449. required=False,
  450. widget=APISelectMultiple(
  451. value_field="slug",
  452. null_option=True,
  453. )
  454. )
  455. cluster_id = DynamicModelMultipleChoiceField(
  456. queryset=Cluster.objects.all(),
  457. required=False,
  458. label='Cluster'
  459. )
  460. region = DynamicModelMultipleChoiceField(
  461. queryset=Region.objects.all(),
  462. to_field_name='slug',
  463. required=False,
  464. widget=APISelectMultiple(
  465. value_field="slug",
  466. filter_for={
  467. 'site': 'region'
  468. }
  469. )
  470. )
  471. site = DynamicModelMultipleChoiceField(
  472. queryset=Site.objects.all(),
  473. to_field_name='slug',
  474. required=False,
  475. widget=APISelectMultiple(
  476. value_field="slug",
  477. null_option=True,
  478. )
  479. )
  480. role = DynamicModelMultipleChoiceField(
  481. queryset=DeviceRole.objects.filter(vm_role=True),
  482. to_field_name='slug',
  483. required=False,
  484. widget=APISelectMultiple(
  485. value_field="slug",
  486. null_option=True,
  487. additional_query_params={
  488. 'vm_role': "True"
  489. }
  490. )
  491. )
  492. status = forms.MultipleChoiceField(
  493. choices=VirtualMachineStatusChoices,
  494. required=False,
  495. widget=StaticSelect2Multiple()
  496. )
  497. platform = DynamicModelMultipleChoiceField(
  498. queryset=Platform.objects.all(),
  499. to_field_name='slug',
  500. required=False,
  501. widget=APISelectMultiple(
  502. value_field="slug",
  503. null_option=True,
  504. )
  505. )
  506. mac_address = forms.CharField(
  507. required=False,
  508. label='MAC address'
  509. )
  510. tag = TagFilterField(model)
  511. #
  512. # VM interfaces
  513. #
  514. class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
  515. untagged_vlan = DynamicModelChoiceField(
  516. queryset=VLAN.objects.all(),
  517. required=False,
  518. widget=APISelect(
  519. display_field='display_name',
  520. full=True,
  521. additional_query_params={
  522. 'site_id': 'null',
  523. },
  524. )
  525. )
  526. tagged_vlans = DynamicModelMultipleChoiceField(
  527. queryset=VLAN.objects.all(),
  528. required=False,
  529. widget=APISelectMultiple(
  530. display_field='display_name',
  531. full=True,
  532. additional_query_params={
  533. 'site_id': 'null',
  534. },
  535. )
  536. )
  537. tags = DynamicModelMultipleChoiceField(
  538. queryset=Tag.objects.all(),
  539. required=False
  540. )
  541. class Meta:
  542. model = VMInterface
  543. fields = [
  544. 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan',
  545. 'tagged_vlans',
  546. ]
  547. widgets = {
  548. 'virtual_machine': forms.HiddenInput(),
  549. 'mode': StaticSelect2()
  550. }
  551. labels = {
  552. 'mode': '802.1Q Mode',
  553. }
  554. help_texts = {
  555. 'mode': INTERFACE_MODE_HELP_TEXT,
  556. }
  557. def __init__(self, *args, **kwargs):
  558. super().__init__(*args, **kwargs)
  559. virtual_machine = VirtualMachine.objects.get(
  560. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  561. )
  562. # Add current site to VLANs query params
  563. site = virtual_machine.site
  564. if site:
  565. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  566. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  567. def clean(self):
  568. super().clean()
  569. # Validate VLAN assignments
  570. tagged_vlans = self.cleaned_data['tagged_vlans']
  571. # Untagged interfaces cannot be assigned tagged VLANs
  572. if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
  573. raise forms.ValidationError({
  574. 'mode': "An access interface cannot have tagged VLANs assigned."
  575. })
  576. # Remove all tagged VLAN assignments from "tagged all" interfaces
  577. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
  578. self.cleaned_data['tagged_vlans'] = []
  579. class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
  580. virtual_machine = DynamicModelChoiceField(
  581. queryset=VirtualMachine.objects.all()
  582. )
  583. name_pattern = ExpandableNameField(
  584. label='Name'
  585. )
  586. enabled = forms.BooleanField(
  587. required=False,
  588. initial=True
  589. )
  590. mtu = forms.IntegerField(
  591. required=False,
  592. min_value=INTERFACE_MTU_MIN,
  593. max_value=INTERFACE_MTU_MAX,
  594. label='MTU'
  595. )
  596. mac_address = forms.CharField(
  597. required=False,
  598. label='MAC Address'
  599. )
  600. description = forms.CharField(
  601. max_length=100,
  602. required=False
  603. )
  604. mode = forms.ChoiceField(
  605. choices=add_blank_choice(InterfaceModeChoices),
  606. required=False,
  607. widget=StaticSelect2(),
  608. )
  609. untagged_vlan = DynamicModelChoiceField(
  610. queryset=VLAN.objects.all(),
  611. required=False,
  612. widget=APISelect(
  613. display_field='display_name',
  614. full=True,
  615. additional_query_params={
  616. 'site_id': 'null',
  617. },
  618. )
  619. )
  620. tagged_vlans = DynamicModelMultipleChoiceField(
  621. queryset=VLAN.objects.all(),
  622. required=False,
  623. widget=APISelectMultiple(
  624. display_field='display_name',
  625. full=True,
  626. additional_query_params={
  627. 'site_id': 'null',
  628. },
  629. )
  630. )
  631. tags = DynamicModelMultipleChoiceField(
  632. queryset=Tag.objects.all(),
  633. required=False
  634. )
  635. def __init__(self, *args, **kwargs):
  636. super().__init__(*args, **kwargs)
  637. virtual_machine = VirtualMachine.objects.get(
  638. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  639. )
  640. # Add current site to VLANs query params
  641. site = virtual_machine.site
  642. if site:
  643. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  644. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  645. class VMInterfaceCSVForm(CSVModelForm):
  646. virtual_machine = CSVModelChoiceField(
  647. queryset=VirtualMachine.objects.all(),
  648. to_field_name='name'
  649. )
  650. mode = CSVChoiceField(
  651. choices=InterfaceModeChoices,
  652. required=False,
  653. help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
  654. )
  655. class Meta:
  656. model = VMInterface
  657. fields = VMInterface.csv_headers
  658. def clean_enabled(self):
  659. # Make sure enabled is True when it's not included in the uploaded data
  660. if 'enabled' not in self.data:
  661. return True
  662. else:
  663. return self.cleaned_data['enabled']
  664. class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  665. pk = forms.ModelMultipleChoiceField(
  666. queryset=VMInterface.objects.all(),
  667. widget=forms.MultipleHiddenInput()
  668. )
  669. virtual_machine = forms.ModelChoiceField(
  670. queryset=VirtualMachine.objects.all(),
  671. widget=forms.HiddenInput()
  672. )
  673. enabled = forms.NullBooleanField(
  674. required=False,
  675. widget=BulkEditNullBooleanSelect()
  676. )
  677. mtu = forms.IntegerField(
  678. required=False,
  679. min_value=INTERFACE_MTU_MIN,
  680. max_value=INTERFACE_MTU_MAX,
  681. label='MTU'
  682. )
  683. description = forms.CharField(
  684. max_length=100,
  685. required=False
  686. )
  687. mode = forms.ChoiceField(
  688. choices=add_blank_choice(InterfaceModeChoices),
  689. required=False,
  690. widget=StaticSelect2()
  691. )
  692. untagged_vlan = DynamicModelChoiceField(
  693. queryset=VLAN.objects.all(),
  694. required=False,
  695. widget=APISelect(
  696. display_field='display_name',
  697. full=True,
  698. additional_query_params={
  699. 'site_id': 'null',
  700. },
  701. )
  702. )
  703. tagged_vlans = DynamicModelMultipleChoiceField(
  704. queryset=VLAN.objects.all(),
  705. required=False,
  706. widget=APISelectMultiple(
  707. display_field='display_name',
  708. full=True,
  709. additional_query_params={
  710. 'site_id': 'null',
  711. },
  712. )
  713. )
  714. class Meta:
  715. nullable_fields = [
  716. 'mtu', 'description',
  717. ]
  718. def __init__(self, *args, **kwargs):
  719. super().__init__(*args, **kwargs)
  720. # Limit available VLANs based on the parent VirtualMachine
  721. if 'virtual_machine' in self.initial:
  722. parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
  723. site = getattr(parent_obj.cluster, 'site', None)
  724. if site is not None:
  725. # Add current site to VLANs query params
  726. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  727. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  728. class VMInterfaceFilterForm(forms.Form):
  729. model = VMInterface
  730. enabled = forms.NullBooleanField(
  731. required=False,
  732. widget=StaticSelect2(
  733. choices=BOOLEAN_WITH_BLANK_CHOICES
  734. )
  735. )
  736. tag = TagFilterField(model)
  737. #
  738. # Bulk VirtualMachine component creation
  739. #
  740. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  741. pk = forms.ModelMultipleChoiceField(
  742. queryset=VirtualMachine.objects.all(),
  743. widget=forms.MultipleHiddenInput()
  744. )
  745. name_pattern = ExpandableNameField(
  746. label='Name'
  747. )
  748. def clean_tags(self):
  749. # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
  750. # must first convert the list of tags to a string.
  751. return ','.join(self.cleaned_data.get('tags'))
  752. class VMInterfaceBulkCreateForm(
  753. form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
  754. VirtualMachineBulkAddComponentForm
  755. ):
  756. pass