forms.py 32 KB

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