forms.py 19 KB

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