forms.py 26 KB

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