forms.py 19 KB

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