forms.py 30 KB

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