forms.py 24 KB

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