forms.py 21 KB

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