views.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043
  1. import netaddr
  2. from django.conf import settings
  3. from django.contrib.auth.mixins import PermissionRequiredMixin
  4. from django.db.models import Count, Q
  5. from django.db.models.expressions import RawSQL
  6. from django.shortcuts import get_object_or_404, redirect, render
  7. from django.views.generic import View
  8. from django_tables2 import RequestConfig
  9. from dcim.models import Device, Interface
  10. from utilities.paginator import EnhancedPaginator
  11. from utilities.views import (
  12. BulkCreateView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
  13. )
  14. from virtualization.models import VirtualMachine
  15. from . import filters, forms, tables
  16. from .constants import *
  17. from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
  18. def add_available_prefixes(parent, prefix_list):
  19. """
  20. Create fake Prefix objects for all unallocated space within a prefix.
  21. """
  22. # Find all unallocated space
  23. available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
  24. available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
  25. # Concatenate and sort complete list of children
  26. prefix_list = list(prefix_list) + available_prefixes
  27. prefix_list.sort(key=lambda p: p.prefix)
  28. return prefix_list
  29. def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
  30. """
  31. Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
  32. considered usable (regardless of mask length).
  33. """
  34. output = []
  35. prev_ip = None
  36. # Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
  37. if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
  38. first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
  39. last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
  40. else:
  41. first_ip_in_prefix = netaddr.IPAddress(prefix.first)
  42. last_ip_in_prefix = netaddr.IPAddress(prefix.last)
  43. if not ipaddress_list:
  44. return [(
  45. int(last_ip_in_prefix - first_ip_in_prefix + 1),
  46. '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
  47. )]
  48. # Account for any available IPs before the first real IP
  49. if ipaddress_list[0].address.ip > first_ip_in_prefix:
  50. skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
  51. first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
  52. output.append((skipped_count, first_skipped))
  53. # Iterate through existing IPs and annotate free ranges
  54. for ip in ipaddress_list:
  55. if prev_ip:
  56. diff = int(ip.address.ip - prev_ip.address.ip)
  57. if diff > 1:
  58. first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
  59. output.append((diff - 1, first_skipped))
  60. output.append(ip)
  61. prev_ip = ip
  62. # Include any remaining available IPs
  63. if prev_ip.address.ip < last_ip_in_prefix:
  64. skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
  65. first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
  66. output.append((skipped_count, first_skipped))
  67. return output
  68. def add_available_vlans(vlan_group, vlans):
  69. """
  70. Create fake records for all gaps between used VLANs
  71. """
  72. MIN_VLAN = 1
  73. MAX_VLAN = 4094
  74. if not vlans:
  75. return [{'vid': MIN_VLAN, 'available': MAX_VLAN - MIN_VLAN + 1}]
  76. prev_vid = MAX_VLAN
  77. new_vlans = []
  78. for vlan in vlans:
  79. if vlan.vid - prev_vid > 1:
  80. new_vlans.append({'vid': prev_vid + 1, 'available': vlan.vid - prev_vid - 1})
  81. prev_vid = vlan.vid
  82. if vlans[0].vid > MIN_VLAN:
  83. new_vlans.append({'vid': MIN_VLAN, 'available': vlans[0].vid - MIN_VLAN})
  84. if prev_vid < MAX_VLAN:
  85. new_vlans.append({'vid': prev_vid + 1, 'available': MAX_VLAN - prev_vid})
  86. vlans = list(vlans) + new_vlans
  87. vlans.sort(key=lambda v: v.vid if type(v) == VLAN else v['vid'])
  88. return vlans
  89. #
  90. # VRFs
  91. #
  92. class VRFListView(PermissionRequiredMixin, ObjectListView):
  93. permission_required = 'ipam.view_vrf'
  94. queryset = VRF.objects.prefetch_related('tenant')
  95. filter = filters.VRFFilter
  96. filter_form = forms.VRFFilterForm
  97. table = tables.VRFTable
  98. template_name = 'ipam/vrf_list.html'
  99. class VRFView(PermissionRequiredMixin, View):
  100. permission_required = 'ipam.view_vrf'
  101. def get(self, request, pk):
  102. vrf = get_object_or_404(VRF.objects.all(), pk=pk)
  103. prefix_count = Prefix.objects.filter(vrf=vrf).count()
  104. return render(request, 'ipam/vrf.html', {
  105. 'vrf': vrf,
  106. 'prefix_count': prefix_count,
  107. })
  108. class VRFCreateView(PermissionRequiredMixin, ObjectEditView):
  109. permission_required = 'ipam.add_vrf'
  110. model = VRF
  111. model_form = forms.VRFForm
  112. template_name = 'ipam/vrf_edit.html'
  113. default_return_url = 'ipam:vrf_list'
  114. class VRFEditView(VRFCreateView):
  115. permission_required = 'ipam.change_vrf'
  116. class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  117. permission_required = 'ipam.delete_vrf'
  118. model = VRF
  119. default_return_url = 'ipam:vrf_list'
  120. class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
  121. permission_required = 'ipam.add_vrf'
  122. model_form = forms.VRFCSVForm
  123. table = tables.VRFTable
  124. default_return_url = 'ipam:vrf_list'
  125. class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
  126. permission_required = 'ipam.change_vrf'
  127. queryset = VRF.objects.prefetch_related('tenant')
  128. filter = filters.VRFFilter
  129. table = tables.VRFTable
  130. form = forms.VRFBulkEditForm
  131. default_return_url = 'ipam:vrf_list'
  132. class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  133. permission_required = 'ipam.delete_vrf'
  134. queryset = VRF.objects.prefetch_related('tenant')
  135. filter = filters.VRFFilter
  136. table = tables.VRFTable
  137. default_return_url = 'ipam:vrf_list'
  138. #
  139. # RIRs
  140. #
  141. class RIRListView(PermissionRequiredMixin, ObjectListView):
  142. permission_required = 'ipam.view_rir'
  143. queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
  144. filter = filters.RIRFilter
  145. filter_form = forms.RIRFilterForm
  146. table = tables.RIRDetailTable
  147. template_name = 'ipam/rir_list.html'
  148. def alter_queryset(self, request):
  149. if request.GET.get('family') == '6':
  150. family = 6
  151. denominator = 2 ** 64 # Count /64s for IPv6 rather than individual IPs
  152. else:
  153. family = 4
  154. denominator = 1
  155. rirs = []
  156. for rir in self.queryset:
  157. stats = {
  158. 'total': 0,
  159. 'active': 0,
  160. 'reserved': 0,
  161. 'deprecated': 0,
  162. 'available': 0,
  163. }
  164. aggregate_list = Aggregate.objects.filter(family=family, rir=rir)
  165. for aggregate in aggregate_list:
  166. queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
  167. # Find all consumed space for each prefix status (we ignore containers for this purpose).
  168. active_prefixes = netaddr.cidr_merge(
  169. [p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
  170. )
  171. reserved_prefixes = netaddr.cidr_merge(
  172. [p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
  173. )
  174. deprecated_prefixes = netaddr.cidr_merge(
  175. [p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
  176. )
  177. # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
  178. available_prefixes = (
  179. netaddr.IPSet([aggregate.prefix]) -
  180. netaddr.IPSet(active_prefixes) -
  181. netaddr.IPSet(reserved_prefixes) -
  182. netaddr.IPSet(deprecated_prefixes)
  183. )
  184. # Add the size of each metric to the RIR total.
  185. stats['total'] += int(aggregate.prefix.size / denominator)
  186. stats['active'] += int(netaddr.IPSet(active_prefixes).size / denominator)
  187. stats['reserved'] += int(netaddr.IPSet(reserved_prefixes).size / denominator)
  188. stats['deprecated'] += int(netaddr.IPSet(deprecated_prefixes).size / denominator)
  189. stats['available'] += int(available_prefixes.size / denominator)
  190. # Calculate the percentage of total space for each prefix status.
  191. total = float(stats['total'])
  192. stats['percentages'] = {
  193. 'active': float('{:.2f}'.format(stats['active'] / total * 100)) if total else 0,
  194. 'reserved': float('{:.2f}'.format(stats['reserved'] / total * 100)) if total else 0,
  195. 'deprecated': float('{:.2f}'.format(stats['deprecated'] / total * 100)) if total else 0,
  196. }
  197. stats['percentages']['available'] = (
  198. 100 -
  199. stats['percentages']['active'] -
  200. stats['percentages']['reserved'] -
  201. stats['percentages']['deprecated']
  202. )
  203. rir.stats = stats
  204. rirs.append(rir)
  205. return rirs
  206. class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
  207. permission_required = 'ipam.add_rir'
  208. model = RIR
  209. model_form = forms.RIRForm
  210. default_return_url = 'ipam:rir_list'
  211. class RIREditView(RIRCreateView):
  212. permission_required = 'ipam.change_rir'
  213. class RIRBulkImportView(PermissionRequiredMixin, BulkImportView):
  214. permission_required = 'ipam.add_rir'
  215. model_form = forms.RIRCSVForm
  216. table = tables.RIRTable
  217. default_return_url = 'ipam:rir_list'
  218. class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  219. permission_required = 'ipam.delete_rir'
  220. queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
  221. filter = filters.RIRFilter
  222. table = tables.RIRTable
  223. default_return_url = 'ipam:rir_list'
  224. #
  225. # Aggregates
  226. #
  227. class AggregateListView(PermissionRequiredMixin, ObjectListView):
  228. permission_required = 'ipam.view_aggregate'
  229. queryset = Aggregate.objects.prefetch_related('rir').annotate(
  230. child_count=RawSQL('SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix', ())
  231. )
  232. filter = filters.AggregateFilter
  233. filter_form = forms.AggregateFilterForm
  234. table = tables.AggregateDetailTable
  235. template_name = 'ipam/aggregate_list.html'
  236. def extra_context(self):
  237. ipv4_total = 0
  238. ipv6_total = 0
  239. for aggregate in self.queryset:
  240. if aggregate.prefix.version == 6:
  241. # Report equivalent /64s for IPv6 to keep things sane
  242. ipv6_total += int(aggregate.prefix.size / 2 ** 64)
  243. else:
  244. ipv4_total += aggregate.prefix.size
  245. return {
  246. 'ipv4_total': ipv4_total,
  247. 'ipv6_total': ipv6_total,
  248. }
  249. class AggregateView(PermissionRequiredMixin, View):
  250. permission_required = 'ipam.view_aggregate'
  251. def get(self, request, pk):
  252. aggregate = get_object_or_404(Aggregate, pk=pk)
  253. # Find all child prefixes contained by this aggregate
  254. child_prefixes = Prefix.objects.filter(
  255. prefix__net_contained_or_equal=str(aggregate.prefix)
  256. ).prefetch_related(
  257. 'site', 'role'
  258. ).annotate_depth(
  259. limit=0
  260. )
  261. # Add available prefixes to the table if requested
  262. if request.GET.get('show_available', 'true') == 'true':
  263. child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
  264. prefix_table = tables.PrefixDetailTable(child_prefixes)
  265. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  266. prefix_table.columns.show('pk')
  267. paginate = {
  268. 'paginator_class': EnhancedPaginator,
  269. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  270. }
  271. RequestConfig(request, paginate).configure(prefix_table)
  272. # Compile permissions list for rendering the object table
  273. permissions = {
  274. 'add': request.user.has_perm('ipam.add_prefix'),
  275. 'change': request.user.has_perm('ipam.change_prefix'),
  276. 'delete': request.user.has_perm('ipam.delete_prefix'),
  277. }
  278. return render(request, 'ipam/aggregate.html', {
  279. 'aggregate': aggregate,
  280. 'prefix_table': prefix_table,
  281. 'permissions': permissions,
  282. 'show_available': request.GET.get('show_available', 'true') == 'true',
  283. })
  284. class AggregateCreateView(PermissionRequiredMixin, ObjectEditView):
  285. permission_required = 'ipam.add_aggregate'
  286. model = Aggregate
  287. model_form = forms.AggregateForm
  288. template_name = 'ipam/aggregate_edit.html'
  289. default_return_url = 'ipam:aggregate_list'
  290. class AggregateEditView(AggregateCreateView):
  291. permission_required = 'ipam.change_aggregate'
  292. class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  293. permission_required = 'ipam.delete_aggregate'
  294. model = Aggregate
  295. default_return_url = 'ipam:aggregate_list'
  296. class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
  297. permission_required = 'ipam.add_aggregate'
  298. model_form = forms.AggregateCSVForm
  299. table = tables.AggregateTable
  300. default_return_url = 'ipam:aggregate_list'
  301. class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
  302. permission_required = 'ipam.change_aggregate'
  303. queryset = Aggregate.objects.prefetch_related('rir')
  304. filter = filters.AggregateFilter
  305. table = tables.AggregateTable
  306. form = forms.AggregateBulkEditForm
  307. default_return_url = 'ipam:aggregate_list'
  308. class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  309. permission_required = 'ipam.delete_aggregate'
  310. queryset = Aggregate.objects.prefetch_related('rir')
  311. filter = filters.AggregateFilter
  312. table = tables.AggregateTable
  313. default_return_url = 'ipam:aggregate_list'
  314. #
  315. # Prefix/VLAN roles
  316. #
  317. class RoleListView(PermissionRequiredMixin, ObjectListView):
  318. permission_required = 'ipam.view_role'
  319. queryset = Role.objects.all()
  320. table = tables.RoleTable
  321. template_name = 'ipam/role_list.html'
  322. class RoleCreateView(PermissionRequiredMixin, ObjectEditView):
  323. permission_required = 'ipam.add_role'
  324. model = Role
  325. model_form = forms.RoleForm
  326. default_return_url = 'ipam:role_list'
  327. class RoleEditView(RoleCreateView):
  328. permission_required = 'ipam.change_role'
  329. class RoleBulkImportView(PermissionRequiredMixin, BulkImportView):
  330. permission_required = 'ipam.add_role'
  331. model_form = forms.RoleCSVForm
  332. table = tables.RoleTable
  333. default_return_url = 'ipam:role_list'
  334. class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  335. permission_required = 'ipam.delete_role'
  336. queryset = Role.objects.all()
  337. table = tables.RoleTable
  338. default_return_url = 'ipam:role_list'
  339. #
  340. # Prefixes
  341. #
  342. class PrefixListView(PermissionRequiredMixin, ObjectListView):
  343. permission_required = 'ipam.view_prefix'
  344. queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
  345. filter = filters.PrefixFilter
  346. filter_form = forms.PrefixFilterForm
  347. table = tables.PrefixDetailTable
  348. template_name = 'ipam/prefix_list.html'
  349. def alter_queryset(self, request):
  350. # Show only top-level prefixes by default (unless searching)
  351. limit = None if request.GET.get('expand') or request.GET.get('q') else 0
  352. return self.queryset.annotate_depth(limit=limit)
  353. class PrefixView(PermissionRequiredMixin, View):
  354. permission_required = 'ipam.view_prefix'
  355. def get(self, request, pk):
  356. prefix = get_object_or_404(Prefix.objects.prefetch_related(
  357. 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
  358. ), pk=pk)
  359. try:
  360. aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
  361. except Aggregate.DoesNotExist:
  362. aggregate = None
  363. # Parent prefixes table
  364. parent_prefixes = Prefix.objects.filter(
  365. Q(vrf=prefix.vrf) | Q(vrf__isnull=True)
  366. ).filter(
  367. prefix__net_contains=str(prefix.prefix)
  368. ).prefetch_related(
  369. 'site', 'role'
  370. ).annotate_depth()
  371. parent_prefix_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
  372. parent_prefix_table.exclude = ('vrf',)
  373. # Duplicate prefixes table
  374. duplicate_prefixes = Prefix.objects.filter(
  375. vrf=prefix.vrf, prefix=str(prefix.prefix)
  376. ).exclude(
  377. pk=prefix.pk
  378. ).prefetch_related(
  379. 'site', 'role'
  380. )
  381. duplicate_prefix_table = tables.PrefixTable(list(duplicate_prefixes), orderable=False)
  382. duplicate_prefix_table.exclude = ('vrf',)
  383. return render(request, 'ipam/prefix.html', {
  384. 'prefix': prefix,
  385. 'aggregate': aggregate,
  386. 'parent_prefix_table': parent_prefix_table,
  387. 'duplicate_prefix_table': duplicate_prefix_table,
  388. })
  389. class PrefixPrefixesView(PermissionRequiredMixin, View):
  390. permission_required = 'ipam.view_prefix'
  391. def get(self, request, pk):
  392. prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
  393. # Child prefixes table
  394. child_prefixes = prefix.get_child_prefixes().prefetch_related(
  395. 'site', 'vlan', 'role',
  396. ).annotate_depth(limit=0)
  397. # Add available prefixes to the table if requested
  398. if child_prefixes and request.GET.get('show_available', 'true') == 'true':
  399. child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
  400. prefix_table = tables.PrefixDetailTable(child_prefixes)
  401. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  402. prefix_table.columns.show('pk')
  403. paginate = {
  404. 'paginator_class': EnhancedPaginator,
  405. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  406. }
  407. RequestConfig(request, paginate).configure(prefix_table)
  408. # Compile permissions list for rendering the object table
  409. permissions = {
  410. 'add': request.user.has_perm('ipam.add_prefix'),
  411. 'change': request.user.has_perm('ipam.change_prefix'),
  412. 'delete': request.user.has_perm('ipam.delete_prefix'),
  413. }
  414. return render(request, 'ipam/prefix_prefixes.html', {
  415. 'prefix': prefix,
  416. 'first_available_prefix': prefix.get_first_available_prefix(),
  417. 'prefix_table': prefix_table,
  418. 'permissions': permissions,
  419. 'bulk_querystring': 'vrf_id={}&within={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
  420. 'active_tab': 'prefixes',
  421. 'show_available': request.GET.get('show_available', 'true') == 'true',
  422. })
  423. class PrefixIPAddressesView(PermissionRequiredMixin, View):
  424. permission_required = 'ipam.view_prefix'
  425. def get(self, request, pk):
  426. prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
  427. # Find all IPAddresses belonging to this Prefix
  428. ipaddresses = prefix.get_child_ips().prefetch_related(
  429. 'vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for'
  430. )
  431. # Add available IP addresses to the table if requested
  432. if request.GET.get('show_available', 'true') == 'true':
  433. ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
  434. ip_table = tables.IPAddressTable(ipaddresses)
  435. if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
  436. ip_table.columns.show('pk')
  437. paginate = {
  438. 'paginator_class': EnhancedPaginator,
  439. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  440. }
  441. RequestConfig(request, paginate).configure(ip_table)
  442. # Compile permissions list for rendering the object table
  443. permissions = {
  444. 'add': request.user.has_perm('ipam.add_ipaddress'),
  445. 'change': request.user.has_perm('ipam.change_ipaddress'),
  446. 'delete': request.user.has_perm('ipam.delete_ipaddress'),
  447. }
  448. return render(request, 'ipam/prefix_ipaddresses.html', {
  449. 'prefix': prefix,
  450. 'first_available_ip': prefix.get_first_available_ip(),
  451. 'ip_table': ip_table,
  452. 'permissions': permissions,
  453. 'bulk_querystring': 'vrf_id={}&parent={}'.format(prefix.vrf.pk if prefix.vrf else '0', prefix.prefix),
  454. 'active_tab': 'ip-addresses',
  455. 'show_available': request.GET.get('show_available', 'true') == 'true',
  456. })
  457. class PrefixCreateView(PermissionRequiredMixin, ObjectEditView):
  458. permission_required = 'ipam.add_prefix'
  459. model = Prefix
  460. model_form = forms.PrefixForm
  461. template_name = 'ipam/prefix_edit.html'
  462. default_return_url = 'ipam:prefix_list'
  463. class PrefixEditView(PrefixCreateView):
  464. permission_required = 'ipam.change_prefix'
  465. class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  466. permission_required = 'ipam.delete_prefix'
  467. model = Prefix
  468. template_name = 'ipam/prefix_delete.html'
  469. default_return_url = 'ipam:prefix_list'
  470. class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
  471. permission_required = 'ipam.add_prefix'
  472. model_form = forms.PrefixCSVForm
  473. table = tables.PrefixTable
  474. default_return_url = 'ipam:prefix_list'
  475. class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
  476. permission_required = 'ipam.change_prefix'
  477. queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
  478. filter = filters.PrefixFilter
  479. table = tables.PrefixTable
  480. form = forms.PrefixBulkEditForm
  481. default_return_url = 'ipam:prefix_list'
  482. class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  483. permission_required = 'ipam.delete_prefix'
  484. queryset = Prefix.objects.prefetch_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
  485. filter = filters.PrefixFilter
  486. table = tables.PrefixTable
  487. default_return_url = 'ipam:prefix_list'
  488. #
  489. # IP addresses
  490. #
  491. class IPAddressListView(PermissionRequiredMixin, ObjectListView):
  492. permission_required = 'ipam.view_ipaddress'
  493. queryset = IPAddress.objects.prefetch_related(
  494. 'vrf__tenant', 'tenant', 'nat_inside', 'interface__device', 'interface__virtual_machine'
  495. )
  496. filter = filters.IPAddressFilter
  497. filter_form = forms.IPAddressFilterForm
  498. table = tables.IPAddressDetailTable
  499. template_name = 'ipam/ipaddress_list.html'
  500. class IPAddressView(PermissionRequiredMixin, View):
  501. permission_required = 'ipam.view_ipaddress'
  502. def get(self, request, pk):
  503. ipaddress = get_object_or_404(IPAddress.objects.prefetch_related('vrf__tenant', 'tenant'), pk=pk)
  504. # Parent prefixes table
  505. parent_prefixes = Prefix.objects.filter(
  506. vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip)
  507. ).prefetch_related(
  508. 'site', 'role'
  509. )
  510. parent_prefixes_table = tables.PrefixTable(list(parent_prefixes), orderable=False)
  511. parent_prefixes_table.exclude = ('vrf',)
  512. # Duplicate IPs table
  513. duplicate_ips = IPAddress.objects.filter(
  514. vrf=ipaddress.vrf, address=str(ipaddress.address)
  515. ).exclude(
  516. pk=ipaddress.pk
  517. ).prefetch_related(
  518. 'nat_inside', 'interface__device'
  519. )
  520. # Exclude anycast IPs if this IP is anycast
  521. if ipaddress.role == IPADDRESS_ROLE_ANYCAST:
  522. duplicate_ips = duplicate_ips.exclude(role=IPADDRESS_ROLE_ANYCAST)
  523. duplicate_ips_table = tables.IPAddressTable(list(duplicate_ips), orderable=False)
  524. # Related IP table
  525. related_ips = IPAddress.objects.prefetch_related(
  526. 'interface__device'
  527. ).exclude(
  528. address=str(ipaddress.address)
  529. ).filter(
  530. vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address)
  531. )
  532. related_ips_table = tables.IPAddressTable(list(related_ips), orderable=False)
  533. return render(request, 'ipam/ipaddress.html', {
  534. 'ipaddress': ipaddress,
  535. 'parent_prefixes_table': parent_prefixes_table,
  536. 'duplicate_ips_table': duplicate_ips_table,
  537. 'related_ips_table': related_ips_table,
  538. })
  539. class IPAddressCreateView(PermissionRequiredMixin, ObjectEditView):
  540. permission_required = 'ipam.add_ipaddress'
  541. model = IPAddress
  542. model_form = forms.IPAddressForm
  543. template_name = 'ipam/ipaddress_edit.html'
  544. default_return_url = 'ipam:ipaddress_list'
  545. def alter_obj(self, obj, request, url_args, url_kwargs):
  546. interface_id = request.GET.get('interface')
  547. if interface_id:
  548. try:
  549. obj.interface = Interface.objects.get(pk=interface_id)
  550. except (ValueError, Interface.DoesNotExist):
  551. pass
  552. return obj
  553. class IPAddressEditView(IPAddressCreateView):
  554. permission_required = 'ipam.change_ipaddress'
  555. class IPAddressAssignView(PermissionRequiredMixin, View):
  556. """
  557. Search for IPAddresses to be assigned to an Interface.
  558. """
  559. permission_required = 'ipam.change_ipaddress'
  560. def dispatch(self, request, *args, **kwargs):
  561. # Redirect user if an interface has not been provided
  562. if 'interface' not in request.GET:
  563. return redirect('ipam:ipaddress_add')
  564. return super().dispatch(request, *args, **kwargs)
  565. def get(self, request):
  566. form = forms.IPAddressAssignForm()
  567. return render(request, 'ipam/ipaddress_assign.html', {
  568. 'form': form,
  569. 'return_url': request.GET.get('return_url', ''),
  570. })
  571. def post(self, request):
  572. form = forms.IPAddressAssignForm(request.POST)
  573. table = None
  574. if form.is_valid():
  575. queryset = IPAddress.objects.prefetch_related(
  576. 'vrf', 'tenant', 'interface__device', 'interface__virtual_machine'
  577. ).filter(
  578. vrf=form.cleaned_data['vrf'],
  579. address__istartswith=form.cleaned_data['address'],
  580. )[:100] # Limit to 100 results
  581. table = tables.IPAddressAssignTable(queryset)
  582. return render(request, 'ipam/ipaddress_assign.html', {
  583. 'form': form,
  584. 'table': table,
  585. 'return_url': request.GET.get('return_url', ''),
  586. })
  587. class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  588. permission_required = 'ipam.delete_ipaddress'
  589. model = IPAddress
  590. default_return_url = 'ipam:ipaddress_list'
  591. class IPAddressBulkCreateView(PermissionRequiredMixin, BulkCreateView):
  592. permission_required = 'ipam.add_ipaddress'
  593. form = forms.IPAddressBulkCreateForm
  594. model_form = forms.IPAddressBulkAddForm
  595. pattern_target = 'address'
  596. template_name = 'ipam/ipaddress_bulk_add.html'
  597. default_return_url = 'ipam:ipaddress_list'
  598. class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
  599. permission_required = 'ipam.add_ipaddress'
  600. model_form = forms.IPAddressCSVForm
  601. table = tables.IPAddressTable
  602. default_return_url = 'ipam:ipaddress_list'
  603. class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
  604. permission_required = 'ipam.change_ipaddress'
  605. queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
  606. filter = filters.IPAddressFilter
  607. table = tables.IPAddressTable
  608. form = forms.IPAddressBulkEditForm
  609. default_return_url = 'ipam:ipaddress_list'
  610. class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  611. permission_required = 'ipam.delete_ipaddress'
  612. queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant').prefetch_related('interface__device')
  613. filter = filters.IPAddressFilter
  614. table = tables.IPAddressTable
  615. default_return_url = 'ipam:ipaddress_list'
  616. #
  617. # VLAN groups
  618. #
  619. class VLANGroupListView(PermissionRequiredMixin, ObjectListView):
  620. permission_required = 'ipam.view_vlangroup'
  621. queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
  622. filter = filters.VLANGroupFilter
  623. filter_form = forms.VLANGroupFilterForm
  624. table = tables.VLANGroupTable
  625. template_name = 'ipam/vlangroup_list.html'
  626. class VLANGroupCreateView(PermissionRequiredMixin, ObjectEditView):
  627. permission_required = 'ipam.add_vlangroup'
  628. model = VLANGroup
  629. model_form = forms.VLANGroupForm
  630. default_return_url = 'ipam:vlangroup_list'
  631. class VLANGroupEditView(VLANGroupCreateView):
  632. permission_required = 'ipam.change_vlangroup'
  633. class VLANGroupBulkImportView(PermissionRequiredMixin, BulkImportView):
  634. permission_required = 'ipam.add_vlangroup'
  635. model_form = forms.VLANGroupCSVForm
  636. table = tables.VLANGroupTable
  637. default_return_url = 'ipam:vlangroup_list'
  638. class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  639. permission_required = 'ipam.delete_vlangroup'
  640. queryset = VLANGroup.objects.prefetch_related('site').annotate(vlan_count=Count('vlans'))
  641. filter = filters.VLANGroupFilter
  642. table = tables.VLANGroupTable
  643. default_return_url = 'ipam:vlangroup_list'
  644. class VLANGroupVLANsView(PermissionRequiredMixin, View):
  645. permission_required = 'ipam.view_vlangroup'
  646. def get(self, request, pk):
  647. vlan_group = get_object_or_404(VLANGroup.objects.all(), pk=pk)
  648. vlans = VLAN.objects.filter(group_id=pk)
  649. vlans = add_available_vlans(vlan_group, vlans)
  650. vlan_table = tables.VLANDetailTable(vlans)
  651. if request.user.has_perm('ipam.change_vlan') or request.user.has_perm('ipam.delete_vlan'):
  652. vlan_table.columns.show('pk')
  653. vlan_table.columns.hide('site')
  654. vlan_table.columns.hide('group')
  655. paginate = {
  656. 'paginator_class': EnhancedPaginator,
  657. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  658. }
  659. RequestConfig(request, paginate).configure(vlan_table)
  660. # Compile permissions list for rendering the object table
  661. permissions = {
  662. 'add': request.user.has_perm('ipam.add_vlan'),
  663. 'change': request.user.has_perm('ipam.change_vlan'),
  664. 'delete': request.user.has_perm('ipam.delete_vlan'),
  665. }
  666. return render(request, 'ipam/vlangroup_vlans.html', {
  667. 'vlan_group': vlan_group,
  668. 'first_available_vlan': vlan_group.get_next_available_vid(),
  669. 'vlan_table': vlan_table,
  670. 'permissions': permissions,
  671. })
  672. #
  673. # VLANs
  674. #
  675. class VLANListView(PermissionRequiredMixin, ObjectListView):
  676. permission_required = 'ipam.view_vlan'
  677. queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
  678. filter = filters.VLANFilter
  679. filter_form = forms.VLANFilterForm
  680. table = tables.VLANDetailTable
  681. template_name = 'ipam/vlan_list.html'
  682. class VLANView(PermissionRequiredMixin, View):
  683. permission_required = 'ipam.view_vlan'
  684. def get(self, request, pk):
  685. vlan = get_object_or_404(VLAN.objects.prefetch_related(
  686. 'site__region', 'tenant__group', 'role'
  687. ), pk=pk)
  688. prefixes = Prefix.objects.filter(vlan=vlan).prefetch_related('vrf', 'site', 'role')
  689. prefix_table = tables.PrefixTable(list(prefixes), orderable=False)
  690. prefix_table.exclude = ('vlan',)
  691. return render(request, 'ipam/vlan.html', {
  692. 'vlan': vlan,
  693. 'prefix_table': prefix_table,
  694. })
  695. class VLANMembersView(PermissionRequiredMixin, View):
  696. permission_required = 'ipam.view_vlan'
  697. def get(self, request, pk):
  698. vlan = get_object_or_404(VLAN.objects.all(), pk=pk)
  699. members = vlan.get_members().prefetch_related('device', 'virtual_machine')
  700. members_table = tables.VLANMemberTable(members)
  701. paginate = {
  702. 'paginator_class': EnhancedPaginator,
  703. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  704. }
  705. RequestConfig(request, paginate).configure(members_table)
  706. return render(request, 'ipam/vlan_members.html', {
  707. 'vlan': vlan,
  708. 'members_table': members_table,
  709. 'active_tab': 'members',
  710. })
  711. class VLANCreateView(PermissionRequiredMixin, ObjectEditView):
  712. permission_required = 'ipam.add_vlan'
  713. model = VLAN
  714. model_form = forms.VLANForm
  715. template_name = 'ipam/vlan_edit.html'
  716. default_return_url = 'ipam:vlan_list'
  717. class VLANEditView(VLANCreateView):
  718. permission_required = 'ipam.change_vlan'
  719. class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  720. permission_required = 'ipam.delete_vlan'
  721. model = VLAN
  722. default_return_url = 'ipam:vlan_list'
  723. class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
  724. permission_required = 'ipam.add_vlan'
  725. model_form = forms.VLANCSVForm
  726. table = tables.VLANTable
  727. default_return_url = 'ipam:vlan_list'
  728. class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
  729. permission_required = 'ipam.change_vlan'
  730. queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
  731. filter = filters.VLANFilter
  732. table = tables.VLANTable
  733. form = forms.VLANBulkEditForm
  734. default_return_url = 'ipam:vlan_list'
  735. class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  736. permission_required = 'ipam.delete_vlan'
  737. queryset = VLAN.objects.prefetch_related('site', 'group', 'tenant', 'role')
  738. filter = filters.VLANFilter
  739. table = tables.VLANTable
  740. default_return_url = 'ipam:vlan_list'
  741. #
  742. # Services
  743. #
  744. class ServiceListView(PermissionRequiredMixin, ObjectListView):
  745. permission_required = 'ipam.view_service'
  746. queryset = Service.objects.prefetch_related('device', 'virtual_machine')
  747. filter = filters.ServiceFilter
  748. filter_form = forms.ServiceFilterForm
  749. table = tables.ServiceTable
  750. template_name = 'ipam/service_list.html'
  751. class ServiceView(PermissionRequiredMixin, View):
  752. permission_required = 'ipam.view_service'
  753. def get(self, request, pk):
  754. service = get_object_or_404(Service, pk=pk)
  755. return render(request, 'ipam/service.html', {
  756. 'service': service,
  757. })
  758. class ServiceCreateView(PermissionRequiredMixin, ObjectEditView):
  759. permission_required = 'ipam.add_service'
  760. model = Service
  761. model_form = forms.ServiceForm
  762. template_name = 'ipam/service_edit.html'
  763. def alter_obj(self, obj, request, url_args, url_kwargs):
  764. if 'device' in url_kwargs:
  765. obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
  766. elif 'virtualmachine' in url_kwargs:
  767. obj.virtual_machine = get_object_or_404(VirtualMachine, pk=url_kwargs['virtualmachine'])
  768. return obj
  769. def get_return_url(self, request, service):
  770. return service.parent.get_absolute_url()
  771. class ServiceEditView(ServiceCreateView):
  772. permission_required = 'ipam.change_service'
  773. class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  774. permission_required = 'ipam.delete_service'
  775. model = Service
  776. class ServiceBulkEditView(PermissionRequiredMixin, BulkEditView):
  777. permission_required = 'ipam.change_service'
  778. queryset = Service.objects.prefetch_related('device', 'virtual_machine')
  779. filter = filters.ServiceFilter
  780. table = tables.ServiceTable
  781. form = forms.ServiceBulkEditForm
  782. default_return_url = 'ipam:service_list'
  783. class ServiceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  784. permission_required = 'ipam.delete_service'
  785. queryset = Service.objects.prefetch_related('device', 'virtual_machine')
  786. filter = filters.ServiceFilter
  787. table = tables.ServiceTable
  788. default_return_url = 'ipam:service_list'