forms.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. from django import forms
  2. from django.core.exceptions import ValidationError
  3. from taggit.forms import TagField
  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, Interface, Platform, Rack, Region, Site
  8. from extras.forms import (
  9. AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldModelCSVForm, CustomFieldModelForm, CustomFieldFilterForm,
  10. )
  11. from ipam.models import IPAddress, VLAN
  12. from tenancy.forms import TenancyFilterForm, TenancyForm
  13. from tenancy.models import Tenant
  14. from utilities.forms import (
  15. add_blank_choice, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect,
  16. CommentField, ConfirmationForm, CSVChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
  17. ExpandableNameField, JSONField, SlugField, SmallTextarea, StaticSelect2, StaticSelect2Multiple, TagFilterField,
  18. )
  19. from .choices import *
  20. from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine
  21. #
  22. # Cluster types
  23. #
  24. class ClusterTypeForm(BootstrapMixin, forms.ModelForm):
  25. slug = SlugField()
  26. class Meta:
  27. model = ClusterType
  28. fields = [
  29. 'name', 'slug', 'description',
  30. ]
  31. class ClusterTypeCSVForm(forms.ModelForm):
  32. slug = SlugField()
  33. class Meta:
  34. model = ClusterType
  35. fields = ClusterType.csv_headers
  36. help_texts = {
  37. 'name': 'Name of cluster type',
  38. }
  39. #
  40. # Cluster groups
  41. #
  42. class ClusterGroupForm(BootstrapMixin, forms.ModelForm):
  43. slug = SlugField()
  44. class Meta:
  45. model = ClusterGroup
  46. fields = [
  47. 'name', 'slug', 'description',
  48. ]
  49. class ClusterGroupCSVForm(forms.ModelForm):
  50. slug = SlugField()
  51. class Meta:
  52. model = ClusterGroup
  53. fields = ClusterGroup.csv_headers
  54. help_texts = {
  55. 'name': 'Name of cluster group',
  56. }
  57. #
  58. # Clusters
  59. #
  60. class ClusterForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  61. type = DynamicModelChoiceField(
  62. queryset=ClusterType.objects.all()
  63. )
  64. group = DynamicModelChoiceField(
  65. queryset=ClusterGroup.objects.all(),
  66. required=False
  67. )
  68. site = DynamicModelChoiceField(
  69. queryset=Site.objects.all(),
  70. required=False
  71. )
  72. comments = CommentField()
  73. tags = TagField(
  74. required=False
  75. )
  76. class Meta:
  77. model = Cluster
  78. fields = (
  79. 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags',
  80. )
  81. class ClusterCSVForm(CustomFieldModelCSVForm):
  82. type = forms.ModelChoiceField(
  83. queryset=ClusterType.objects.all(),
  84. to_field_name='name',
  85. help_text='Name of cluster type',
  86. error_messages={
  87. 'invalid_choice': 'Invalid cluster type name.',
  88. }
  89. )
  90. group = forms.ModelChoiceField(
  91. queryset=ClusterGroup.objects.all(),
  92. to_field_name='name',
  93. required=False,
  94. help_text='Name of cluster group',
  95. error_messages={
  96. 'invalid_choice': 'Invalid cluster group name.',
  97. }
  98. )
  99. site = forms.ModelChoiceField(
  100. queryset=Site.objects.all(),
  101. to_field_name='name',
  102. required=False,
  103. help_text='Name of assigned site',
  104. error_messages={
  105. 'invalid_choice': 'Invalid site name.',
  106. }
  107. )
  108. tenant = forms.ModelChoiceField(
  109. queryset=Tenant.objects.all(),
  110. to_field_name='name',
  111. required=False,
  112. help_text='Name of assigned tenant',
  113. error_messages={
  114. 'invalid_choice': 'Invalid tenant name'
  115. }
  116. )
  117. class Meta:
  118. model = Cluster
  119. fields = Cluster.csv_headers
  120. class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  121. pk = forms.ModelMultipleChoiceField(
  122. queryset=Cluster.objects.all(),
  123. widget=forms.MultipleHiddenInput()
  124. )
  125. type = DynamicModelChoiceField(
  126. queryset=ClusterType.objects.all(),
  127. required=False
  128. )
  129. group = DynamicModelChoiceField(
  130. queryset=ClusterGroup.objects.all(),
  131. required=False
  132. )
  133. tenant = DynamicModelChoiceField(
  134. queryset=Tenant.objects.all(),
  135. required=False
  136. )
  137. site = DynamicModelChoiceField(
  138. queryset=Site.objects.all(),
  139. required=False
  140. )
  141. comments = CommentField(
  142. widget=SmallTextarea,
  143. label='Comments'
  144. )
  145. class Meta:
  146. nullable_fields = [
  147. 'group', 'site', 'comments', 'tenant',
  148. ]
  149. class ClusterFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  150. model = Cluster
  151. field_order = [
  152. 'q', 'type', 'region', 'site', 'group', 'tenant_group', 'tenant'
  153. ]
  154. q = forms.CharField(required=False, label='Search')
  155. type = DynamicModelMultipleChoiceField(
  156. queryset=ClusterType.objects.all(),
  157. to_field_name='slug',
  158. required=False,
  159. widget=APISelectMultiple(
  160. value_field='slug',
  161. )
  162. )
  163. region = DynamicModelMultipleChoiceField(
  164. queryset=Region.objects.all(),
  165. to_field_name='slug',
  166. required=False,
  167. widget=APISelectMultiple(
  168. value_field="slug",
  169. filter_for={
  170. 'site': 'region'
  171. }
  172. )
  173. )
  174. site = DynamicModelMultipleChoiceField(
  175. queryset=Site.objects.all(),
  176. to_field_name='slug',
  177. required=False,
  178. widget=APISelectMultiple(
  179. value_field='slug',
  180. null_option=True,
  181. )
  182. )
  183. group = DynamicModelMultipleChoiceField(
  184. queryset=ClusterGroup.objects.all(),
  185. to_field_name='slug',
  186. required=False,
  187. widget=APISelectMultiple(
  188. value_field='slug',
  189. null_option=True,
  190. )
  191. )
  192. tag = TagFilterField(model)
  193. class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
  194. region = DynamicModelChoiceField(
  195. queryset=Region.objects.all(),
  196. required=False,
  197. widget=APISelect(
  198. filter_for={
  199. "site": "region_id",
  200. },
  201. attrs={
  202. 'nullable': 'true',
  203. }
  204. )
  205. )
  206. site = DynamicModelChoiceField(
  207. queryset=Site.objects.all(),
  208. required=False,
  209. widget=APISelect(
  210. filter_for={
  211. "rack": "site_id",
  212. "devices": "site_id",
  213. }
  214. )
  215. )
  216. rack = DynamicModelChoiceField(
  217. queryset=Rack.objects.all(),
  218. required=False,
  219. widget=APISelect(
  220. filter_for={
  221. "devices": "rack_id"
  222. },
  223. attrs={
  224. 'nullable': 'true',
  225. }
  226. )
  227. )
  228. devices = DynamicModelMultipleChoiceField(
  229. queryset=Device.objects.filter(cluster__isnull=True),
  230. widget=APISelectMultiple(
  231. display_field='display_name',
  232. disabled_indicator='cluster'
  233. )
  234. )
  235. class Meta:
  236. fields = [
  237. 'region', 'site', 'rack', 'devices',
  238. ]
  239. def __init__(self, cluster, *args, **kwargs):
  240. self.cluster = cluster
  241. super().__init__(*args, **kwargs)
  242. self.fields['devices'].choices = []
  243. def clean(self):
  244. super().clean()
  245. # If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
  246. if self.cluster.site is not None:
  247. for device in self.cleaned_data.get('devices', []):
  248. if device.site != self.cluster.site:
  249. raise ValidationError({
  250. 'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
  251. device, device.site, self.cluster.site
  252. )
  253. })
  254. class ClusterRemoveDevicesForm(ConfirmationForm):
  255. pk = forms.ModelMultipleChoiceField(
  256. queryset=Device.objects.all(),
  257. widget=forms.MultipleHiddenInput()
  258. )
  259. #
  260. # Virtual Machines
  261. #
  262. class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
  263. cluster_group = DynamicModelChoiceField(
  264. queryset=ClusterGroup.objects.all(),
  265. required=False,
  266. widget=APISelect(
  267. filter_for={
  268. "cluster": "group_id",
  269. },
  270. attrs={
  271. 'nullable': 'true',
  272. }
  273. )
  274. )
  275. cluster = DynamicModelChoiceField(
  276. queryset=Cluster.objects.all()
  277. )
  278. role = DynamicModelChoiceField(
  279. queryset=DeviceRole.objects.all(),
  280. required=False,
  281. widget=APISelect(
  282. additional_query_params={
  283. "vm_role": "True"
  284. }
  285. )
  286. )
  287. platform = DynamicModelChoiceField(
  288. queryset=Platform.objects.all(),
  289. required=False
  290. )
  291. tags = TagField(
  292. required=False
  293. )
  294. local_context_data = JSONField(
  295. required=False,
  296. label=''
  297. )
  298. class Meta:
  299. model = VirtualMachine
  300. fields = [
  301. 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
  302. 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data',
  303. ]
  304. help_texts = {
  305. 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered "
  306. "config context",
  307. }
  308. widgets = {
  309. "status": StaticSelect2(),
  310. 'primary_ip4': StaticSelect2(),
  311. 'primary_ip6': StaticSelect2(),
  312. }
  313. def __init__(self, *args, **kwargs):
  314. # Initialize helper selector
  315. instance = kwargs.get('instance')
  316. if instance.pk and instance.cluster is not None:
  317. initial = kwargs.get('initial', {}).copy()
  318. initial['cluster_group'] = instance.cluster.group
  319. kwargs['initial'] = initial
  320. super().__init__(*args, **kwargs)
  321. if self.instance.pk:
  322. # Compile list of choices for primary IPv4 and IPv6 addresses
  323. for family in [4, 6]:
  324. ip_choices = [(None, '---------')]
  325. # Collect interface IPs
  326. interface_ips = IPAddress.objects.prefetch_related('interface').filter(
  327. address__family=family, interface__virtual_machine=self.instance
  328. )
  329. if interface_ips:
  330. ip_choices.append(
  331. ('Interface IPs', [
  332. (ip.id, '{} ({})'.format(ip.address, ip.interface)) for ip in interface_ips
  333. ])
  334. )
  335. # Collect NAT IPs
  336. nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
  337. address__family=family, nat_inside__interface__virtual_machine=self.instance
  338. )
  339. if nat_ips:
  340. ip_choices.append(
  341. ('NAT IPs', [
  342. (ip.id, '{} ({})'.format(ip.address, ip.nat_inside.address)) for ip in nat_ips
  343. ])
  344. )
  345. self.fields['primary_ip{}'.format(family)].choices = ip_choices
  346. else:
  347. # An object that doesn't exist yet can't have any IPs assigned to it
  348. self.fields['primary_ip4'].choices = []
  349. self.fields['primary_ip4'].widget.attrs['readonly'] = True
  350. self.fields['primary_ip6'].choices = []
  351. self.fields['primary_ip6'].widget.attrs['readonly'] = True
  352. class VirtualMachineCSVForm(CustomFieldModelCSVForm):
  353. status = CSVChoiceField(
  354. choices=VirtualMachineStatusChoices,
  355. required=False,
  356. help_text='Operational status of device'
  357. )
  358. cluster = forms.ModelChoiceField(
  359. queryset=Cluster.objects.all(),
  360. to_field_name='name',
  361. help_text='Name of parent cluster',
  362. error_messages={
  363. 'invalid_choice': 'Invalid cluster name.',
  364. }
  365. )
  366. role = forms.ModelChoiceField(
  367. queryset=DeviceRole.objects.filter(
  368. vm_role=True
  369. ),
  370. required=False,
  371. to_field_name='name',
  372. help_text='Name of functional role',
  373. error_messages={
  374. 'invalid_choice': 'Invalid role name.'
  375. }
  376. )
  377. tenant = forms.ModelChoiceField(
  378. queryset=Tenant.objects.all(),
  379. required=False,
  380. to_field_name='name',
  381. help_text='Name of assigned tenant',
  382. error_messages={
  383. 'invalid_choice': 'Tenant not found.'
  384. }
  385. )
  386. platform = forms.ModelChoiceField(
  387. queryset=Platform.objects.all(),
  388. required=False,
  389. to_field_name='name',
  390. help_text='Name of assigned platform',
  391. error_messages={
  392. 'invalid_choice': 'Invalid platform.',
  393. }
  394. )
  395. class Meta:
  396. model = VirtualMachine
  397. fields = VirtualMachine.csv_headers
  398. class VirtualMachineBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEditForm):
  399. pk = forms.ModelMultipleChoiceField(
  400. queryset=VirtualMachine.objects.all(),
  401. widget=forms.MultipleHiddenInput()
  402. )
  403. status = forms.ChoiceField(
  404. choices=add_blank_choice(VirtualMachineStatusChoices),
  405. required=False,
  406. initial='',
  407. widget=StaticSelect2(),
  408. )
  409. cluster = DynamicModelChoiceField(
  410. queryset=Cluster.objects.all(),
  411. required=False
  412. )
  413. role = DynamicModelChoiceField(
  414. queryset=DeviceRole.objects.filter(
  415. vm_role=True
  416. ),
  417. required=False,
  418. widget=APISelect(
  419. additional_query_params={
  420. "vm_role": "True"
  421. }
  422. )
  423. )
  424. tenant = DynamicModelChoiceField(
  425. queryset=Tenant.objects.all(),
  426. required=False
  427. )
  428. platform = DynamicModelChoiceField(
  429. queryset=Platform.objects.all(),
  430. required=False
  431. )
  432. vcpus = forms.IntegerField(
  433. required=False,
  434. label='vCPUs'
  435. )
  436. memory = forms.IntegerField(
  437. required=False,
  438. label='Memory (MB)'
  439. )
  440. disk = forms.IntegerField(
  441. required=False,
  442. label='Disk (GB)'
  443. )
  444. comments = CommentField(
  445. widget=SmallTextarea,
  446. label='Comments'
  447. )
  448. class Meta:
  449. nullable_fields = [
  450. 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments',
  451. ]
  452. class VirtualMachineFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldFilterForm):
  453. model = VirtualMachine
  454. field_order = [
  455. 'q', 'cluster_group', 'cluster_type', 'cluster_id', 'status', 'role', 'region', 'site', 'tenant_group',
  456. 'tenant', 'platform', 'mac_address',
  457. ]
  458. q = forms.CharField(
  459. required=False,
  460. label='Search'
  461. )
  462. cluster_group = DynamicModelMultipleChoiceField(
  463. queryset=ClusterGroup.objects.all(),
  464. to_field_name='slug',
  465. required=False,
  466. widget=APISelectMultiple(
  467. value_field="slug",
  468. null_option=True,
  469. )
  470. )
  471. cluster_type = DynamicModelMultipleChoiceField(
  472. queryset=ClusterType.objects.all(),
  473. to_field_name='slug',
  474. required=False,
  475. widget=APISelectMultiple(
  476. value_field="slug",
  477. null_option=True,
  478. )
  479. )
  480. cluster_id = DynamicModelMultipleChoiceField(
  481. queryset=Cluster.objects.all(),
  482. required=False,
  483. label='Cluster'
  484. )
  485. region = DynamicModelMultipleChoiceField(
  486. queryset=Region.objects.all(),
  487. to_field_name='slug',
  488. required=False,
  489. widget=APISelectMultiple(
  490. value_field="slug",
  491. filter_for={
  492. 'site': 'region'
  493. }
  494. )
  495. )
  496. site = DynamicModelMultipleChoiceField(
  497. queryset=Site.objects.all(),
  498. to_field_name='slug',
  499. required=False,
  500. widget=APISelectMultiple(
  501. value_field="slug",
  502. null_option=True,
  503. )
  504. )
  505. role = DynamicModelMultipleChoiceField(
  506. queryset=DeviceRole.objects.filter(vm_role=True),
  507. to_field_name='slug',
  508. required=False,
  509. widget=APISelectMultiple(
  510. value_field="slug",
  511. null_option=True,
  512. additional_query_params={
  513. 'vm_role': "True"
  514. }
  515. )
  516. )
  517. status = forms.MultipleChoiceField(
  518. choices=VirtualMachineStatusChoices,
  519. required=False,
  520. widget=StaticSelect2Multiple()
  521. )
  522. platform = DynamicModelMultipleChoiceField(
  523. queryset=Platform.objects.all(),
  524. to_field_name='slug',
  525. required=False,
  526. widget=APISelectMultiple(
  527. value_field="slug",
  528. null_option=True,
  529. )
  530. )
  531. mac_address = forms.CharField(
  532. required=False,
  533. label='MAC address'
  534. )
  535. tag = TagFilterField(model)
  536. #
  537. # VM interfaces
  538. #
  539. class InterfaceForm(BootstrapMixin, forms.ModelForm):
  540. untagged_vlan = DynamicModelChoiceField(
  541. queryset=VLAN.objects.all(),
  542. required=False,
  543. widget=APISelect(
  544. display_field='display_name',
  545. full=True,
  546. additional_query_params={
  547. 'site_id': 'null',
  548. },
  549. )
  550. )
  551. tagged_vlans = DynamicModelMultipleChoiceField(
  552. queryset=VLAN.objects.all(),
  553. required=False,
  554. widget=APISelectMultiple(
  555. display_field='display_name',
  556. full=True,
  557. additional_query_params={
  558. 'site_id': 'null',
  559. },
  560. )
  561. )
  562. tags = TagField(
  563. required=False
  564. )
  565. class Meta:
  566. model = Interface
  567. fields = [
  568. 'virtual_machine', 'name', 'type', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags',
  569. 'untagged_vlan', 'tagged_vlans',
  570. ]
  571. widgets = {
  572. 'virtual_machine': forms.HiddenInput(),
  573. 'type': forms.HiddenInput(),
  574. 'mode': StaticSelect2()
  575. }
  576. labels = {
  577. 'mode': '802.1Q Mode',
  578. }
  579. help_texts = {
  580. 'mode': INTERFACE_MODE_HELP_TEXT,
  581. }
  582. def __init__(self, *args, **kwargs):
  583. super().__init__(*args, **kwargs)
  584. # Add current site to VLANs query params
  585. site = getattr(self.instance.parent, 'site', None)
  586. if site is not None:
  587. # Add current site to VLANs query params
  588. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  589. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  590. def clean(self):
  591. super().clean()
  592. # Validate VLAN assignments
  593. tagged_vlans = self.cleaned_data['tagged_vlans']
  594. # Untagged interfaces cannot be assigned tagged VLANs
  595. if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
  596. raise forms.ValidationError({
  597. 'mode': "An access interface cannot have tagged VLANs assigned."
  598. })
  599. # Remove all tagged VLAN assignments from "tagged all" interfaces
  600. elif self.cleaned_data['mode'] == InterfaceModeChoices.MODE_TAGGED_ALL:
  601. self.cleaned_data['tagged_vlans'] = []
  602. class InterfaceCreateForm(BootstrapMixin, forms.Form):
  603. virtual_machine = forms.ModelChoiceField(
  604. queryset=VirtualMachine.objects.all(),
  605. widget=forms.HiddenInput()
  606. )
  607. name_pattern = ExpandableNameField(
  608. label='Name'
  609. )
  610. type = forms.ChoiceField(
  611. choices=VMInterfaceTypeChoices,
  612. initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
  613. widget=forms.HiddenInput()
  614. )
  615. enabled = forms.BooleanField(
  616. required=False,
  617. initial=True
  618. )
  619. mtu = forms.IntegerField(
  620. required=False,
  621. min_value=INTERFACE_MTU_MIN,
  622. max_value=INTERFACE_MTU_MAX,
  623. label='MTU'
  624. )
  625. mac_address = forms.CharField(
  626. required=False,
  627. label='MAC Address'
  628. )
  629. description = forms.CharField(
  630. max_length=100,
  631. required=False
  632. )
  633. mode = forms.ChoiceField(
  634. choices=add_blank_choice(InterfaceModeChoices),
  635. required=False,
  636. widget=StaticSelect2(),
  637. )
  638. untagged_vlan = DynamicModelChoiceField(
  639. queryset=VLAN.objects.all(),
  640. required=False,
  641. widget=APISelect(
  642. display_field='display_name',
  643. full=True,
  644. additional_query_params={
  645. 'site_id': 'null',
  646. },
  647. )
  648. )
  649. tagged_vlans = DynamicModelMultipleChoiceField(
  650. queryset=VLAN.objects.all(),
  651. required=False,
  652. widget=APISelectMultiple(
  653. display_field='display_name',
  654. full=True,
  655. additional_query_params={
  656. 'site_id': 'null',
  657. },
  658. )
  659. )
  660. tags = TagField(
  661. required=False
  662. )
  663. def __init__(self, *args, **kwargs):
  664. super().__init__(*args, **kwargs)
  665. virtual_machine = VirtualMachine.objects.get(
  666. pk=self.initial.get('virtual_machine') or self.data.get('virtual_machine')
  667. )
  668. site = getattr(virtual_machine.cluster, 'site', None)
  669. if site is not None:
  670. # Add current site to VLANs query params
  671. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  672. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  673. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  674. pk = forms.ModelMultipleChoiceField(
  675. queryset=Interface.objects.all(),
  676. widget=forms.MultipleHiddenInput()
  677. )
  678. virtual_machine = forms.ModelChoiceField(
  679. queryset=VirtualMachine.objects.all(),
  680. widget=forms.HiddenInput()
  681. )
  682. enabled = forms.NullBooleanField(
  683. required=False,
  684. widget=BulkEditNullBooleanSelect()
  685. )
  686. mtu = forms.IntegerField(
  687. required=False,
  688. min_value=INTERFACE_MTU_MIN,
  689. max_value=INTERFACE_MTU_MAX,
  690. label='MTU'
  691. )
  692. description = forms.CharField(
  693. max_length=100,
  694. required=False
  695. )
  696. mode = forms.ChoiceField(
  697. choices=add_blank_choice(InterfaceModeChoices),
  698. required=False,
  699. widget=StaticSelect2()
  700. )
  701. untagged_vlan = DynamicModelChoiceField(
  702. queryset=VLAN.objects.all(),
  703. required=False,
  704. widget=APISelect(
  705. display_field='display_name',
  706. full=True,
  707. additional_query_params={
  708. 'site_id': 'null',
  709. },
  710. )
  711. )
  712. tagged_vlans = DynamicModelMultipleChoiceField(
  713. queryset=VLAN.objects.all(),
  714. required=False,
  715. widget=APISelectMultiple(
  716. display_field='display_name',
  717. full=True,
  718. additional_query_params={
  719. 'site_id': 'null',
  720. },
  721. )
  722. )
  723. class Meta:
  724. nullable_fields = [
  725. 'mtu', 'description',
  726. ]
  727. def __init__(self, *args, **kwargs):
  728. super().__init__(*args, **kwargs)
  729. # Limit available VLANs based on the parent VirtualMachine
  730. if 'virtual_machine' in self.initial:
  731. parent_obj = VirtualMachine.objects.filter(pk=self.initial['virtual_machine']).first()
  732. site = getattr(parent_obj.cluster, 'site', None)
  733. if site is not None:
  734. # Add current site to VLANs query params
  735. self.fields['untagged_vlan'].widget.add_additional_query_param('site_id', site.pk)
  736. self.fields['tagged_vlans'].widget.add_additional_query_param('site_id', site.pk)
  737. #
  738. # Bulk VirtualMachine component creation
  739. #
  740. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  741. pk = forms.ModelMultipleChoiceField(
  742. queryset=VirtualMachine.objects.all(),
  743. widget=forms.MultipleHiddenInput()
  744. )
  745. name_pattern = ExpandableNameField(
  746. label='Name'
  747. )
  748. class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
  749. type = forms.ChoiceField(
  750. choices=VMInterfaceTypeChoices,
  751. initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
  752. widget=forms.HiddenInput()
  753. )
  754. enabled = forms.BooleanField(
  755. required=False,
  756. initial=True
  757. )
  758. mtu = forms.IntegerField(
  759. required=False,
  760. min_value=INTERFACE_MTU_MIN,
  761. max_value=INTERFACE_MTU_MAX,
  762. label='MTU'
  763. )
  764. description = forms.CharField(
  765. max_length=100,
  766. required=False
  767. )