forms.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. from django import forms
  2. from django.db.models import Count
  3. from dcim.models import Site, Rack, Device, Interface
  4. from extras.forms import CustomFieldForm, CustomFieldBulkEditForm, CustomFieldFilterForm
  5. from tenancy.forms import TenancyForm
  6. from tenancy.models import Tenant
  7. from utilities.forms import (
  8. APISelect, BootstrapMixin, BulkEditNullBooleanSelect, BulkImportForm, ChainedModelChoiceField, CSVDataField,
  9. ExpandableIPAddressField, FilterChoiceField, Livesearch, ReturnURLForm, SlugField, add_blank_choice,
  10. )
  11. from .models import (
  12. Aggregate, IPAddress, IPADDRESS_STATUS_CHOICES, Prefix, PREFIX_STATUS_CHOICES, RIR, Role, Service, VLAN,
  13. VLANGroup, VLAN_STATUS_CHOICES, VRF,
  14. )
  15. IP_FAMILY_CHOICES = [
  16. ('', 'All'),
  17. (4, 'IPv4'),
  18. (6, 'IPv6'),
  19. ]
  20. PREFIX_MASK_LENGTH_CHOICES = [
  21. ('', '---------'),
  22. ] + [(i, i) for i in range(1, 128)]
  23. IPADDRESS_MASK_LENGTH_CHOICES = PREFIX_MASK_LENGTH_CHOICES + [(128, 128)]
  24. #
  25. # VRFs
  26. #
  27. class VRFForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  28. class Meta:
  29. model = VRF
  30. fields = ['name', 'rd', 'enforce_unique', 'description', 'tenant_group', 'tenant']
  31. labels = {
  32. 'rd': "RD",
  33. }
  34. help_texts = {
  35. 'rd': "Route distinguisher in any format",
  36. }
  37. class VRFFromCSVForm(forms.ModelForm):
  38. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  39. error_messages={'invalid_choice': 'Tenant not found.'})
  40. class Meta:
  41. model = VRF
  42. fields = ['name', 'rd', 'tenant', 'enforce_unique', 'description']
  43. class VRFImportForm(BootstrapMixin, BulkImportForm):
  44. csv = CSVDataField(csv_form=VRFFromCSVForm)
  45. class VRFBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  46. pk = forms.ModelMultipleChoiceField(queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput)
  47. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  48. enforce_unique = forms.NullBooleanField(
  49. required=False, widget=BulkEditNullBooleanSelect, label='Enforce unique space'
  50. )
  51. description = forms.CharField(max_length=100, required=False)
  52. class Meta:
  53. nullable_fields = ['tenant', 'description']
  54. class VRFFilterForm(BootstrapMixin, CustomFieldFilterForm):
  55. model = VRF
  56. q = forms.CharField(required=False, label='Search')
  57. tenant = FilterChoiceField(queryset=Tenant.objects.annotate(filter_count=Count('vrfs')), to_field_name='slug',
  58. null_option=(0, None))
  59. #
  60. # RIRs
  61. #
  62. class RIRForm(BootstrapMixin, forms.ModelForm):
  63. slug = SlugField()
  64. class Meta:
  65. model = RIR
  66. fields = ['name', 'slug', 'is_private']
  67. class RIRFilterForm(BootstrapMixin, forms.Form):
  68. is_private = forms.NullBooleanField(required=False, label='Private', widget=forms.Select(choices=[
  69. ('', '---------'),
  70. ('True', 'Yes'),
  71. ('False', 'No'),
  72. ]))
  73. #
  74. # Aggregates
  75. #
  76. class AggregateForm(BootstrapMixin, CustomFieldForm):
  77. class Meta:
  78. model = Aggregate
  79. fields = ['prefix', 'rir', 'date_added', 'description']
  80. help_texts = {
  81. 'prefix': "IPv4 or IPv6 network",
  82. 'rir': "Regional Internet Registry responsible for this prefix",
  83. 'date_added': "Format: YYYY-MM-DD",
  84. }
  85. class AggregateFromCSVForm(forms.ModelForm):
  86. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), to_field_name='name',
  87. error_messages={'invalid_choice': 'RIR not found.'})
  88. class Meta:
  89. model = Aggregate
  90. fields = ['prefix', 'rir', 'date_added', 'description']
  91. class AggregateImportForm(BootstrapMixin, BulkImportForm):
  92. csv = CSVDataField(csv_form=AggregateFromCSVForm)
  93. class AggregateBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  94. pk = forms.ModelMultipleChoiceField(queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput)
  95. rir = forms.ModelChoiceField(queryset=RIR.objects.all(), required=False, label='RIR')
  96. date_added = forms.DateField(required=False)
  97. description = forms.CharField(max_length=100, required=False)
  98. class Meta:
  99. nullable_fields = ['date_added', 'description']
  100. class AggregateFilterForm(BootstrapMixin, CustomFieldFilterForm):
  101. model = Aggregate
  102. q = forms.CharField(required=False, label='Search')
  103. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address Family')
  104. rir = FilterChoiceField(
  105. queryset=RIR.objects.annotate(filter_count=Count('aggregates')),
  106. to_field_name='slug',
  107. label='RIR'
  108. )
  109. #
  110. # Roles
  111. #
  112. class RoleForm(BootstrapMixin, forms.ModelForm):
  113. slug = SlugField()
  114. class Meta:
  115. model = Role
  116. fields = ['name', 'slug']
  117. #
  118. # Prefixes
  119. #
  120. class PrefixForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  121. site = forms.ModelChoiceField(
  122. queryset=Site.objects.all(), required=False, label='Site', widget=forms.Select(
  123. attrs={'filter-for': 'vlan', 'nullable': 'true'}
  124. )
  125. )
  126. vlan = ChainedModelChoiceField(
  127. queryset=VLAN.objects.all(), chains={'site': 'site'}, required=False, label='VLAN', widget=APISelect(
  128. api_url='/api/ipam/vlans/?site_id={{site}}', display_field='display_name'
  129. )
  130. )
  131. class Meta:
  132. model = Prefix
  133. fields = ['prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'description', 'tenant_group', 'tenant']
  134. def __init__(self, *args, **kwargs):
  135. super(PrefixForm, self).__init__(*args, **kwargs)
  136. self.fields['vrf'].empty_label = 'Global'
  137. class PrefixFromCSVForm(forms.ModelForm):
  138. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  139. error_messages={'invalid_choice': 'VRF not found.'})
  140. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  141. error_messages={'invalid_choice': 'Tenant not found.'})
  142. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False, to_field_name='name',
  143. error_messages={'invalid_choice': 'Site not found.'})
  144. vlan_group_name = forms.CharField(required=False)
  145. vlan_vid = forms.IntegerField(required=False)
  146. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in PREFIX_STATUS_CHOICES])
  147. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False, to_field_name='name',
  148. error_messages={'invalid_choice': 'Invalid role.'})
  149. class Meta:
  150. model = Prefix
  151. fields = ['prefix', 'vrf', 'tenant', 'site', 'vlan_group_name', 'vlan_vid', 'status_name', 'role', 'is_pool',
  152. 'description']
  153. def clean(self):
  154. super(PrefixFromCSVForm, self).clean()
  155. site = self.cleaned_data.get('site')
  156. vlan_group_name = self.cleaned_data.get('vlan_group_name')
  157. vlan_vid = self.cleaned_data.get('vlan_vid')
  158. vlan_group = None
  159. # Validate VLAN group
  160. if vlan_group_name:
  161. try:
  162. vlan_group = VLANGroup.objects.get(site=site, name=vlan_group_name)
  163. except VLANGroup.DoesNotExist:
  164. if site:
  165. self.add_error('vlan_group_name', "Invalid VLAN group ({} - {}).".format(site, vlan_group_name))
  166. else:
  167. self.add_error('vlan_group_name', "Invalid global VLAN group ({}).".format(vlan_group_name))
  168. # Validate VLAN
  169. if vlan_vid:
  170. try:
  171. self.instance.vlan = VLAN.objects.get(site=site, group=vlan_group, vid=vlan_vid)
  172. except VLAN.DoesNotExist:
  173. if site:
  174. self.add_error('vlan_vid', "Invalid VLAN ID ({}) for site {}.".format(vlan_vid, site))
  175. elif vlan_group:
  176. self.add_error('vlan_vid', "Invalid VLAN ID ({}) for group {}.".format(vlan_vid, vlan_group_name))
  177. elif not vlan_group_name:
  178. self.add_error('vlan_vid', "Invalid global VLAN ID ({}).".format(vlan_vid))
  179. except VLAN.MultipleObjectsReturned:
  180. self.add_error('vlan_vid', "Multiple VLANs found ({} - VID {})".format(site, vlan_vid))
  181. def save(self, *args, **kwargs):
  182. # Assign Prefix status by name
  183. self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  184. return super(PrefixFromCSVForm, self).save(*args, **kwargs)
  185. class PrefixImportForm(BootstrapMixin, BulkImportForm):
  186. csv = CSVDataField(csv_form=PrefixFromCSVForm)
  187. class PrefixBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  188. pk = forms.ModelMultipleChoiceField(queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput)
  189. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  190. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  191. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  192. status = forms.ChoiceField(choices=add_blank_choice(PREFIX_STATUS_CHOICES), required=False)
  193. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  194. is_pool = forms.NullBooleanField(required=False, widget=BulkEditNullBooleanSelect, label='Is a pool')
  195. description = forms.CharField(max_length=100, required=False)
  196. class Meta:
  197. nullable_fields = ['site', 'vrf', 'tenant', 'role', 'description']
  198. def prefix_status_choices():
  199. status_counts = {}
  200. for status in Prefix.objects.values('status').annotate(count=Count('status')).order_by('status'):
  201. status_counts[status['status']] = status['count']
  202. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in PREFIX_STATUS_CHOICES]
  203. class PrefixFilterForm(BootstrapMixin, CustomFieldFilterForm):
  204. model = Prefix
  205. q = forms.CharField(required=False, label='Search')
  206. parent = forms.CharField(required=False, label='Parent prefix', widget=forms.TextInput(attrs={
  207. 'placeholder': 'Prefix',
  208. }))
  209. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
  210. mask_length = forms.ChoiceField(required=False, choices=PREFIX_MASK_LENGTH_CHOICES, label='Mask length')
  211. vrf = FilterChoiceField(
  212. queryset=VRF.objects.annotate(filter_count=Count('prefixes')),
  213. to_field_name='rd',
  214. label='VRF',
  215. null_option=(0, 'Global')
  216. )
  217. tenant = FilterChoiceField(
  218. queryset=Tenant.objects.annotate(filter_count=Count('prefixes')),
  219. to_field_name='slug',
  220. null_option=(0, 'None')
  221. )
  222. status = forms.MultipleChoiceField(choices=prefix_status_choices, required=False)
  223. site = FilterChoiceField(
  224. queryset=Site.objects.annotate(filter_count=Count('prefixes')),
  225. to_field_name='slug',
  226. null_option=(0, 'None')
  227. )
  228. role = FilterChoiceField(
  229. queryset=Role.objects.annotate(filter_count=Count('prefixes')),
  230. to_field_name='slug',
  231. null_option=(0, 'None')
  232. )
  233. expand = forms.BooleanField(required=False, label='Expand prefix hierarchy')
  234. #
  235. # IP addresses
  236. #
  237. class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldForm):
  238. interface_site = forms.ModelChoiceField(
  239. queryset=Site.objects.all(),
  240. required=False,
  241. label='Site',
  242. widget=forms.Select(
  243. attrs={'filter-for': 'interface_rack'}
  244. )
  245. )
  246. interface_rack = ChainedModelChoiceField(
  247. queryset=Rack.objects.all(),
  248. chains={'site': 'interface_site'},
  249. required=False,
  250. label='Rack',
  251. widget=APISelect(
  252. api_url='/api/dcim/racks/?site_id={{interface_site}}',
  253. display_field='display_name',
  254. attrs={'filter-for': 'interface_device', 'nullable': 'true'}
  255. )
  256. )
  257. interface_device = ChainedModelChoiceField(
  258. queryset=Device.objects.all(),
  259. chains={'site': 'interface_site', 'rack': 'interface_rack'},
  260. required=False,
  261. label='Device',
  262. widget=APISelect(
  263. api_url='/api/dcim/devices/?site_id={{interface_site}}&rack_id={{interface_rack}}',
  264. display_field='display_name',
  265. attrs={'filter-for': 'interface'}
  266. )
  267. )
  268. interface = ChainedModelChoiceField(
  269. queryset=Interface.objects.all(),
  270. chains={'device': 'interface_device'},
  271. required=False,
  272. widget=APISelect(
  273. api_url='/api/dcim/interfaces/?device_id={{interface_device}}'
  274. )
  275. )
  276. nat_site = forms.ModelChoiceField(
  277. queryset=Site.objects.all(),
  278. required=False,
  279. label='Site',
  280. widget=forms.Select(
  281. attrs={'filter-for': 'nat_device'}
  282. )
  283. )
  284. nat_rack = ChainedModelChoiceField(
  285. queryset=Rack.objects.all(),
  286. chains={'site': 'nat_site'},
  287. required=False,
  288. label='Rack',
  289. widget=APISelect(
  290. api_url='/api/dcim/racks/?site_id={{interface_site}}',
  291. display_field='display_name',
  292. attrs={'filter-for': 'nat_device', 'nullable': 'true'}
  293. )
  294. )
  295. nat_device = ChainedModelChoiceField(
  296. queryset=Device.objects.all(),
  297. chains={'site': 'nat_site'},
  298. required=False,
  299. label='Device',
  300. widget=APISelect(
  301. api_url='/api/dcim/devices/?site_id={{nat_site}}',
  302. display_field='display_name',
  303. attrs={'filter-for': 'nat_inside'}
  304. )
  305. )
  306. nat_inside = ChainedModelChoiceField(
  307. queryset=IPAddress.objects.all(),
  308. chains={'interface__device': 'nat_device'},
  309. required=False,
  310. label='IP Address',
  311. widget=APISelect(
  312. api_url='/api/ipam/ip-addresses/?device_id={{nat_device}}',
  313. display_field='address'
  314. )
  315. )
  316. livesearch = forms.CharField(
  317. required=False,
  318. label='IP Address',
  319. widget=Livesearch(
  320. query_key='q',
  321. query_url='ipam-api:ipaddress-list',
  322. field_to_update='nat_inside',
  323. obj_label='address'
  324. )
  325. )
  326. primary_for_device = forms.BooleanField(required=False, label='Make this the primary IP for the device')
  327. class Meta:
  328. model = IPAddress
  329. fields = [
  330. 'address', 'vrf', 'status', 'description', 'interface', 'primary_for_device', 'nat_inside', 'tenant_group',
  331. 'tenant',
  332. ]
  333. def __init__(self, *args, **kwargs):
  334. # Initialize helper selectors
  335. instance = kwargs.get('instance')
  336. initial = kwargs.get('initial', {})
  337. if instance and instance.interface is not None:
  338. initial['interface_site'] = instance.interface.device.site
  339. initial['interface_rack'] = instance.interface.device.rack
  340. initial['interface_device'] = instance.interface.device
  341. if instance and instance.nat_inside is not None:
  342. initial['nat_site'] = instance.nat_inside.device.site
  343. initial['nat_rack'] = instance.nat_inside.device.rack
  344. initial['nat_device'] = instance.nat_inside.device
  345. kwargs['initial'] = initial
  346. super(IPAddressForm, self).__init__(*args, **kwargs)
  347. self.fields['vrf'].empty_label = 'Global'
  348. # Initialize primary_for_device if IP address is already assigned
  349. if self.instance.interface is not None:
  350. device = self.instance.interface.device
  351. if (
  352. self.instance.address.version == 4 and device.primary_ip4 == self.instance or
  353. self.instance.address.version == 6 and device.primary_ip6 == self.instance
  354. ):
  355. self.initial['primary_for_device'] = True
  356. def clean(self):
  357. super(IPAddressForm, self).clean()
  358. # Primary IP assignment is only available if an interface has been assigned.
  359. if self.cleaned_data.get('primary_for_device') and not self.cleaned_data.get('interface'):
  360. self.add_error(
  361. 'primary_for_device', "Only IP addresses assigned to an interface can be designated as primary IPs."
  362. )
  363. def save(self, *args, **kwargs):
  364. ipaddress = super(IPAddressForm, self).save(*args, **kwargs)
  365. # Assign this IPAddress as the primary for the associated Device.
  366. if self.cleaned_data['primary_for_device']:
  367. device = self.cleaned_data['interface'].device
  368. if ipaddress.address.version == 4:
  369. device.primary_ip4 = ipaddress
  370. else:
  371. device.primary_ip6 = ipaddress
  372. device.save()
  373. # Clear assignment as primary for device if set.
  374. else:
  375. try:
  376. if ipaddress.address.version == 4:
  377. device = ipaddress.primary_ip4_for
  378. device.primary_ip4 = None
  379. else:
  380. device = ipaddress.primary_ip6_for
  381. device.primary_ip6 = None
  382. device.save()
  383. except Device.DoesNotExist:
  384. pass
  385. return ipaddress
  386. class IPAddressPatternForm(BootstrapMixin, forms.Form):
  387. pattern = ExpandableIPAddressField(label='Address pattern')
  388. class IPAddressBulkAddForm(BootstrapMixin, CustomFieldForm):
  389. class Meta:
  390. model = IPAddress
  391. fields = ['address', 'status', 'vrf', 'tenant', 'description']
  392. def __init__(self, *args, **kwargs):
  393. super(IPAddressBulkAddForm, self).__init__(*args, **kwargs)
  394. self.fields['vrf'].empty_label = 'Global'
  395. class IPAddressFromCSVForm(forms.ModelForm):
  396. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, to_field_name='rd',
  397. error_messages={'invalid_choice': 'VRF not found.'})
  398. tenant = forms.ModelChoiceField(Tenant.objects.all(), to_field_name='name', required=False,
  399. error_messages={'invalid_choice': 'Tenant not found.'})
  400. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in IPADDRESS_STATUS_CHOICES])
  401. device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
  402. error_messages={'invalid_choice': 'Device not found.'})
  403. interface_name = forms.CharField(required=False)
  404. is_primary = forms.BooleanField(required=False)
  405. class Meta:
  406. model = IPAddress
  407. fields = ['address', 'vrf', 'tenant', 'status_name', 'device', 'interface_name', 'is_primary', 'description']
  408. def clean(self):
  409. device = self.cleaned_data.get('device')
  410. interface_name = self.cleaned_data.get('interface_name')
  411. is_primary = self.cleaned_data.get('is_primary')
  412. # Validate interface
  413. if device and interface_name:
  414. try:
  415. Interface.objects.get(device=device, name=interface_name)
  416. except Interface.DoesNotExist:
  417. self.add_error('interface_name', "Invalid interface ({}) for {}".format(interface_name, device))
  418. elif device and not interface_name:
  419. self.add_error('interface_name', "Device set ({}) but interface missing".format(device))
  420. elif interface_name and not device:
  421. self.add_error('device', "Interface set ({}) but device missing or invalid".format(interface_name))
  422. # Validate is_primary
  423. if is_primary and not device:
  424. self.add_error('is_primary', "No device specified; cannot set as primary IP")
  425. def save(self, *args, **kwargs):
  426. # Assign status by name
  427. self.instance.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  428. # Set interface
  429. if self.cleaned_data['device'] and self.cleaned_data['interface_name']:
  430. self.instance.interface = Interface.objects.get(device=self.cleaned_data['device'],
  431. name=self.cleaned_data['interface_name'])
  432. # Set as primary for device
  433. if self.cleaned_data['is_primary']:
  434. if self.instance.address.version == 4:
  435. self.instance.primary_ip4_for = self.cleaned_data['device']
  436. elif self.instance.address.version == 6:
  437. self.instance.primary_ip6_for = self.cleaned_data['device']
  438. return super(IPAddressFromCSVForm, self).save(*args, **kwargs)
  439. class IPAddressImportForm(BootstrapMixin, BulkImportForm):
  440. csv = CSVDataField(csv_form=IPAddressFromCSVForm)
  441. class IPAddressBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  442. pk = forms.ModelMultipleChoiceField(queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput)
  443. vrf = forms.ModelChoiceField(queryset=VRF.objects.all(), required=False, label='VRF')
  444. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  445. status = forms.ChoiceField(choices=add_blank_choice(IPADDRESS_STATUS_CHOICES), required=False)
  446. description = forms.CharField(max_length=100, required=False)
  447. class Meta:
  448. nullable_fields = ['vrf', 'tenant', 'description']
  449. def ipaddress_status_choices():
  450. status_counts = {}
  451. for status in IPAddress.objects.values('status').annotate(count=Count('status')).order_by('status'):
  452. status_counts[status['status']] = status['count']
  453. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in IPADDRESS_STATUS_CHOICES]
  454. class IPAddressFilterForm(BootstrapMixin, CustomFieldFilterForm):
  455. model = IPAddress
  456. q = forms.CharField(required=False, label='Search')
  457. parent = forms.CharField(required=False, label='Parent Prefix', widget=forms.TextInput(attrs={
  458. 'placeholder': 'Prefix',
  459. }))
  460. family = forms.ChoiceField(required=False, choices=IP_FAMILY_CHOICES, label='Address family')
  461. mask_length = forms.ChoiceField(required=False, choices=IPADDRESS_MASK_LENGTH_CHOICES, label='Mask length')
  462. vrf = FilterChoiceField(
  463. queryset=VRF.objects.annotate(filter_count=Count('ip_addresses')),
  464. to_field_name='rd',
  465. label='VRF',
  466. null_option=(0, 'Global')
  467. )
  468. tenant = FilterChoiceField(
  469. queryset=Tenant.objects.annotate(filter_count=Count('ip_addresses')),
  470. to_field_name='slug',
  471. null_option=(0, 'None')
  472. )
  473. status = forms.MultipleChoiceField(choices=ipaddress_status_choices, required=False)
  474. #
  475. # VLAN groups
  476. #
  477. class VLANGroupForm(BootstrapMixin, forms.ModelForm):
  478. slug = SlugField()
  479. class Meta:
  480. model = VLANGroup
  481. fields = ['site', 'name', 'slug']
  482. class VLANGroupFilterForm(BootstrapMixin, forms.Form):
  483. site = FilterChoiceField(
  484. queryset=Site.objects.annotate(filter_count=Count('vlan_groups')),
  485. to_field_name='slug',
  486. null_option=(0, 'Global')
  487. )
  488. #
  489. # VLANs
  490. #
  491. class VLANForm(BootstrapMixin, TenancyForm, CustomFieldForm):
  492. site = forms.ModelChoiceField(
  493. queryset=Site.objects.all(),
  494. widget=forms.Select(
  495. attrs={'filter-for': 'group', 'nullable': 'true'}
  496. )
  497. )
  498. group = ChainedModelChoiceField(
  499. queryset=VLANGroup.objects.all(),
  500. chains={'site': 'site'},
  501. required=False,
  502. label='Group',
  503. widget=APISelect(
  504. api_url='/api/ipam/vlan-groups/?site_id={{site}}',
  505. )
  506. )
  507. class Meta:
  508. model = VLAN
  509. fields = ['site', 'group', 'vid', 'name', 'status', 'role', 'description', 'tenant_group', 'tenant']
  510. help_texts = {
  511. 'site': "Leave blank if this VLAN spans multiple sites",
  512. 'group': "VLAN group (optional)",
  513. 'vid': "Configured VLAN ID",
  514. 'name': "Configured VLAN name",
  515. 'status': "Operational status of this VLAN",
  516. 'role': "The primary function of this VLAN",
  517. }
  518. class VLANFromCSVForm(forms.ModelForm):
  519. site = forms.ModelChoiceField(
  520. queryset=Site.objects.all(), required=False, to_field_name='name',
  521. error_messages={'invalid_choice': 'Site not found.'}
  522. )
  523. group_name = forms.CharField(required=False)
  524. tenant = forms.ModelChoiceField(
  525. Tenant.objects.all(), to_field_name='name', required=False,
  526. error_messages={'invalid_choice': 'Tenant not found.'}
  527. )
  528. status_name = forms.ChoiceField(choices=[(s[1], s[0]) for s in VLAN_STATUS_CHOICES])
  529. role = forms.ModelChoiceField(
  530. queryset=Role.objects.all(), required=False, to_field_name='name',
  531. error_messages={'invalid_choice': 'Invalid role.'}
  532. )
  533. class Meta:
  534. model = VLAN
  535. fields = ['site', 'group_name', 'vid', 'name', 'tenant', 'status_name', 'role', 'description']
  536. def clean(self):
  537. super(VLANFromCSVForm, self).clean()
  538. # Validate VLANGroup
  539. group_name = self.cleaned_data.get('group_name')
  540. if group_name:
  541. try:
  542. VLANGroup.objects.get(site=self.cleaned_data.get('site'), name=group_name)
  543. except VLANGroup.DoesNotExist:
  544. self.add_error('group_name', "Invalid VLAN group {}.".format(group_name))
  545. def save(self, *args, **kwargs):
  546. vlan = super(VLANFromCSVForm, self).save(commit=False)
  547. # Assign VLANGroup by site and name
  548. if self.cleaned_data['group_name']:
  549. vlan.group = VLANGroup.objects.get(site=self.cleaned_data['site'], name=self.cleaned_data['group_name'])
  550. # Assign VLAN status by name
  551. vlan.status = dict(self.fields['status_name'].choices)[self.cleaned_data['status_name']]
  552. if kwargs.get('commit'):
  553. vlan.save()
  554. return vlan
  555. class VLANImportForm(BootstrapMixin, BulkImportForm):
  556. csv = CSVDataField(csv_form=VLANFromCSVForm)
  557. class VLANBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
  558. pk = forms.ModelMultipleChoiceField(queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput)
  559. site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
  560. group = forms.ModelChoiceField(queryset=VLANGroup.objects.all(), required=False)
  561. tenant = forms.ModelChoiceField(queryset=Tenant.objects.all(), required=False)
  562. status = forms.ChoiceField(choices=add_blank_choice(VLAN_STATUS_CHOICES), required=False)
  563. role = forms.ModelChoiceField(queryset=Role.objects.all(), required=False)
  564. description = forms.CharField(max_length=100, required=False)
  565. class Meta:
  566. nullable_fields = ['group', 'tenant', 'role', 'description']
  567. def vlan_status_choices():
  568. status_counts = {}
  569. for status in VLAN.objects.values('status').annotate(count=Count('status')).order_by('status'):
  570. status_counts[status['status']] = status['count']
  571. return [(s[0], u'{} ({})'.format(s[1], status_counts.get(s[0], 0))) for s in VLAN_STATUS_CHOICES]
  572. class VLANFilterForm(BootstrapMixin, CustomFieldFilterForm):
  573. model = VLAN
  574. q = forms.CharField(required=False, label='Search')
  575. site = FilterChoiceField(
  576. queryset=Site.objects.annotate(filter_count=Count('vlans')),
  577. to_field_name='slug',
  578. null_option=(0, 'Global')
  579. )
  580. group_id = FilterChoiceField(
  581. queryset=VLANGroup.objects.annotate(filter_count=Count('vlans')),
  582. label='VLAN group',
  583. null_option=(0, 'None')
  584. )
  585. tenant = FilterChoiceField(
  586. queryset=Tenant.objects.annotate(filter_count=Count('vlans')),
  587. to_field_name='slug',
  588. null_option=(0, 'None')
  589. )
  590. status = forms.MultipleChoiceField(choices=vlan_status_choices, required=False)
  591. role = FilterChoiceField(
  592. queryset=Role.objects.annotate(filter_count=Count('vlans')),
  593. to_field_name='slug',
  594. null_option=(0, 'None')
  595. )
  596. #
  597. # Services
  598. #
  599. class ServiceForm(BootstrapMixin, forms.ModelForm):
  600. class Meta:
  601. model = Service
  602. fields = ['name', 'protocol', 'port', 'ipaddresses', 'description']
  603. help_texts = {
  604. 'ipaddresses': "IP address assignment is optional. If no IPs are selected, the service is assumed to be "
  605. "reachable via all IPs assigned to the device.",
  606. }
  607. def __init__(self, *args, **kwargs):
  608. super(ServiceForm, self).__init__(*args, **kwargs)
  609. # Limit IP address choices to those assigned to interfaces of the parent device
  610. self.fields['ipaddresses'].queryset = IPAddress.objects.filter(interface__device=self.instance.device)