forms.py 26 KB

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