forms.py 29 KB

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