views.py 34 KB

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