views.py 35 KB

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