forms.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948
  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, ComponentForm,
  17. ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField,
  18. SmallTextarea, StaticSelect2, 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(ComponentForm):
  669. name_pattern = ExpandableNameField(
  670. label='Name'
  671. )
  672. type = forms.ChoiceField(
  673. choices=VMInterfaceTypeChoices,
  674. initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
  675. widget=forms.HiddenInput()
  676. )
  677. enabled = forms.BooleanField(
  678. required=False
  679. )
  680. mtu = forms.IntegerField(
  681. required=False,
  682. min_value=INTERFACE_MTU_MIN,
  683. max_value=INTERFACE_MTU_MAX,
  684. label='MTU'
  685. )
  686. mac_address = forms.CharField(
  687. required=False,
  688. label='MAC Address'
  689. )
  690. description = forms.CharField(
  691. max_length=100,
  692. required=False
  693. )
  694. mode = forms.ChoiceField(
  695. choices=add_blank_choice(InterfaceModeChoices),
  696. required=False,
  697. widget=StaticSelect2(),
  698. )
  699. untagged_vlan = forms.ModelChoiceField(
  700. queryset=VLAN.objects.all(),
  701. required=False,
  702. widget=APISelect(
  703. api_url="/api/ipam/vlans/",
  704. display_field='display_name',
  705. full=True
  706. )
  707. )
  708. tagged_vlans = forms.ModelMultipleChoiceField(
  709. queryset=VLAN.objects.all(),
  710. required=False,
  711. widget=APISelectMultiple(
  712. api_url="/api/ipam/vlans/",
  713. display_field='display_name',
  714. full=True
  715. )
  716. )
  717. tags = TagField(
  718. required=False
  719. )
  720. def __init__(self, *args, **kwargs):
  721. # Set interfaces enabled by default
  722. kwargs['initial'] = kwargs.get('initial', {}).copy()
  723. kwargs['initial'].update({'enabled': True})
  724. super().__init__(*args, **kwargs)
  725. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  726. vlan_choices = []
  727. global_vlans = VLAN.objects.filter(site=None, group=None)
  728. vlan_choices.append(
  729. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  730. )
  731. for group in VLANGroup.objects.filter(site=None):
  732. global_group_vlans = VLAN.objects.filter(group=group)
  733. vlan_choices.append(
  734. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  735. )
  736. site = getattr(self.parent.cluster, 'site', None)
  737. if site is not None:
  738. # Add non-grouped site VLANs
  739. site_vlans = VLAN.objects.filter(site=site, group=None)
  740. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  741. # Add grouped site VLANs
  742. for group in VLANGroup.objects.filter(site=site):
  743. site_group_vlans = VLAN.objects.filter(group=group)
  744. vlan_choices.append((
  745. '{} / {}'.format(group.site.name, group.name),
  746. [(vlan.pk, vlan) for vlan in site_group_vlans]
  747. ))
  748. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  749. self.fields['tagged_vlans'].choices = vlan_choices
  750. class InterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
  751. pk = forms.ModelMultipleChoiceField(
  752. queryset=Interface.objects.all(),
  753. widget=forms.MultipleHiddenInput()
  754. )
  755. enabled = forms.NullBooleanField(
  756. required=False,
  757. widget=BulkEditNullBooleanSelect()
  758. )
  759. mtu = forms.IntegerField(
  760. required=False,
  761. min_value=INTERFACE_MTU_MIN,
  762. max_value=INTERFACE_MTU_MAX,
  763. label='MTU'
  764. )
  765. description = forms.CharField(
  766. max_length=100,
  767. required=False
  768. )
  769. mode = forms.ChoiceField(
  770. choices=add_blank_choice(InterfaceModeChoices),
  771. required=False,
  772. widget=StaticSelect2()
  773. )
  774. untagged_vlan = forms.ModelChoiceField(
  775. queryset=VLAN.objects.all(),
  776. required=False,
  777. widget=APISelect(
  778. api_url="/api/ipam/vlans/",
  779. display_field='display_name',
  780. full=True
  781. )
  782. )
  783. tagged_vlans = forms.ModelMultipleChoiceField(
  784. queryset=VLAN.objects.all(),
  785. required=False,
  786. widget=APISelectMultiple(
  787. api_url="/api/ipam/vlans/",
  788. display_field='display_name',
  789. full=True
  790. )
  791. )
  792. class Meta:
  793. nullable_fields = [
  794. 'mtu', 'description',
  795. ]
  796. def __init__(self, *args, **kwargs):
  797. super().__init__(*args, **kwargs)
  798. # Limit VLan choices to those in: global vlans, global groups, the current site's group, the current site
  799. vlan_choices = []
  800. global_vlans = VLAN.objects.filter(site=None, group=None)
  801. vlan_choices.append(
  802. ('Global', [(vlan.pk, vlan) for vlan in global_vlans])
  803. )
  804. for group in VLANGroup.objects.filter(site=None):
  805. global_group_vlans = VLAN.objects.filter(group=group)
  806. vlan_choices.append(
  807. (group.name, [(vlan.pk, vlan) for vlan in global_group_vlans])
  808. )
  809. if self.parent_obj.cluster is not None:
  810. site = getattr(self.parent_obj.cluster, 'site', None)
  811. if site is not None:
  812. # Add non-grouped site VLANs
  813. site_vlans = VLAN.objects.filter(site=site, group=None)
  814. vlan_choices.append((site.name, [(vlan.pk, vlan) for vlan in site_vlans]))
  815. # Add grouped site VLANs
  816. for group in VLANGroup.objects.filter(site=site):
  817. site_group_vlans = VLAN.objects.filter(group=group)
  818. vlan_choices.append((
  819. '{} / {}'.format(group.site.name, group.name),
  820. [(vlan.pk, vlan) for vlan in site_group_vlans]
  821. ))
  822. self.fields['untagged_vlan'].choices = [(None, '---------')] + vlan_choices
  823. self.fields['tagged_vlans'].choices = vlan_choices
  824. #
  825. # Bulk VirtualMachine component creation
  826. #
  827. class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
  828. pk = forms.ModelMultipleChoiceField(
  829. queryset=VirtualMachine.objects.all(),
  830. widget=forms.MultipleHiddenInput()
  831. )
  832. name_pattern = ExpandableNameField(
  833. label='Name'
  834. )
  835. class VirtualMachineBulkAddInterfaceForm(VirtualMachineBulkAddComponentForm):
  836. type = forms.ChoiceField(
  837. choices=VMInterfaceTypeChoices,
  838. initial=VMInterfaceTypeChoices.TYPE_VIRTUAL,
  839. widget=forms.HiddenInput()
  840. )
  841. enabled = forms.BooleanField(
  842. required=False,
  843. initial=True
  844. )
  845. mtu = forms.IntegerField(
  846. required=False,
  847. min_value=INTERFACE_MTU_MIN,
  848. max_value=INTERFACE_MTU_MAX,
  849. label='MTU'
  850. )
  851. description = forms.CharField(
  852. max_length=100,
  853. required=False
  854. )