views.py 35 KB

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