forms.py 28 KB

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