forms.py 23 KB

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