forms.py 27 KB

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