views.py 36 KB

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