forms.py 28 KB

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