forms.py 28 KB

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