forms.py 28 KB

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