forms.py 21 KB

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