forms.py 30 KB

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