forms.py 27 KB

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