forms.py 17 KB

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