views.py 27 KB

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