views.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. from netaddr import IPSet
  2. from django_tables2 import RequestConfig
  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, render
  6. from dcim.models import Device
  7. from utilities.paginator import EnhancedPaginator
  8. from utilities.views import (
  9. BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
  10. )
  11. from . import filters, forms, tables
  12. from .models import Aggregate, IPAddress, Prefix, RIR, Role, VLAN, VLANGroup, VRF
  13. def add_available_prefixes(parent, prefix_list):
  14. """
  15. Create fake Prefix objects for all unallocated space within a prefix.
  16. """
  17. # Find all unallocated space
  18. available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list])
  19. available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
  20. # Concatenate and sort complete list of children
  21. prefix_list = list(prefix_list) + available_prefixes
  22. prefix_list.sort(key=lambda p: p.prefix)
  23. return prefix_list
  24. #
  25. # VRFs
  26. #
  27. class VRFListView(ObjectListView):
  28. queryset = VRF.objects.select_related('tenant')
  29. filter = filters.VRFFilter
  30. filter_form = forms.VRFFilterForm
  31. table = tables.VRFTable
  32. edit_permissions = ['ipam.change_vrf', 'ipam.delete_vrf']
  33. template_name = 'ipam/vrf_list.html'
  34. def vrf(request, pk):
  35. vrf = get_object_or_404(VRF.objects.all(), pk=pk)
  36. prefixes = Prefix.objects.filter(vrf=vrf)
  37. prefix_table = tables.PrefixBriefTable(prefixes)
  38. return render(request, 'ipam/vrf.html', {
  39. 'vrf': vrf,
  40. 'prefix_table': prefix_table,
  41. })
  42. class VRFEditView(PermissionRequiredMixin, ObjectEditView):
  43. permission_required = 'ipam.change_vrf'
  44. model = VRF
  45. form_class = forms.VRFForm
  46. cancel_url = 'ipam:vrf_list'
  47. class VRFDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  48. permission_required = 'ipam.delete_vrf'
  49. model = VRF
  50. redirect_url = 'ipam:vrf_list'
  51. class VRFBulkImportView(PermissionRequiredMixin, BulkImportView):
  52. permission_required = 'ipam.add_vrf'
  53. form = forms.VRFImportForm
  54. table = tables.VRFTable
  55. template_name = 'ipam/vrf_import.html'
  56. obj_list_url = 'ipam:vrf_list'
  57. class VRFBulkEditView(PermissionRequiredMixin, BulkEditView):
  58. permission_required = 'ipam.change_vrf'
  59. cls = VRF
  60. form = forms.VRFBulkEditForm
  61. template_name = 'ipam/vrf_bulk_edit.html'
  62. default_redirect_url = 'ipam:vrf_list'
  63. def update_objects(self, pk_list, form):
  64. fields_to_update = {}
  65. if form.cleaned_data['tenant'] == 0:
  66. fields_to_update['tenant'] = None
  67. elif form.cleaned_data['tenant']:
  68. fields_to_update['tenant'] = form.cleaned_data['tenant']
  69. for field in ['description']:
  70. if form.cleaned_data[field]:
  71. fields_to_update[field] = form.cleaned_data[field]
  72. return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  73. class VRFBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  74. permission_required = 'ipam.delete_vrf'
  75. cls = VRF
  76. default_redirect_url = 'ipam:vrf_list'
  77. #
  78. # RIRs
  79. #
  80. class RIRListView(ObjectListView):
  81. queryset = RIR.objects.annotate(aggregate_count=Count('aggregates'))
  82. table = tables.RIRTable
  83. edit_permissions = ['ipam.change_rir', 'ipam.delete_rir']
  84. template_name = 'ipam/rir_list.html'
  85. class RIREditView(PermissionRequiredMixin, ObjectEditView):
  86. permission_required = 'ipam.change_rir'
  87. model = RIR
  88. form_class = forms.RIRForm
  89. success_url = 'ipam:rir_list'
  90. cancel_url = 'ipam:rir_list'
  91. class RIRBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  92. permission_required = 'ipam.delete_rir'
  93. cls = RIR
  94. default_redirect_url = 'ipam:rir_list'
  95. #
  96. # Aggregates
  97. #
  98. class AggregateListView(ObjectListView):
  99. queryset = Aggregate.objects.select_related('rir').extra(select={
  100. 'child_count': 'SELECT COUNT(*) FROM ipam_prefix WHERE ipam_prefix.prefix <<= ipam_aggregate.prefix',
  101. })
  102. filter = filters.AggregateFilter
  103. filter_form = forms.AggregateFilterForm
  104. table = tables.AggregateTable
  105. edit_permissions = ['ipam.change_aggregate', 'ipam.delete_aggregate']
  106. template_name = 'ipam/aggregate_list.html'
  107. def extra_context(self):
  108. ipv4_total = 0
  109. ipv6_total = 0
  110. for a in self.queryset:
  111. if a.prefix.version == 4:
  112. ipv4_total += a.prefix.size
  113. elif a.prefix.version == 6:
  114. ipv6_total += a.prefix.size / 2 ** 64
  115. return {
  116. 'ipv4_total': ipv4_total,
  117. 'ipv6_total': ipv6_total,
  118. }
  119. def aggregate(request, pk):
  120. aggregate = get_object_or_404(Aggregate, pk=pk)
  121. # Find all child prefixes contained by this aggregate
  122. child_prefixes = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))\
  123. .select_related('site', 'role').annotate_depth(limit=0)
  124. child_prefixes = add_available_prefixes(aggregate.prefix, child_prefixes)
  125. prefix_table = tables.PrefixTable(child_prefixes)
  126. prefix_table.model = Prefix
  127. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  128. prefix_table.base_columns['pk'].visible = True
  129. RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(prefix_table)
  130. return render(request, 'ipam/aggregate.html', {
  131. 'aggregate': aggregate,
  132. 'prefix_table': prefix_table,
  133. })
  134. class AggregateEditView(PermissionRequiredMixin, ObjectEditView):
  135. permission_required = 'ipam.change_aggregate'
  136. model = Aggregate
  137. form_class = forms.AggregateForm
  138. cancel_url = 'ipam:aggregate_list'
  139. class AggregateDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  140. permission_required = 'ipam.delete_aggregate'
  141. model = Aggregate
  142. redirect_url = 'ipam:aggregate_list'
  143. class AggregateBulkImportView(PermissionRequiredMixin, BulkImportView):
  144. permission_required = 'ipam.add_aggregate'
  145. form = forms.AggregateImportForm
  146. table = tables.AggregateTable
  147. template_name = 'ipam/aggregate_import.html'
  148. obj_list_url = 'ipam:aggregate_list'
  149. class AggregateBulkEditView(PermissionRequiredMixin, BulkEditView):
  150. permission_required = 'ipam.change_aggregate'
  151. cls = Aggregate
  152. form = forms.AggregateBulkEditForm
  153. template_name = 'ipam/aggregate_bulk_edit.html'
  154. default_redirect_url = 'ipam:aggregate_list'
  155. def update_objects(self, pk_list, form):
  156. fields_to_update = {}
  157. for field in ['rir', 'date_added', 'description']:
  158. if form.cleaned_data[field]:
  159. fields_to_update[field] = form.cleaned_data[field]
  160. return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  161. class AggregateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  162. permission_required = 'ipam.delete_aggregate'
  163. cls = Aggregate
  164. default_redirect_url = 'ipam:aggregate_list'
  165. #
  166. # Prefix/VLAN roles
  167. #
  168. class RoleListView(ObjectListView):
  169. queryset = Role.objects.all()
  170. table = tables.RoleTable
  171. edit_permissions = ['ipam.change_role', 'ipam.delete_role']
  172. template_name = 'ipam/role_list.html'
  173. class RoleEditView(PermissionRequiredMixin, ObjectEditView):
  174. permission_required = 'ipam.change_role'
  175. model = Role
  176. form_class = forms.RoleForm
  177. success_url = 'ipam:role_list'
  178. cancel_url = 'ipam:role_list'
  179. class RoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  180. permission_required = 'ipam.delete_role'
  181. cls = Role
  182. default_redirect_url = 'ipam:role_list'
  183. #
  184. # Prefixes
  185. #
  186. class PrefixListView(ObjectListView):
  187. queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'role')
  188. filter = filters.PrefixFilter
  189. filter_form = forms.PrefixFilterForm
  190. table = tables.PrefixTable
  191. edit_permissions = ['ipam.change_prefix', 'ipam.delete_prefix']
  192. template_name = 'ipam/prefix_list.html'
  193. def alter_queryset(self, request):
  194. # Show only top-level prefixes by default (unless searching)
  195. limit = None if request.GET.get('expand') or request.GET.get('q') else 0
  196. return self.queryset.annotate_depth(limit=limit)
  197. def prefix(request, pk):
  198. prefix = get_object_or_404(Prefix.objects.select_related('site', 'vlan', 'role'), pk=pk)
  199. try:
  200. aggregate = Aggregate.objects.get(prefix__net_contains_or_equals=str(prefix.prefix))
  201. except Aggregate.DoesNotExist:
  202. aggregate = None
  203. # Count child IP addresses
  204. ipaddress_count = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
  205. .count()
  206. # Parent prefixes table
  207. parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\
  208. .filter(prefix__net_contains=str(prefix.prefix))\
  209. .select_related('site', 'role').annotate_depth()
  210. parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
  211. # Duplicate prefixes table
  212. duplicate_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix=str(prefix.prefix)).exclude(pk=prefix.pk)\
  213. .select_related('site', 'role')
  214. duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
  215. # Child prefixes table
  216. if prefix.vrf:
  217. # If the prefix is in a VRF, show child prefixes only within that VRF.
  218. child_prefixes = Prefix.objects.filter(vrf=prefix.vrf)
  219. else:
  220. # If the prefix is in the global table, show child prefixes from all VRFs.
  221. child_prefixes = Prefix.objects.all()
  222. child_prefixes = child_prefixes.filter(prefix__net_contained=str(prefix.prefix))\
  223. .select_related('site', 'role').annotate_depth(limit=0)
  224. if child_prefixes:
  225. child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes)
  226. child_prefix_table = tables.PrefixTable(child_prefixes)
  227. child_prefix_table.model = Prefix
  228. if request.user.has_perm('ipam.change_prefix') or request.user.has_perm('ipam.delete_prefix'):
  229. child_prefix_table.base_columns['pk'].visible = True
  230. RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(child_prefix_table)
  231. return render(request, 'ipam/prefix.html', {
  232. 'prefix': prefix,
  233. 'aggregate': aggregate,
  234. 'ipaddress_count': ipaddress_count,
  235. 'parent_prefix_table': parent_prefix_table,
  236. 'child_prefix_table': child_prefix_table,
  237. 'duplicate_prefix_table': duplicate_prefix_table,
  238. })
  239. class PrefixEditView(PermissionRequiredMixin, ObjectEditView):
  240. permission_required = 'ipam.change_prefix'
  241. model = Prefix
  242. form_class = forms.PrefixForm
  243. fields_initial = ['site', 'vrf', 'prefix']
  244. cancel_url = 'ipam:prefix_list'
  245. class PrefixDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  246. permission_required = 'ipam.delete_prefix'
  247. model = Prefix
  248. redirect_url = 'ipam:prefix_list'
  249. class PrefixBulkImportView(PermissionRequiredMixin, BulkImportView):
  250. permission_required = 'ipam.add_prefix'
  251. form = forms.PrefixImportForm
  252. table = tables.PrefixTable
  253. template_name = 'ipam/prefix_import.html'
  254. obj_list_url = 'ipam:prefix_list'
  255. class PrefixBulkEditView(PermissionRequiredMixin, BulkEditView):
  256. permission_required = 'ipam.change_prefix'
  257. cls = Prefix
  258. form = forms.PrefixBulkEditForm
  259. template_name = 'ipam/prefix_bulk_edit.html'
  260. default_redirect_url = 'ipam:prefix_list'
  261. def update_objects(self, pk_list, form):
  262. fields_to_update = {}
  263. for field in ['vrf', 'tenant']:
  264. if form.cleaned_data[field] == 0:
  265. fields_to_update[field] = None
  266. elif form.cleaned_data[field]:
  267. fields_to_update[field] = form.cleaned_data[field]
  268. for field in ['site', 'status', 'role', 'description']:
  269. if form.cleaned_data[field]:
  270. fields_to_update[field] = form.cleaned_data[field]
  271. return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  272. class PrefixBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  273. permission_required = 'ipam.delete_prefix'
  274. cls = Prefix
  275. default_redirect_url = 'ipam:prefix_list'
  276. def prefix_ipaddresses(request, pk):
  277. prefix = get_object_or_404(Prefix.objects.all(), pk=pk)
  278. # Find all IPAddresses belonging to this Prefix
  279. ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\
  280. .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for')
  281. ip_table = tables.IPAddressTable(ipaddresses)
  282. ip_table.model = IPAddress
  283. if request.user.has_perm('ipam.change_ipaddress') or request.user.has_perm('ipam.delete_ipaddress'):
  284. ip_table.base_columns['pk'].visible = True
  285. RequestConfig(request, paginate={'klass': EnhancedPaginator}).configure(ip_table)
  286. return render(request, 'ipam/prefix_ipaddresses.html', {
  287. 'prefix': prefix,
  288. 'ip_table': ip_table,
  289. })
  290. #
  291. # IP addresses
  292. #
  293. class IPAddressListView(ObjectListView):
  294. queryset = IPAddress.objects.select_related('vrf__tenant', 'interface__device')
  295. filter = filters.IPAddressFilter
  296. filter_form = forms.IPAddressFilterForm
  297. table = tables.IPAddressTable
  298. edit_permissions = ['ipam.change_ipaddress', 'ipam.delete_ipaddress']
  299. template_name = 'ipam/ipaddress_list.html'
  300. def ipaddress(request, pk):
  301. ipaddress = get_object_or_404(IPAddress.objects.select_related('interface__device'), pk=pk)
  302. # Parent prefixes table
  303. parent_prefixes = Prefix.objects.filter(vrf=ipaddress.vrf, prefix__net_contains=str(ipaddress.address.ip))
  304. parent_prefixes_table = tables.PrefixBriefTable(parent_prefixes)
  305. # Duplicate IPs table
  306. duplicate_ips = IPAddress.objects.filter(vrf=ipaddress.vrf, address=str(ipaddress.address))\
  307. .exclude(pk=ipaddress.pk).select_related('interface__device', 'nat_inside')
  308. duplicate_ips_table = tables.IPAddressBriefTable(duplicate_ips)
  309. # Related IP table
  310. related_ips = IPAddress.objects.select_related('interface__device').exclude(address=str(ipaddress.address))\
  311. .filter(vrf=ipaddress.vrf, address__net_contained_or_equal=str(ipaddress.address))
  312. related_ips_table = tables.IPAddressBriefTable(related_ips)
  313. return render(request, 'ipam/ipaddress.html', {
  314. 'ipaddress': ipaddress,
  315. 'parent_prefixes_table': parent_prefixes_table,
  316. 'duplicate_ips_table': duplicate_ips_table,
  317. 'related_ips_table': related_ips_table,
  318. })
  319. class IPAddressEditView(PermissionRequiredMixin, ObjectEditView):
  320. permission_required = 'ipam.change_ipaddress'
  321. model = IPAddress
  322. form_class = forms.IPAddressForm
  323. fields_initial = ['address', 'vrf']
  324. template_name = 'ipam/ipaddress_edit.html'
  325. cancel_url = 'ipam:ipaddress_list'
  326. class IPAddressDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  327. permission_required = 'ipam.delete_ipaddress'
  328. model = IPAddress
  329. redirect_url = 'ipam:ipaddress_list'
  330. class IPAddressBulkImportView(PermissionRequiredMixin, BulkImportView):
  331. permission_required = 'ipam.add_ipaddress'
  332. form = forms.IPAddressImportForm
  333. table = tables.IPAddressTable
  334. template_name = 'ipam/ipaddress_import.html'
  335. obj_list_url = 'ipam:ipaddress_list'
  336. def save_obj(self, obj):
  337. obj.save()
  338. # Update primary IP for device if needed
  339. try:
  340. if obj.family == 4 and obj.primary_ip4_for:
  341. device = obj.primary_ip4_for
  342. device.primary_ip4 = obj
  343. device.save()
  344. elif obj.family == 6 and obj.primary_ip6_for:
  345. device = obj.primary_ip6_for
  346. device.primary_ip6 = obj
  347. device.save()
  348. except Device.DoesNotExist:
  349. pass
  350. class IPAddressBulkEditView(PermissionRequiredMixin, BulkEditView):
  351. permission_required = 'ipam.change_ipaddress'
  352. cls = IPAddress
  353. form = forms.IPAddressBulkEditForm
  354. template_name = 'ipam/ipaddress_bulk_edit.html'
  355. default_redirect_url = 'ipam:ipaddress_list'
  356. def update_objects(self, pk_list, form):
  357. fields_to_update = {}
  358. for field in ['vrf', 'tenant']:
  359. if form.cleaned_data[field] == 0:
  360. fields_to_update[field] = None
  361. elif form.cleaned_data[field]:
  362. fields_to_update[field] = form.cleaned_data[field]
  363. for field in ['description']:
  364. if form.cleaned_data[field]:
  365. fields_to_update[field] = form.cleaned_data[field]
  366. return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  367. class IPAddressBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  368. permission_required = 'ipam.delete_ipaddress'
  369. cls = IPAddress
  370. default_redirect_url = 'ipam:ipaddress_list'
  371. #
  372. # VLAN groups
  373. #
  374. class VLANGroupListView(ObjectListView):
  375. queryset = VLANGroup.objects.annotate(vlan_count=Count('vlans'))
  376. filter = filters.VLANGroupFilter
  377. filter_form = forms.VLANGroupFilterForm
  378. table = tables.VLANGroupTable
  379. edit_permissions = ['ipam.change_vlangroup', 'ipam.delete_vlangroup']
  380. template_name = 'ipam/vlangroup_list.html'
  381. class VLANGroupEditView(PermissionRequiredMixin, ObjectEditView):
  382. permission_required = 'ipam.change_vlangroup'
  383. model = VLANGroup
  384. form_class = forms.VLANGroupForm
  385. cancel_url = 'ipam:vlangroup_list'
  386. class VLANGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  387. permission_required = 'ipam.delete_vlangroup'
  388. cls = VLANGroup
  389. default_redirect_url = 'ipam:vlangroup_list'
  390. #
  391. # VLANs
  392. #
  393. class VLANListView(ObjectListView):
  394. queryset = VLAN.objects.select_related('site', 'role')
  395. filter = filters.VLANFilter
  396. filter_form = forms.VLANFilterForm
  397. table = tables.VLANTable
  398. edit_permissions = ['ipam.change_vlan', 'ipam.delete_vlan']
  399. template_name = 'ipam/vlan_list.html'
  400. def vlan(request, pk):
  401. vlan = get_object_or_404(VLAN.objects.select_related('site', 'role'), pk=pk)
  402. prefixes = Prefix.objects.filter(vlan=vlan)
  403. prefix_table = tables.PrefixBriefTable(prefixes)
  404. return render(request, 'ipam/vlan.html', {
  405. 'vlan': vlan,
  406. 'prefix_table': prefix_table,
  407. })
  408. class VLANEditView(PermissionRequiredMixin, ObjectEditView):
  409. permission_required = 'ipam.change_vlan'
  410. model = VLAN
  411. form_class = forms.VLANForm
  412. cancel_url = 'ipam:vlan_list'
  413. class VLANDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  414. permission_required = 'ipam.delete_vlan'
  415. model = VLAN
  416. redirect_url = 'ipam:vlan_list'
  417. class VLANBulkImportView(PermissionRequiredMixin, BulkImportView):
  418. permission_required = 'ipam.add_vlan'
  419. form = forms.VLANImportForm
  420. table = tables.VLANTable
  421. template_name = 'ipam/vlan_import.html'
  422. obj_list_url = 'ipam:vlan_list'
  423. class VLANBulkEditView(PermissionRequiredMixin, BulkEditView):
  424. permission_required = 'ipam.change_vlan'
  425. cls = VLAN
  426. form = forms.VLANBulkEditForm
  427. template_name = 'ipam/vlan_bulk_edit.html'
  428. default_redirect_url = 'ipam:vlan_list'
  429. def update_objects(self, pk_list, form):
  430. fields_to_update = {}
  431. if form.cleaned_data['tenant'] == 0:
  432. fields_to_update['tenant'] = None
  433. elif form.cleaned_data['tenant']:
  434. fields_to_update['tenant'] = form.cleaned_data['tenant']
  435. for field in ['site', 'group', 'status', 'role', 'description']:
  436. if form.cleaned_data[field]:
  437. fields_to_update[field] = form.cleaned_data[field]
  438. return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
  439. class VLANBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  440. permission_required = 'ipam.delete_vlan'
  441. cls = VLAN
  442. default_redirect_url = 'ipam:vlan_list'