forms.py 24 KB

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