views.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745
  1. from django_tables2 import RequestConfig
  2. import netaddr
  3. from django.conf import settings
  4. from django.contrib.auth.decorators import permission_required
  5. from django.contrib.auth.mixins import PermissionRequiredMixin
  6. from django.contrib import messages
  7. from django.db.models import Count, Q
  8. from django.shortcuts import get_object_or_404, redirect, render
  9. from django.urls import reverse
  10. from dcim.models import Device
  11. from utilities.forms import ConfirmationForm
  12. from utilities.paginator import EnhancedPaginator
  13. from utilities.views import (
  14. BulkAddView, BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
  15. )
  16. from . import filters, forms, tables
  17. from .models import (
  18. Aggregate, IPAddress, PREFIX_STATUS_ACTIVE, PREFIX_STATUS_DEPRECATED, PREFIX_STATUS_RESERVED, Prefix, RIR, Role,
  19. Service, VLAN, VLANGroup, VRF,
  20. )
  21. def add_available_prefixes(parent, prefix_list):
  22. """
  23. Create fake Prefix objects for all unallocated space within a prefix.
  24. """
  25. # Find all unallocated space
  26. available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list])
  27. available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
  28. # Concatenate and sort complete list of children
  29. prefix_list = list(prefix_list) + available_prefixes
  30. prefix_list.sort(key=lambda p: p.prefix)
  31. return prefix_list
  32. def add_available_ipaddresses(prefix, ipaddress_list, is_pool=False):
  33. """
  34. Annotate ranges of available IP addresses within a given prefix. If is_pool is True, the first and last IP will be
  35. considered usable (regardless of mask length).
  36. """
  37. output = []
  38. prev_ip = None
  39. # Ignore the network and broadcast addresses for non-pool IPv4 prefixes larger than /31.
  40. if prefix.version == 4 and prefix.prefixlen < 31 and not is_pool:
  41. first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1)
  42. last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1)
  43. else:
  44. first_ip_in_prefix = netaddr.IPAddress(prefix.first)
  45. last_ip_in_prefix = netaddr.IPAddress(prefix.last)
  46. if not ipaddress_list:
  47. return [(
  48. int(last_ip_in_prefix - first_ip_in_prefix + 1),
  49. '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
  50. )]
  51. # Account for any available IPs before the first real IP
  52. if ipaddress_list[0].address.ip > first_ip_in_prefix:
  53. skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix)
  54. first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen)
  55. output.append((skipped_count, first_skipped))
  56. # Iterate through existing IPs and annotate free ranges
  57. for ip in ipaddress_list:
  58. if prev_ip:
  59. diff = int(ip.address.ip - prev_ip.address.ip)
  60. if diff > 1:
  61. first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
  62. output.append((diff - 1, first_skipped))
  63. output.append(ip)
  64. prev_ip = ip
  65. # Include any remaining available IPs
  66. if prev_ip.address.ip < last_ip_in_prefix:
  67. skipped_count = int(last_ip_in_prefix - prev_ip.address.ip)
  68. first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen)
  69. output.append((skipped_count, first_skipped))
  70. return output
  71. #
  72. # VRFs
  73. #
  74. class VRFListView(ObjectListView):
  75. queryset = VRF.objects.select_related('tenant')
  76. filter = filters.VRFFilter
  77. filter_form = forms.VRFFilterForm
  78. table = tables.VRFTable
  79. template_name = 'ipam/vrf_list.html'
  80. def vrf(request, pk):
  81. vrf = get_object_or_404(VRF.objects.all(), pk=pk)
  82. prefix_table = tables.PrefixBriefTable(
  83. list(Prefix.objects.filter(vrf=vrf).select_related('site', 'role'))
  84. )
  85. prefix_table.exclude = ('vrf',)
  86. return render(request, 'ipam/vrf.html', {
  87. 'vrf': vrf,
  88. 'prefix_table': prefix_table,
  89. })
  90. class VRFEditView(PermissionRequiredMixin, ObjectEditView):
  91. permission_required = 'ipam.change_vrf'
  92. model = VRF
  93. form_class = forms.VRFForm
  94. template_name = 'ipam/vrf_edit.html'
  95. default_return_url = 'ipam:vrf_list'
  96. class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  97. permission_required = 'ipam.delete_vrf'
  98. model = VRF
  99. default_return_url = 'ipam:vrf_list'
  100. class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
  101. permission_required = 'ipam.add_vrf'
  102. form = forms.VRFImportForm
  103. table = tables.VRFTable
  104. template_name = 'ipam/vrf_import.html'
  105. default_return_url = 'ipam:vrf_list'
  106. class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
  107. permission_required = 'ipam.change_vrf'
  108. cls = VRF
  109. filter = filters.VRFFilter
  110. form = forms.VRFBulkEditForm
  111. template_name = 'ipam/vrf_bulk_edit.html'
  112. default_return_url = 'ipam:vrf_list'
  113. class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  114. permission_required = 'ipam.delete_vrf'
  115. cls = VRF
  116. filter = filters.VRFFilter
  117. default_return_url = 'ipam:vrf_list'
  118. #
  119. # RIRs
  120. #
  121. class RIRListView(ObjectListView):
  122. queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
  123. filter = filters.RIRFilter
  124. filter_form = forms.RIRFilterForm
  125. table = tables.RIRTable
  126. template_name = 'ipam/rir_list.html'
  127. def alter_queryset(self, request):
  128. if request.GET.get('family') == '6':
  129. family = 6
  130. denominator = 2 ** 64 # Count /64s for IPv6 rather than individual IPs
  131. else:
  132. family = 4
  133. denominator = 1
  134. rirs = []
  135. for rir in self.queryset:
  136. stats = {
  137. 'total': 0,
  138. 'active': 0,
  139. 'reserved': 0,
  140. 'deprecated': 0,
  141. 'available': 0,
  142. }
  143. aggregate_list = Aggregate.objects.filter(family=family, rir=rir)
  144. for aggregate in aggregate_list:
  145. queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
  146. # Find all consumed space for each prefix status (we ignore containers for this purpose).
  147. active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
  148. reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
  149. deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
  150. # Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
  151. available_prefixes = (
  152. netaddr.IPSet([aggregate.prefix]) -
  153. netaddr.IPSet(active_prefixes) -
  154. netaddr.IPSet(reserved_prefixes) -
  155. netaddr.IPSet(deprecated_prefixes)
  156. )
  157. # Add the size of each metric to the RIR total.
  158. stats['total'] += aggregate.prefix.size / denominator
  159. stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
  160. stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
  161. stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
  162. stats['available'] += available_prefixes.size / denominator
  163. # Calculate the percentage of total space for each prefix status.
  164. total = float(stats['total'])
  165. stats['percentages'] = {
  166. 'active': float('{:.2f}'.format(stats['active'] / total * 100)) if total else 0,
  167. 'reserved': float('{:.2f}'.format(stats['reserved'] / total * 100)) if total else 0,
  168. 'deprecated': float('{:.2f}'.format(stats['deprecated'] / total * 100)) if total else 0,
  169. }
  170. stats['percentages']['available'] = (
  171. 100 -
  172. stats['percentages']['active'] -
  173. stats['percentages']['reserved'] -
  174. stats['percentages']['deprecated']
  175. )
  176. rir.stats = stats
  177. rirs.append(rir)
  178. return rirs
  179. def extra_context(self):
  180. totals = {
  181. 'total': sum([rir.stats['total'] for rir in self.queryset]),
  182. 'active': sum([rir.stats['active'] for rir in self.queryset]),
  183. 'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
  184. 'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
  185. 'available': sum([rir.stats['available'] for rir in self.queryset]),
  186. }
  187. return {
  188. 'totals': totals,
  189. }
  190. class RIREditView(PermissionRequiredMixin, ObjectEditView):
  191. permission_required = 'ipam.change_rir'
  192. model = RIR
  193. form_class = forms.RIRForm
  194. def get_return_url(self, request, obj):
  195. return reverse('ipam:rir_list')
  196. class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  197. permission_required = 'ipam.delete_rir'
  198. cls = RIR
  199. filter = filters.RIRFilter
  200. default_return_url = 'ipam:rir_list'
  201. #
  202. # Aggregates
  203. #
  204. class AggregateListView(ObjectListView):
  205. queryset = Aggregate.objects.select_related('rir').extra(select={
  206. 'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
  207. })
  208. filter = filters.AggregateFilter
  209. filter_form = forms.AggregateFilterForm
  210. table = tables.AggregateTable
  211. template_name = 'ipam/aggregate_list.html'
  212. def extra_context(self):
  213. ipv4_total = 0
  214. ipv6_total = 0
  215. for a in self.queryset:
  216. if a.prefix.version == 4:
  217. ipv4_total += a.prefix.size
  218. elif a.prefix.version == 6:
  219. ipv6_total += a.prefix.size / 2 ** 64
  220. return {
  221. 'ipv4_total': ipv4_total,
  222. 'ipv6_total': ipv6_total,
  223. }
  224. def aggregate(request, pk):
  225. aggregate = get_object_or_404(Aggregate, pk=pk)
  226. # Find all child prefixes contained by this aggregate
  227. child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
  228. .select_related('site', 'role').annotate_depth(limit=0)
  229. child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
  230. prefix_table = tables.PrefixTable(child_prefixes)
  231. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  232. prefix_table.base_columns['pk'].visible = True
  233. paginate = {
  234. 'klass': EnhancedPaginator,
  235. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  236. }
  237. RequestConfig(request, paginate).configure(prefix_table)
  238. # Compile permissions list for rendering the object table
  239. permissions = {
  240. 'add': request.user.has_perm('ipam.add_prefix'),
  241. 'change': request.user.has_perm('ipam.change_prefix'),
  242. 'delete': request.user.has_perm('ipam.delete_prefix'),
  243. }
  244. return render(request, 'ipam/aggregate.html', {
  245. 'aggregate': aggregate,
  246. 'prefix_table': prefix_table,
  247. 'permissions': permissions,
  248. })
  249. class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
  250. permission_required = 'ipam.change_aggregate'
  251. model = Aggregate
  252. form_class = forms.AggregateForm
  253. template_name = 'ipam/aggregate_edit.html'
  254. default_return_url = 'ipam:aggregate_list'
  255. class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  256. permission_required = 'ipam.delete_aggregate'
  257. model = Aggregate
  258. default_return_url = 'ipam:aggregate_list'
  259. class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
  260. permission_required = 'ipam.add_aggregate'
  261. form = forms.AggregateImportForm
  262. table = tables.AggregateTable
  263. template_name = 'ipam/aggregate_import.html'
  264. default_return_url = 'ipam:aggregate_list'
  265. class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
  266. permission_required = 'ipam.change_aggregate'
  267. cls = Aggregate
  268. filter = filters.AggregateFilter
  269. form = forms.AggregateBulkEditForm
  270. template_name = 'ipam/aggregate_bulk_edit.html'
  271. default_return_url = 'ipam:aggregate_list'
  272. class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  273. permission_required = 'ipam.delete_aggregate'
  274. cls = Aggregate
  275. filter = filters.AggregateFilter
  276. default_return_url = 'ipam:aggregate_list'
  277. #
  278. # Prefix/VLAN roles
  279. #
  280. class RoleListView(ObjectListView):
  281. queryset = Role.objects.all()
  282. table = tables.RoleTable
  283. template_name = 'ipam/role_list.html'
  284. class RoleEditView(PermissionRequiredMixin, ObjectEditView):
  285. permission_required = 'ipam.change_role'
  286. model = Role
  287. form_class = forms.RoleForm
  288. def get_return_url(self, request, obj):
  289. return reverse('ipam:role_list')
  290. class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  291. permission_required = 'ipam.delete_role'
  292. cls = Role
  293. default_return_url = 'ipam:role_list'
  294. #
  295. # Prefixes
  296. #
  297. class PrefixListView(ObjectListView):
  298. queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')
  299. filter = filters.PrefixFilter
  300. filter_form = forms.PrefixFilterForm
  301. table = tables.PrefixTable
  302. template_name = 'ipam/prefix_list.html'
  303. def alter_queryset(self, request):
  304. # Show only top-level prefixes by default (unless searching)
  305. limit = None if request.GET.get('expand') or request.GET.get('q') else 0
  306. return self.queryset.annotate_depth(limit=limit)
  307. def prefix(request, pk):
  308. prefix = get_object_or_404(Prefix.objects.select_related(
  309. 'vrf', 'site__region', 'tenant__group', 'vlan__group', 'role'
  310. ), pk=pk)
  311. try:
  312. aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
  313. except Aggregate.DoesNotExist:
  314. aggregate = None
  315. # Count child IP addresses
  316. ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
  317. .count()
  318. # Parent prefixes table
  319. parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
  320. .filter(prefix__net_contains=str(prefix.prefix))\
  321. .select_related('site', 'role').annotate_depth()
  322. parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
  323. parent_prefix_table.exclude = ('vrf',)
  324. # Duplicate prefixes table
  325. duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
  326. .select_related('site', 'role')
  327. duplicate_prefix_table = tables.PrefixBriefTable(list(duplicate_prefixes))
  328. duplicate_prefix_table.exclude = ('vrf',)
  329. # Child prefixes table
  330. child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\
  331. .select_related('site', 'role').annotate_depth(limit=0)
  332. if child_prefixes:
  333. child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
  334. child_prefix_table = tables.PrefixTable(child_prefixes)
  335. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  336. child_prefix_table.base_columns['pk'].visible = True
  337. paginate = {
  338. 'klass': EnhancedPaginator,
  339. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  340. }
  341. RequestConfig(request, paginate).configure(child_prefix_table)
  342. # Compile permissions list for rendering the object table
  343. permissions = {
  344. 'add': request.user.has_perm('ipam.add_prefix'),
  345. 'change': request.user.has_perm('ipam.change_prefix'),
  346. 'delete': request.user.has_perm('ipam.delete_prefix'),
  347. }
  348. return render(request, 'ipam/prefix.html', {
  349. 'prefix': prefix,
  350. 'aggregate': aggregate,
  351. 'ipaddress_count': ipaddress_count,
  352. 'parent_prefix_table': parent_prefix_table,
  353. 'child_prefix_table': child_prefix_table,
  354. 'duplicate_prefix_table': duplicate_prefix_table,
  355. 'permissions': permissions,
  356. 'return_url': prefix.get_absolute_url(),
  357. })
  358. class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
  359. permission_required = 'ipam.change_prefix'
  360. model = Prefix
  361. form_class = forms.PrefixForm
  362. template_name = 'ipam/prefix_edit.html'
  363. default_return_url = 'ipam:prefix_list'
  364. class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  365. permission_required = 'ipam.delete_prefix'
  366. model = Prefix
  367. template_name = 'ipam/prefix_delete.html'
  368. default_return_url = 'ipam:prefix_list'
  369. class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
  370. permission_required = 'ipam.add_prefix'
  371. form = forms.PrefixImportForm
  372. table = tables.PrefixTable
  373. template_name = 'ipam/prefix_import.html'
  374. default_return_url = 'ipam:prefix_list'
  375. class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
  376. permission_required = 'ipam.change_prefix'
  377. cls = Prefix
  378. filter = filters.PrefixFilter
  379. form = forms.PrefixBulkEditForm
  380. template_name = 'ipam/prefix_bulk_edit.html'
  381. default_return_url = 'ipam:prefix_list'
  382. class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  383. permission_required = 'ipam.delete_prefix'
  384. cls = Prefix
  385. filter = filters.PrefixFilter
  386. default_return_url = 'ipam:prefix_list'
  387. def prefix_ipaddresses(request, pk):
  388. prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
  389. # Find all IPAddresses belonging to this Prefix
  390. ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_host_contained=str(prefix.prefix))\
  391. .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
  392. ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses, prefix.is_pool)
  393. ip_table = tables.IPAddressTable(ipaddresses)
  394. if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
  395. ip_table.base_columns['pk'].visible = True
  396. paginate = {
  397. 'klass': EnhancedPaginator,
  398. 'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
  399. }
  400. RequestConfig(request, paginate).configure(ip_table)
  401. # Compile permissions list for rendering the object table
  402. permissions = {
  403. 'add': request.user.has_perm('ipam.add_ipaddress'),
  404. 'change': request.user.has_perm('ipam.change_ipaddress'),
  405. 'delete': request.user.has_perm('ipam.delete_ipaddress'),
  406. }
  407. return render(request, 'ipam/prefix_ipaddresses.html', {
  408. 'prefix': prefix,
  409. 'ip_table': ip_table,
  410. 'permissions': permissions,
  411. })
  412. #
  413. # IP addresses
  414. #
  415. class IPAddressListView(ObjectListView):
  416. queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device')
  417. filter = filters.IPAddressFilter
  418. filter_form = forms.IPAddressFilterForm
  419. table = tables.IPAddressTable
  420. template_name = 'ipam/ipaddress_list.html'
  421. def ipaddress(request, pk):
  422. ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
  423. # Parent prefixes table
  424. parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))\
  425. .select_related('site', 'role')
  426. parent_prefixes_table = tables.PrefixBriefTable(list(parent_prefixes))
  427. parent_prefixes_table.exclude = ('vrf',)
  428. # Duplicate IPs table
  429. duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
  430. .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
  431. duplicate_ips_table = tables.IPAddressBriefTable(list(duplicate_ips))
  432. # Related IP table
  433. related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
  434. .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
  435. related_ips_table = tables.IPAddressBriefTable(list(related_ips))
  436. return render(request, 'ipam/ipaddress.html', {
  437. 'ipaddress': ipaddress,
  438. 'parent_prefixes_table': parent_prefixes_table,
  439. 'duplicate_ips_table': duplicate_ips_table,
  440. 'related_ips_table': related_ips_table,
  441. })
  442. class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
  443. permission_required = 'ipam.change_ipaddress'
  444. model = IPAddress
  445. form_class = forms.IPAddressForm
  446. template_name = 'ipam/ipaddress_edit.html'
  447. default_return_url = 'ipam:ipaddress_list'
  448. class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  449. permission_required = 'ipam.delete_ipaddress'
  450. model = IPAddress
  451. default_return_url = 'ipam:ipaddress_list'
  452. class IPAddressBulkAddView(PermissionRequiredMixin, BulkAddView):
  453. permission_required = 'ipam.add_ipaddress'
  454. form = forms.IPAddressBulkAddForm
  455. model = IPAddress
  456. template_name = 'ipam/ipaddress_bulk_add.html'
  457. default_return_url = 'ipam:ipaddress_list'
  458. class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
  459. permission_required = 'ipam.add_ipaddress'
  460. form = forms.IPAddressImportForm
  461. table = tables.IPAddressTable
  462. template_name = 'ipam/ipaddress_import.html'
  463. default_return_url = 'ipam:ipaddress_list'
  464. def save_obj(self, obj):
  465. obj.save()
  466. # Update primary IP for device if needed. The Device must be updated directly in the database; otherwise we risk
  467. # overwriting a previous IP assignment from the same import (see #861).
  468. try:
  469. if obj.family == 4 and obj.primary_ip4_for:
  470. Device.objects.filter(pk=obj.primary_ip4_for.pk).update(primary_ip4=obj)
  471. elif obj.family == 6 and obj.primary_ip6_for:
  472. Device.objects.filter(pk=obj.primary_ip6_for.pk).update(primary_ip6=obj)
  473. except Device.DoesNotExist:
  474. pass
  475. class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
  476. permission_required = 'ipam.change_ipaddress'
  477. cls = IPAddress
  478. filter = filters.IPAddressFilter
  479. form = forms.IPAddressBulkEditForm
  480. template_name = 'ipam/ipaddress_bulk_edit.html'
  481. default_return_url = 'ipam:ipaddress_list'
  482. class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  483. permission_required = 'ipam.delete_ipaddress'
  484. cls = IPAddress
  485. filter = filters.IPAddressFilter
  486. default_return_url = 'ipam:ipaddress_list'
  487. #
  488. # VLAN groups
  489. #
  490. class VLANGroupListView(ObjectListView):
  491. queryset = VLANGroup.objects.select_related('site').annotate(vlan_count=Count('vlans'))
  492. filter = filters.VLANGroupFilter
  493. filter_form = forms.VLANGroupFilterForm
  494. table = tables.VLANGroupTable
  495. template_name = 'ipam/vlangroup_list.html'
  496. class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
  497. permission_required = 'ipam.change_vlangroup'
  498. model = VLANGroup
  499. form_class = forms.VLANGroupForm
  500. def get_return_url(self, request, obj):
  501. return reverse('ipam:vlangroup_list')
  502. class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  503. permission_required = 'ipam.delete_vlangroup'
  504. cls = VLANGroup
  505. filter = filters.VLANGroupFilter
  506. default_return_url = 'ipam:vlangroup_list'
  507. #
  508. # VLANs
  509. #
  510. class VLANListView(ObjectListView):
  511. queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('prefixes')
  512. filter = filters.VLANFilter
  513. filter_form = forms.VLANFilterForm
  514. table = tables.VLANTable
  515. template_name = 'ipam/vlan_list.html'
  516. def vlan(request, pk):
  517. vlan = get_object_or_404(VLAN.objects.select_related('site__region', 'tenant__group', 'role'), pk=pk)
  518. prefixes = Prefix.objects.filter(vlan=vlan).select_related('vrf', 'site', 'role')
  519. prefix_table = tables.PrefixBriefTable(list(prefixes))
  520. prefix_table.exclude = ('vlan',)
  521. return render(request, 'ipam/vlan.html', {
  522. 'vlan': vlan,
  523. 'prefix_table': prefix_table,
  524. })
  525. class VLANEditView(PermissionRequiredMixin, ObjectEditView):
  526. permission_required = 'ipam.change_vlan'
  527. model = VLAN
  528. form_class = forms.VLANForm
  529. template_name = 'ipam/vlan_edit.html'
  530. default_return_url = 'ipam:vlan_list'
  531. class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  532. permission_required = 'ipam.delete_vlan'
  533. model = VLAN
  534. default_return_url = 'ipam:vlan_list'
  535. class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
  536. permission_required = 'ipam.add_vlan'
  537. form = forms.VLANImportForm
  538. table = tables.VLANTable
  539. template_name = 'ipam/vlan_import.html'
  540. default_return_url = 'ipam:vlan_list'
  541. class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
  542. permission_required = 'ipam.change_vlan'
  543. cls = VLAN
  544. filter = filters.VLANFilter
  545. form = forms.VLANBulkEditForm
  546. template_name = 'ipam/vlan_bulk_edit.html'
  547. default_return_url = 'ipam:vlan_list'
  548. class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  549. permission_required = 'ipam.delete_vlan'
  550. cls = VLAN
  551. filter = filters.VLANFilter
  552. default_return_url = 'ipam:vlan_list'
  553. #
  554. # Services
  555. #
  556. class ServiceEditView(PermissionRequiredMixin, ObjectEditView):
  557. permission_required = 'ipam.change_service'
  558. model = Service
  559. form_class = forms.ServiceForm
  560. template_name = 'ipam/service_edit.html'
  561. def alter_obj(self, obj, request, url_args, url_kwargs):
  562. if 'device' in url_kwargs:
  563. obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
  564. return obj
  565. def get_return_url(self, request, obj):
  566. return obj.device.get_absolute_url()
  567. class ServiceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  568. permission_required = 'ipam.delete_service'
  569. model = Service