forms.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816
  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. has_primary_ip = forms.NullBooleanField(
  461. required=False,
  462. label='Has a primary IP',
  463. widget=StaticSelect2(
  464. choices=BOOLEAN_WITH_BLANK_CHOICES
  465. )
  466. )
  467. tag = TagFilterField(model)
  468. #
  469. # VM interfaces
  470. #
  471. class VMInterfaceForm(BootstrapMixin, forms.ModelForm):
  472. untagged_vlan = DynamicModelChoiceField(
  473. queryset=VLAN.objects.all(),
  474. required=False,
  475. display_field='display_name',
  476. brief_mode=False,
  477. query_params={
  478. 'site_id': 'null',
  479. }
  480. )
  481. tagged_vlans = DynamicModelMultipleChoiceField(
  482. queryset=VLAN.objects.all(),
  483. required=False,
  484. display_field='display_name',
  485. brief_mode=False,
  486. query_params={
  487. 'site_id': 'null',
  488. }
  489. )
  490. tags = DynamicModelMultipleChoiceField(
  491. queryset=Tag.objects.all(),
  492. required=False
  493. )
  494. class Meta:
  495. model = VMInterface
  496. fields = [
  497. 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan',
  498. 'tagged_vlans',
  499. ]
  500. widgets = {
  501. 'virtual_machine': forms.HiddenInput(),
  502. 'mode': StaticSelect2()
  503. }
  504. labels = {
  505. 'mode': '802.1Q Mode',
  506. }
  507. help_texts = {
  508. 'mode': INTERFACE_MODE_HELP_TEXT,
  509. }
  510. def __init__(self, *args, **kwargs):
  511. super().__init__(*args, **kwargs)
  512. virtual_machine = VirtualMachine.objects.get(
  513. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  514. )
  515. # Add current site to VLANs query params
  516. site = virtual_machine.site
  517. if site:
  518. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  519. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  520. def clean(self):
  521. super().clean()
  522. # Validate VLAN assignments
  523. tagged_vlans = self.cleaned_data['tagged_vlans']
  524. # Untagged interfaces cannot be assigned tagged VLANs
  525. if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
  526. raise forms.ValidationError({
  527. 'mode': "An access interface cannot have tagged VLANs assigned."
  528. })
  529. # Remove all tagged VLAN assignments from "tagged all" interfaces
  530. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
  531. self.cleaned_data['tagged_vlans'] = []
  532. class VMInterfaceCreateForm(BootstrapMixin, forms.Form):
  533. virtual_machine = DynamicModelChoiceField(
  534. queryset=VirtualMachine.objects.all()
  535. )
  536. name_pattern = ExpandableNameField(
  537. label='Name'
  538. )
  539. enabled = forms.BooleanField(
  540. required=False,
  541. initial=True
  542. )
  543. mtu = forms.IntegerField(
  544. required=False,
  545. min_value=INTERFACE_MTU_MIN,
  546. max_value=INTERFACE_MTU_MAX,
  547. label='MTU'
  548. )
  549. mac_address = forms.CharField(
  550. required=False,
  551. label='MAC Address'
  552. )
  553. description = forms.CharField(
  554. max_length=100,
  555. required=False
  556. )
  557. mode = forms.ChoiceField(
  558. choices=add_blank_choice(InterfaceModeChoices),
  559. required=False,
  560. widget=StaticSelect2(),
  561. )
  562. untagged_vlan = DynamicModelChoiceField(
  563. queryset=VLAN.objects.all(),
  564. required=False,
  565. display_field='display_name',
  566. brief_mode=False,
  567. query_params={
  568. 'site_id': 'null',
  569. }
  570. )
  571. tagged_vlans = DynamicModelMultipleChoiceField(
  572. queryset=VLAN.objects.all(),
  573. required=False,
  574. display_field='display_name',
  575. brief_mode=False,
  576. query_params={
  577. 'site_id': 'null',
  578. }
  579. )
  580. tags = DynamicModelMultipleChoiceField(
  581. queryset=Tag.objects.all(),
  582. required=False
  583. )
  584. def __init__(self, *args, **kwargs):
  585. super().__init__(*args, **kwargs)
  586. virtual_machine = VirtualMachine.objects.get(
  587. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  588. )
  589. # Add current site to VLANs query params
  590. site = virtual_machine.site
  591. if site:
  592. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  593. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  594. class VMInterfaceCSVForm(CSVModelForm):
  595. virtual_machine = CSVModelChoiceField(
  596. queryset=VirtualMachine.objects.all(),
  597. to_field_name='name'
  598. )
  599. mode = CSVChoiceField(
  600. choices=InterfaceModeChoices,
  601. required=False,
  602. help_text='IEEE 802.1Q operational mode (for L2 interfaces)'
  603. )
  604. class Meta:
  605. model = VMInterface
  606. fields = VMInterface.csv_headers
  607. def clean_enabled(self):
  608. # Make sure enabled is True when it's not included in the uploaded data
  609. if 'enabled' not in self.data:
  610. return True
  611. else:
  612. return self.cleaned_data['enabled']
  613. class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
  614. pk = forms.ModelMultipleChoiceField(
  615. queryset=VMInterface.objects.all(),
  616. widget=forms.MultipleHiddenInput()
  617. )
  618. virtual_machine = forms.ModelChoiceField(
  619. queryset=VirtualMachine.objects.all(),
  620. required=False,
  621. disabled=True,
  622. widget=forms.HiddenInput()
  623. )
  624. enabled = forms.NullBooleanField(
  625. required=False,
  626. widget=BulkEditNullBooleanSelect()
  627. )
  628. mtu = forms.IntegerField(
  629. required=False,
  630. min_value=INTERFACE_MTU_MIN,
  631. max_value=INTERFACE_MTU_MAX,
  632. label='MTU'
  633. )
  634. description = forms.CharField(
  635. max_length=100,
  636. required=False
  637. )
  638. mode = forms.ChoiceField(
  639. choices=add_blank_choice(InterfaceModeChoices),
  640. required=False,
  641. widget=StaticSelect2()
  642. )
  643. untagged_vlan = DynamicModelChoiceField(
  644. queryset=VLAN.objects.all(),
  645. required=False,
  646. display_field='display_name',
  647. brief_mode=False,
  648. query_params={
  649. 'site_id': 'null',
  650. }
  651. )
  652. tagged_vlans = DynamicModelMultipleChoiceField(
  653. queryset=VLAN.objects.all(),
  654. required=False,
  655. display_field='display_name',
  656. brief_mode=False,
  657. query_params={
  658. 'site_id': 'null',
  659. }
  660. )
  661. class Meta:
  662. nullable_fields = [
  663. 'mtu', 'description',
  664. ]
  665. def __init__(self, *args, **kwargs):
  666. super().__init__(*args, **kwargs)
  667. # Limit available VLANs based on the parent VirtualMachine
  668. if 'virtual_machine' in self.initial:
  669. parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
  670. site = getattr(parent_obj.cluster, 'site', None)
  671. if site is not None:
  672. # Add current site to VLANs query params
  673. self.fields['untagged_vlan'].widget.add_query_param('site_id', site.pk)
  674. self.fields['tagged_vlans'].widget.add_query_param('site_id', site.pk)
  675. class VMInterfaceBulkRenameForm(BulkRenameForm):
  676. pk = forms.ModelMultipleChoiceField(
  677. queryset=VMInterface.objects.all(),
  678. widget=forms.MultipleHiddenInput()
  679. )
  680. class VMInterfaceFilterForm(forms.Form):
  681. model = VMInterface
  682. cluster_id = DynamicModelMultipleChoiceField(
  683. queryset=Cluster.objects.all(),
  684. required=False,
  685. label='Cluster'
  686. )
  687. virtual_machine_id = DynamicModelMultipleChoiceField(
  688. queryset=VirtualMachine.objects.all(),
  689. required=False,
  690. label='Virtual machine',
  691. query_params={
  692. 'cluster_id': '$cluster_id'
  693. }
  694. )
  695. enabled = forms.NullBooleanField(
  696. required=False,
  697. widget=StaticSelect2(
  698. choices=BOOLEAN_WITH_BLANK_CHOICES
  699. )
  700. )
  701. tag = TagFilterField(model)
  702. #
  703. # Bulk VirtualMachine component creation
  704. #
  705. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  706. pk = forms.ModelMultipleChoiceField(
  707. queryset=VirtualMachine.objects.all(),
  708. widget=forms.MultipleHiddenInput()
  709. )
  710. name_pattern = ExpandableNameField(
  711. label='Name'
  712. )
  713. def clean_tags(self):
  714. # Because we're feeding TagField data (on the bulk edit form) to another TagField (on the model form), we
  715. # must first convert the list of tags to a string.
  716. return ','.join(self.cleaned_data.get('tags'))
  717. class VMInterfaceBulkCreateForm(
  718. form_from_model(VMInterface, ['enabled', 'mtu', 'description', 'tags']),
  719. VirtualMachineBulkAddComponentForm
  720. ):
  721. pass