forms.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. from netaddr import IPNetwork
  2. from django import forms
  3. from django.db.models import Count
  4. from dcim.models import Site, Device, Interface
  5. from utilities.forms import BootstrapMixin, APISelect, Livesearch, CSVDataField, BulkImportForm, SlugField
  6. from .models import (
  7. Aggregate, IPAddress, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, VLAN, VLANGroup, VLAN_STATUS_CHOICES, VRF,
  8. )
  9. FORM_PREFIX_STATUS_CHOICES = (('', '---------'),) + PREFIX_STATUS_CHOICES
  10. FORM_VLAN_STATUS_CHOICES = (('', '---------'),) + VLAN_STATUS_CHOICES
  11. #
  12. # VRFs
  13. #
  14. class VRFForm(forms.ModelForm, BootstrapMixin):
  15. class Meta:
  16. model = VRF
  17. fields = ['name', 'rd', 'enforce_unique', 'description']
  18. labels = {
  19. 'rd': "RD",
  20. }
  21. help_texts = {
  22. 'rd': "Route distinguisher in any format",
  23. }
  24. class VRFFromCSVForm(forms.ModelForm):
  25. class Meta:
  26. model = VRF
  27. fields = ['name', 'rd', 'enforce_unique', 'description']
  28. class VRFImportForm(BulkImportForm, BootstrapMixin):
  29. csv = CSVDataField(csv_form=VRFFromCSVForm)
  30. class VRFBulkEditForm(forms.Form, BootstrapMixin):
  31. pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
  32. description = forms.CharField(max_length=100, required=False)
  33. #
  34. # RIRs
  35. #
  36. class RIRForm(forms.ModelForm, BootstrapMixin):
  37. slug = SlugField()
  38. class Meta:
  39. model = RIR
  40. fields = ['name', 'slug']
  41. #
  42. # Aggregates
  43. #
  44. class AggregateForm(forms.ModelForm, BootstrapMixin):
  45. class Meta:
  46. model = Aggregate
  47. fields = ['prefix', 'rir', 'date_added', 'description']
  48. help_texts = {
  49. 'prefix': "IPv4 or IPv6 network",
  50. 'rir': "Regional Internet Registry responsible for this prefix",
  51. 'date_added': "Format: YYYY-MM-DD",
  52. }
  53. class AggregateFromCSVForm(forms.ModelForm):
  54. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
  55. error_messages={'invalid_choice': 'RIR not found.'})
  56. class Meta:
  57. model = Aggregate
  58. fields = ['prefix', 'rir', 'date_added', 'description']
  59. class AggregateImportForm(BulkImportForm, BootstrapMixin):
  60. csv = CSVDataField(csv_form=AggregateFromCSVForm)
  61. class AggregateBulkEditForm(forms.Form, BootstrapMixin):
  62. pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
  63. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
  64. date_added = forms.DateField(required=False)
  65. description = forms.CharField(max_length=50, required=False)
  66. def aggregate_rir_choices():
  67. rir_choices = RIR.objects.annotate(aggregate_count=Count('aggregates'))
  68. return [(r.slug, u'{} ({})'.format(r.name, r.aggregate_count)) for r in rir_choices]
  69. class AggregateFilterForm(forms.Form, BootstrapMixin):
  70. rir = forms.MultipleChoiceField(required=False, choices=aggregate_rir_choices, label='RIR',
  71. widget=forms.SelectMultiple(attrs={'size': 8}))
  72. #
  73. # Roles
  74. #
  75. class RoleForm(forms.ModelForm, BootstrapMixin):
  76. slug = SlugField()
  77. class Meta:
  78. model = Role
  79. fields = ['name', 'slug']
  80. #
  81. # Prefixes
  82. #
  83. class PrefixForm(forms.ModelForm, BootstrapMixin):
  84. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
  85. widget=forms.Select(attrs={'filter-for': 'vlan'}))
  86. vlan = forms.ModelChoiceField(queryset=VLAN.objects.all(), required=False, label='VLAN',
  87. widget=APISelect(api_url='/api/ipam/vlans/?site_id={{site}}',
  88. display_field='display_name'))
  89. class Meta:
  90. model = Prefix
  91. fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'description']
  92. help_texts = {
  93. 'prefix': "IPv4 or IPv6 network",
  94. 'vrf': "VRF (if applicable)",
  95. 'site': "The site to which this prefix is assigned (if applicable)",
  96. 'vlan': "The VLAN to which this prefix is assigned (if applicable)",
  97. 'status': "Operational status of this prefix",
  98. 'role': "The primary function of this prefix",
  99. }
  100. def __init__(self, *args, **kwargs):
  101. super(PrefixForm, self).__init__(*args, **kwargs)
  102. self.fields['vrf'].empty_label = 'Global'
  103. # Initialize field without choices to avoid pulling all VLANs from the database
  104. if self.is_bound and self.data.get('site'):
  105. self.fields['vlan'].queryset = VLAN.objects.filter(site__pk=self.data['site'])
  106. elif self.initial.get('site'):
  107. self.fields['vlan'].queryset = VLAN.objects.filter(site=self.initial['site'])
  108. else:
  109. self.fields['vlan'].choices = []
  110. def clean_prefix(self):
  111. data = self.cleaned_data['prefix']
  112. try:
  113. prefix = IPNetwork(data)
  114. except:
  115. raise
  116. if prefix.version == 4 and prefix.prefixlen == 32:
  117. raise forms.ValidationError("Cannot create host addresses (/32) as prefixes. These should be IPv4 "
  118. "addresses instead.")
  119. elif prefix.version == 6 and prefix.prefixlen == 128:
  120. raise forms.ValidationError("Cannot create host addresses (/128) as prefixes. These should be IPv6 "
  121. "addresses instead.")
  122. return data
  123. class PrefixFromCSVForm(forms.ModelForm):
  124. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  125. error_messages={'invalid_choice': 'VRF not found.'})
  126. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
  127. error_messages={'invalid_choice': 'Site not found.'})
  128. vlan_group_name = forms.CharField(required=False)
  129. vlan_vid = forms.IntegerField(required=False)
  130. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
  131. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
  132. error_messages={'invalid_choice': 'Invalid role.'})
  133. class Meta:
  134. model = Prefix
  135. fields = ['prefix', 'vrf', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'description']
  136. def clean(self):
  137. super(PrefixFromCSVForm, self).clean()
  138. site = self.cleaned_data.get('site')
  139. vlan_group_name = self.cleaned_data.get('vlan_group_name')
  140. vlan_vid = self.cleaned_data.get('vlan_vid')
  141. # Validate VLAN
  142. vlan_group = None
  143. if vlan_group_name:
  144. try:
  145. vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
  146. except VLANGroup.DoesNotExist:
  147. self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
  148. if vlan_vid and vlan_group:
  149. try:
  150. self.instance.vlan = VLAN.objects.get(group=vlan_group, vid=vlan_vid)
  151. except VLAN.DoesNotExist:
  152. self.add_error('vlan_vid', "Invalid VLAN ID ({} - {}).".format(vlan_group, vlan_vid))
  153. elif vlan_vid and site:
  154. try:
  155. self.instance.vlan = VLAN.objects.get(site=site, vid=vlan_vid)
  156. except VLAN.MultipleObjectsReturned:
  157. self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
  158. elif vlan_vid:
  159. self.add_error('vlan_vid', "Must specify site and/or VLAN group when assigning a VLAN.")
  160. def save(self, *args, **kwargs):
  161. m = super(PrefixFromCSVForm, self).save(commit=False)
  162. # Assign Prefix status by name
  163. m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  164. if kwargs.get('commit'):
  165. m.save()
  166. return m
  167. class PrefixImportForm(BulkImportForm, BootstrapMixin):
  168. csv = CSVDataField(csv_form=PrefixFromCSVForm)
  169. class PrefixBulkEditForm(forms.Form, BootstrapMixin):
  170. pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
  171. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  172. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
  173. help_text="Select the VRF to assign, or check below to remove VRF assignment")
  174. vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
  175. status = forms.ChoiceField(choices=FORM_PREFIX_STATUS_CHOICES, required=False)
  176. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  177. description = forms.CharField(max_length=50, required=False)
  178. def prefix_vrf_choices():
  179. vrf_choices = [('', 'All'), (0, 'Global')]
  180. vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
  181. return vrf_choices
  182. def prefix_site_choices():
  183. site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
  184. return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
  185. def prefix_status_choices():
  186. status_counts = {}
  187. for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
  188. status_counts[status['status']] = status['count']
  189. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
  190. def prefix_role_choices():
  191. role_choices = Role.objects.annotate(prefix_count=Count('prefixes'))
  192. return [(r.slug, u'{} ({})'.format(r.name, r.prefix_count)) for r in role_choices]
  193. class PrefixFilterForm(forms.Form, BootstrapMixin):
  194. parent = forms.CharField(required=False, label='Search Within')
  195. vrf = forms.ChoiceField(required=False, choices=prefix_vrf_choices, label='VRF')
  196. status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices)
  197. site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
  198. widget=forms.SelectMultiple(attrs={'size': 8}))
  199. role = forms.MultipleChoiceField(required=False, choices=prefix_role_choices,
  200. widget=forms.SelectMultiple(attrs={'size': 8}))
  201. expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
  202. #
  203. # IP addresses
  204. #
  205. class IPAddressForm(forms.ModelForm, BootstrapMixin):
  206. nat_site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, label='Site',
  207. widget=forms.Select(attrs={'filter-for': 'nat_device'}))
  208. nat_device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, label='Device',
  209. widget=APISelect(api_url='/api/dcim/devices/?site_id={{nat_site}}',
  210. attrs={'filter-for': 'nat_inside'}))
  211. livesearch = forms.CharField(required=False, label='IP Address', widget=Livesearch(
  212. query_key='q', query_url='ipam-api:ipaddress_list', field_to_update='nat_inside', obj_label='address')
  213. )
  214. nat_inside = forms.ModelChoiceField(queryset=IPAddress.objects.all(), required=False, label='NAT (Inside)',
  215. widget=APISelect(api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
  216. display_field='address'))
  217. class Meta:
  218. model = IPAddress
  219. fields = ['address', 'vrf', 'nat_device', 'nat_inside', 'description']
  220. help_texts = {
  221. 'address': "IPv4 or IPv6 address and mask",
  222. 'vrf': "VRF (if applicable)",
  223. }
  224. def __init__(self, *args, **kwargs):
  225. super(IPAddressForm, self).__init__(*args, **kwargs)
  226. self.fields['vrf'].empty_label = 'Global'
  227. if self.instance.nat_inside:
  228. nat_inside = self.instance.nat_inside
  229. # If the IP is assigned to an interface, populate site/device fields accordingly
  230. if self.instance.nat_inside.interface:
  231. self.initial['nat_site'] = self.instance.nat_inside.interface.device.rack.site.pk
  232. self.initial['nat_device'] = self.instance.nat_inside.interface.device.pk
  233. self.fields['nat_device'].queryset = Device.objects.filter(
  234. rack__site=nat_inside.interface.device.rack.site)
  235. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  236. interface__device=nat_inside.interface.device)
  237. else:
  238. self.fields['nat_inside'].queryset = IPAddress.objects.filter(pk=nat_inside.pk)
  239. else:
  240. # Initialize nat_device choices if nat_site is set
  241. if self.is_bound and self.data.get('nat_site'):
  242. self.fields['nat_device'].queryset = Device.objects.filter(rack__site__pk=self.data['nat_site'])
  243. elif self.initial.get('nat_site'):
  244. self.fields['nat_device'].queryset = Device.objects.filter(rack__site=self.initial['nat_site'])
  245. else:
  246. self.fields['nat_device'].choices = []
  247. # Initialize nat_inside choices if nat_device is set
  248. if self.is_bound and self.data.get('nat_device'):
  249. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  250. interface__device__pk=self.data['nat_device'])
  251. elif self.initial.get('nat_device'):
  252. self.fields['nat_inside'].queryset = IPAddress.objects.filter(
  253. interface__device__pk=self.initial['nat_device'])
  254. else:
  255. self.fields['nat_inside'].choices = []
  256. class IPAddressFromCSVForm(forms.ModelForm):
  257. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  258. error_messages={'invalid_choice': 'VRF not found.'})
  259. device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
  260. error_messages={'invalid_choice': 'Device not found.'})
  261. interface_name = forms.CharField(required=False)
  262. is_primary = forms.BooleanField(required=False)
  263. class Meta:
  264. model = IPAddress
  265. fields = ['address', 'vrf', 'device', 'interface_name', 'is_primary', 'description']
  266. def clean(self):
  267. device = self.cleaned_data.get('device')
  268. interface_name = self.cleaned_data.get('interface_name')
  269. is_primary = self.cleaned_data.get('is_primary')
  270. # Validate interface
  271. if device and interface_name:
  272. try:
  273. Interface.objects.get(device=device, name=interface_name)
  274. except Interface.DoesNotExist:
  275. self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
  276. elif device and not interface_name:
  277. self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
  278. elif interface_name and not device:
  279. self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
  280. # Validate is_primary
  281. if is_primary and not device:
  282. self.add_error('is_primary', "No device specified; cannot set as primary IP")
  283. def save(self, commit=True):
  284. # Set interface
  285. if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
  286. self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
  287. name=self.cleaned_data['interface_name'])
  288. # Set as primary for device
  289. if self.cleaned_data['is_primary']:
  290. if self.instance.address.version == 4:
  291. self.instance.primary_ip4_for = self.cleaned_data['device']
  292. elif self.instance.address.version == 6:
  293. self.instance.primary_ip6_for = self.cleaned_data['device']
  294. return super(IPAddressFromCSVForm, self).save(commit=commit)
  295. class IPAddressImportForm(BulkImportForm, BootstrapMixin):
  296. csv = CSVDataField(csv_form=IPAddressFromCSVForm)
  297. class IPAddressBulkEditForm(forms.Form, BootstrapMixin):
  298. pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
  299. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF',
  300. help_text="Select the VRF to assign, or check below to remove VRF assignment")
  301. vrf_global = forms.BooleanField(required=False, label='Set VRF to global')
  302. description = forms.CharField(max_length=50, required=False)
  303. def ipaddress_family_choices():
  304. return [('', 'All'), (4, 'IPv4'), (6, 'IPv6')]
  305. def ipaddress_vrf_choices():
  306. vrf_choices = [('', 'All'), (0, 'Global')]
  307. vrf_choices += [(v.pk, v.name) for v in VRF.objects.all()]
  308. return vrf_choices
  309. class IPAddressFilterForm(forms.Form, BootstrapMixin):
  310. family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
  311. vrf = forms.ChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF')
  312. #
  313. # VLAN groups
  314. #
  315. class VLANGroupForm(forms.ModelForm, BootstrapMixin):
  316. slug = SlugField()
  317. class Meta:
  318. model = VLANGroup
  319. fields = ['site', 'name', 'slug']
  320. def vlangroup_site_choices():
  321. site_choices = Site.objects.annotate(vlangroup_count=Count('vlan_groups'))
  322. return [(s.slug, u'{} ({})'.format(s.name, s.vlangroup_count)) for s in site_choices]
  323. class VLANGroupFilterForm(forms.Form, BootstrapMixin):
  324. site = forms.MultipleChoiceField(required=False, choices=vlangroup_site_choices,
  325. widget=forms.SelectMultiple(attrs={'size': 8}))
  326. #
  327. # VLANs
  328. #
  329. class VLANForm(forms.ModelForm, BootstrapMixin):
  330. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, label='Group', widget=APISelect(
  331. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  332. ))
  333. class Meta:
  334. model = VLAN
  335. fields = ['site', 'group', 'vid', 'name', 'description', 'status', 'role']
  336. help_texts = {
  337. 'site': "The site at which this VLAN exists",
  338. 'group': "VLAN group (optional)",
  339. 'vid': "Configured VLAN ID",
  340. 'name': "Configured VLAN name",
  341. 'status': "Operational status of this VLAN",
  342. 'role': "The primary function of this VLAN",
  343. }
  344. widgets = {
  345. 'site': forms.Select(attrs={'filter-for': 'group'}),
  346. }
  347. def __init__(self, *args, **kwargs):
  348. super(VLANForm, self).__init__(*args, **kwargs)
  349. # Limit VLAN group choices
  350. if self.is_bound and self.data.get('site'):
  351. self.fields['group'].queryset = VLANGroup.objects.filter(site__pk=self.data['site'])
  352. elif self.initial.get('site'):
  353. self.fields['group'].queryset = VLANGroup.objects.filter(site=self.initial['site'])
  354. else:
  355. self.fields['group'].choices = []
  356. class VLANFromCSVForm(forms.ModelForm):
  357. site = forms.ModelChoiceField(queryset=Site.objects.all(), to_field_name='name',
  358. error_messages={'invalid_choice': 'Device not found.'})
  359. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False, to_field_name='name',
  360. error_messages={'invalid_choice': 'VLAN group not found.'})
  361. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
  362. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
  363. error_messages={'invalid_choice': 'Invalid role.'})
  364. class Meta:
  365. model = VLAN
  366. fields = ['site', 'group', 'vid', 'name', 'status_name', 'role', 'description']
  367. def save(self, *args, **kwargs):
  368. m = super(VLANFromCSVForm, self).save(commit=False)
  369. # Assign VLAN status by name
  370. m.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  371. if kwargs.get('commit'):
  372. m.save()
  373. return m
  374. class VLANImportForm(BulkImportForm, BootstrapMixin):
  375. csv = CSVDataField(csv_form=VLANFromCSVForm)
  376. class VLANBulkEditForm(forms.Form, BootstrapMixin):
  377. pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
  378. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  379. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
  380. status = forms.ChoiceField(choices=FORM_VLAN_STATUS_CHOICES, required=False)
  381. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  382. description = forms.CharField(max_length=100, required=False)
  383. def vlan_site_choices():
  384. site_choices = Site.objects.annotate(vlan_count=Count('vlans'))
  385. return [(s.slug, u'{} ({})'.format(s.name, s.vlan_count)) for s in site_choices]
  386. def vlan_group_choices():
  387. group_choices = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
  388. return [(g.pk, u'{} ({})'.format(g, g.vlan_count)) for g in group_choices]
  389. def vlan_status_choices():
  390. status_counts = {}
  391. for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
  392. status_counts[status['status']] = status['count']
  393. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
  394. def vlan_role_choices():
  395. role_choices = Role.objects.annotate(vlan_count=Count('vlans'))
  396. return [(r.slug, u'{} ({})'.format(r.name, r.vlan_count)) for r in role_choices]
  397. class VLANFilterForm(forms.Form, BootstrapMixin):
  398. site = forms.MultipleChoiceField(required=False, choices=vlan_site_choices,
  399. widget=forms.SelectMultiple(attrs={'size': 8}))
  400. group_id = forms.MultipleChoiceField(required=False, choices=vlan_group_choices, label='VLAN Group',
  401. widget=forms.SelectMultiple(attrs={'size': 8}))
  402. status = forms.MultipleChoiceField(required=False, choices=vlan_status_choices)
  403. role = forms.MultipleChoiceField(required=False, choices=vlan_role_choices,
  404. widget=forms.SelectMultiple(attrs={'size': 8}))