views.py 58 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722
  1. from __future__ import unicode_literals
  2. from copy import deepcopy
  3. from difflib import SequenceMatcher
  4. import re
  5. from natsort import natsorted
  6. from operator import attrgetter
  7. from django.contrib import messages
  8. from django.contrib.auth.decorators import permission_required
  9. from django.contrib.auth.mixins import PermissionRequiredMixin
  10. from django.core.paginator import EmptyPage, PageNotAnInteger
  11. from django.db.models import Count
  12. from django.http import HttpResponseRedirect
  13. from django.shortcuts import get_object_or_404, redirect, render
  14. from django.urls import reverse
  15. from django.utils.html import escape
  16. from django.utils.http import urlencode
  17. from django.utils.safestring import mark_safe
  18. from django.views.generic import View
  19. from ipam.models import Prefix, Service, VLAN
  20. from circuits.models import Circuit
  21. from extras.models import Graph, TopologyMap, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE, UserAction
  22. from utilities.forms import ConfirmationForm
  23. from utilities.paginator import EnhancedPaginator
  24. from utilities.views import (
  25. BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
  26. )
  27. from . import filters, forms, tables
  28. from .models import (
  29. CONNECTION_STATUS_CONNECTED, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device,
  30. DeviceBay, DeviceBayTemplate, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate,
  31. Manufacturer, InventoryItem, Platform, PowerOutlet, PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack,
  32. RackGroup, RackReservation, RackRole, Region, Site,
  33. )
  34. EXPANSION_PATTERN = '\[(\d+-\d+)\]'
  35. def xstr(s):
  36. """
  37. Replace None with an empty string (for CSV export)
  38. """
  39. return '' if s is None else str(s)
  40. def expand_pattern(string):
  41. """
  42. Expand a numeric pattern into a list of strings. Examples:
  43. 'ge-0/0/[0-3]' => ['ge-0/0/0', 'ge-0/0/1', 'ge-0/0/2', 'ge-0/0/3']
  44. 'xe-0/[0-3]/[0-7]' => ['xe-0/0/0', 'xe-0/0/1', 'xe-0/0/2', ... 'xe-0/3/5', 'xe-0/3/6', 'xe-0/3/7']
  45. """
  46. lead, pattern, remnant = re.split(EXPANSION_PATTERN, string, maxsplit=1)
  47. x, y = pattern.split('-')
  48. for i in range(int(x), int(y) + 1):
  49. if remnant:
  50. for string in expand_pattern(remnant):
  51. yield "{0}{1}{2}".format(lead, i, string)
  52. else:
  53. yield "{0}{1}".format(lead, i)
  54. class ComponentCreateView(View):
  55. parent_model = None
  56. parent_field = None
  57. model = None
  58. form = None
  59. model_form = None
  60. def get(self, request, pk):
  61. parent = get_object_or_404(self.parent_model, pk=pk)
  62. form = self.form(parent, initial=request.GET)
  63. return render(request, 'dcim/device_component_add.html', {
  64. 'parent': parent,
  65. 'component_type': self.model._meta.verbose_name,
  66. 'form': form,
  67. 'return_url': parent.get_absolute_url(),
  68. })
  69. def post(self, request, pk):
  70. parent = get_object_or_404(self.parent_model, pk=pk)
  71. form = self.form(parent, request.POST)
  72. if form.is_valid():
  73. new_components = []
  74. data = deepcopy(form.cleaned_data)
  75. for name in form.cleaned_data['name_pattern']:
  76. component_data = {
  77. self.parent_field: parent.pk,
  78. 'name': name,
  79. }
  80. # Replace objects with their primary key to keep component_form.clean() happy
  81. for k, v in data.items():
  82. if hasattr(v, 'pk'):
  83. component_data[k] = v.pk
  84. else:
  85. component_data[k] = v
  86. component_form = self.model_form(component_data)
  87. if component_form.is_valid():
  88. new_components.append(component_form.save(commit=False))
  89. else:
  90. for field, errors in component_form.errors.as_data().items():
  91. # Assign errors on the child form's name field to name_pattern on the parent form
  92. if field == 'name':
  93. field = 'name_pattern'
  94. for e in errors:
  95. form.add_error(field, '{}: {}'.format(name, ', '.join(e)))
  96. if not form.errors:
  97. self.model.objects.bulk_create(new_components)
  98. messages.success(request, "Added {} {} to {}.".format(
  99. len(new_components), self.model._meta.verbose_name_plural, parent
  100. ))
  101. if '_addanother' in request.POST:
  102. return redirect(request.path)
  103. else:
  104. return redirect(parent.get_absolute_url())
  105. return render(request, 'dcim/device_component_add.html', {
  106. 'parent': parent,
  107. 'component_type': self.model._meta.verbose_name,
  108. 'form': form,
  109. 'return_url': parent.get_absolute_url(),
  110. })
  111. class ComponentEditView(ObjectEditView):
  112. def get_return_url(self, request, obj):
  113. return obj.device.get_absolute_url()
  114. class ComponentDeleteView(ObjectDeleteView):
  115. def get_return_url(self, request, obj):
  116. return obj.device.get_absolute_url()
  117. #
  118. # Regions
  119. #
  120. class RegionListView(ObjectListView):
  121. queryset = Region.objects.annotate(site_count=Count('sites'))
  122. table = tables.RegionTable
  123. template_name = 'dcim/region_list.html'
  124. class RegionEditView(PermissionRequiredMixin, ObjectEditView):
  125. permission_required = 'dcim.change_region'
  126. model = Region
  127. form_class = forms.RegionForm
  128. def get_return_url(self, request, obj):
  129. return reverse('dcim:region_list')
  130. class RegionBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  131. permission_required = 'dcim.delete_region'
  132. cls = Region
  133. default_return_url = 'dcim:region_list'
  134. #
  135. # Sites
  136. #
  137. class SiteListView(ObjectListView):
  138. queryset = Site.objects.select_related('region', 'tenant')
  139. filter = filters.SiteFilter
  140. filter_form = forms.SiteFilterForm
  141. table = tables.SiteTable
  142. template_name = 'dcim/site_list.html'
  143. class SiteView(View):
  144. def get(self, request, slug):
  145. site = get_object_or_404(Site.objects.select_related('region', 'tenant__group'), slug=slug)
  146. stats = {
  147. 'rack_count': Rack.objects.filter(site=site).count(),
  148. 'device_count': Device.objects.filter(site=site).count(),
  149. 'prefix_count': Prefix.objects.filter(site=site).count(),
  150. 'vlan_count': VLAN.objects.filter(site=site).count(),
  151. 'circuit_count': Circuit.objects.filter(terminations__site=site).count(),
  152. }
  153. rack_groups = RackGroup.objects.filter(site=site).annotate(rack_count=Count('racks'))
  154. topology_maps = TopologyMap.objects.filter(site=site)
  155. show_graphs = Graph.objects.filter(type=GRAPH_TYPE_SITE).exists()
  156. return render(request, 'dcim/site.html', {
  157. 'site': site,
  158. 'stats': stats,
  159. 'rack_groups': rack_groups,
  160. 'topology_maps': topology_maps,
  161. 'show_graphs': show_graphs,
  162. })
  163. class SiteEditView(PermissionRequiredMixin, ObjectEditView):
  164. permission_required = 'dcim.change_site'
  165. model = Site
  166. form_class = forms.SiteForm
  167. template_name = 'dcim/site_edit.html'
  168. default_return_url = 'dcim:site_list'
  169. class SiteDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  170. permission_required = 'dcim.delete_site'
  171. model = Site
  172. default_return_url = 'dcim:site_list'
  173. class SiteBulkImportView(PermissionRequiredMixin, BulkImportView):
  174. permission_required = 'dcim.add_site'
  175. model_form = forms.SiteCSVForm
  176. table = tables.SiteTable
  177. default_return_url = 'dcim:site_list'
  178. class SiteBulkEditView(PermissionRequiredMixin, BulkEditView):
  179. permission_required = 'dcim.change_site'
  180. cls = Site
  181. filter = filters.SiteFilter
  182. form = forms.SiteBulkEditForm
  183. template_name = 'dcim/site_bulk_edit.html'
  184. default_return_url = 'dcim:site_list'
  185. #
  186. # Rack groups
  187. #
  188. class RackGroupListView(ObjectListView):
  189. queryset = RackGroup.objects.select_related('site').annotate(rack_count=Count('racks'))
  190. filter = filters.RackGroupFilter
  191. filter_form = forms.RackGroupFilterForm
  192. table = tables.RackGroupTable
  193. template_name = 'dcim/rackgroup_list.html'
  194. class RackGroupEditView(PermissionRequiredMixin, ObjectEditView):
  195. permission_required = 'dcim.change_rackgroup'
  196. model = RackGroup
  197. form_class = forms.RackGroupForm
  198. def get_return_url(self, request, obj):
  199. return reverse('dcim:rackgroup_list')
  200. class RackGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  201. permission_required = 'dcim.delete_rackgroup'
  202. cls = RackGroup
  203. filter = filters.RackGroupFilter
  204. default_return_url = 'dcim:rackgroup_list'
  205. #
  206. # Rack roles
  207. #
  208. class RackRoleListView(ObjectListView):
  209. queryset = RackRole.objects.annotate(rack_count=Count('racks'))
  210. table = tables.RackRoleTable
  211. template_name = 'dcim/rackrole_list.html'
  212. class RackRoleEditView(PermissionRequiredMixin, ObjectEditView):
  213. permission_required = 'dcim.change_rackrole'
  214. model = RackRole
  215. form_class = forms.RackRoleForm
  216. def get_return_url(self, request, obj):
  217. return reverse('dcim:rackrole_list')
  218. class RackRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  219. permission_required = 'dcim.delete_rackrole'
  220. cls = RackRole
  221. default_return_url = 'dcim:rackrole_list'
  222. #
  223. # Racks
  224. #
  225. class RackListView(ObjectListView):
  226. queryset = Rack.objects.select_related(
  227. 'site', 'group', 'tenant', 'role'
  228. ).prefetch_related(
  229. 'devices__device_type'
  230. ).annotate(
  231. device_count=Count('devices', distinct=True)
  232. )
  233. filter = filters.RackFilter
  234. filter_form = forms.RackFilterForm
  235. table = tables.RackTable
  236. template_name = 'dcim/rack_list.html'
  237. class RackElevationListView(View):
  238. """
  239. Display a set of rack elevations side-by-side.
  240. """
  241. def get(self, request):
  242. racks = Rack.objects.select_related(
  243. 'site', 'group', 'tenant', 'role'
  244. ).prefetch_related(
  245. 'devices__device_type'
  246. )
  247. racks = filters.RackFilter(request.GET, racks).qs
  248. total_count = racks.count()
  249. # Pagination
  250. paginator = EnhancedPaginator(racks, 25)
  251. page_number = request.GET.get('page', 1)
  252. try:
  253. page = paginator.page(page_number)
  254. except PageNotAnInteger:
  255. page = paginator.page(1)
  256. except EmptyPage:
  257. page = paginator.page(paginator.num_pages)
  258. # Determine rack face
  259. if request.GET.get('face') == '1':
  260. face_id = 1
  261. else:
  262. face_id = 0
  263. return render(request, 'dcim/rack_elevation_list.html', {
  264. 'paginator': paginator,
  265. 'page': page,
  266. 'total_count': total_count,
  267. 'face_id': face_id,
  268. 'filter_form': forms.RackFilterForm(request.GET),
  269. })
  270. class RackView(View):
  271. def get(self, request, pk):
  272. rack = get_object_or_404(Rack.objects.select_related('site__region', 'tenant__group', 'group', 'role'), pk=pk)
  273. nonracked_devices = Device.objects.filter(rack=rack, position__isnull=True, parent_bay__isnull=True)\
  274. .select_related('device_type__manufacturer')
  275. next_rack = Rack.objects.filter(site=rack.site, name__gt=rack.name).order_by('name').first()
  276. prev_rack = Rack.objects.filter(site=rack.site, name__lt=rack.name).order_by('-name').first()
  277. reservations = RackReservation.objects.filter(rack=rack)
  278. reserved_units = {}
  279. for r in reservations:
  280. for u in r.units:
  281. reserved_units[u] = r
  282. return render(request, 'dcim/rack.html', {
  283. 'rack': rack,
  284. 'reservations': reservations,
  285. 'reserved_units': reserved_units,
  286. 'nonracked_devices': nonracked_devices,
  287. 'next_rack': next_rack,
  288. 'prev_rack': prev_rack,
  289. 'front_elevation': rack.get_front_elevation(),
  290. 'rear_elevation': rack.get_rear_elevation(),
  291. })
  292. class RackEditView(PermissionRequiredMixin, ObjectEditView):
  293. permission_required = 'dcim.change_rack'
  294. model = Rack
  295. form_class = forms.RackForm
  296. template_name = 'dcim/rack_edit.html'
  297. default_return_url = 'dcim:rack_list'
  298. class RackDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  299. permission_required = 'dcim.delete_rack'
  300. model = Rack
  301. default_return_url = 'dcim:rack_list'
  302. class RackBulkImportView(PermissionRequiredMixin, BulkImportView):
  303. permission_required = 'dcim.add_rack'
  304. model_form = forms.RackCSVForm
  305. table = tables.RackImportTable
  306. default_return_url = 'dcim:rack_list'
  307. class RackBulkEditView(PermissionRequiredMixin, BulkEditView):
  308. permission_required = 'dcim.change_rack'
  309. cls = Rack
  310. filter = filters.RackFilter
  311. form = forms.RackBulkEditForm
  312. template_name = 'dcim/rack_bulk_edit.html'
  313. default_return_url = 'dcim:rack_list'
  314. class RackBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  315. permission_required = 'dcim.delete_rack'
  316. cls = Rack
  317. filter = filters.RackFilter
  318. default_return_url = 'dcim:rack_list'
  319. #
  320. # Rack reservations
  321. #
  322. class RackReservationListView(ObjectListView):
  323. queryset = RackReservation.objects.all()
  324. filter = filters.RackReservationFilter
  325. filter_form = forms.RackReservationFilterForm
  326. table = tables.RackReservationTable
  327. template_name = 'dcim/rackreservation_list.html'
  328. class RackReservationEditView(PermissionRequiredMixin, ObjectEditView):
  329. permission_required = 'dcim.change_rackreservation'
  330. model = RackReservation
  331. form_class = forms.RackReservationForm
  332. def alter_obj(self, obj, request, args, kwargs):
  333. if not obj.pk:
  334. obj.rack = get_object_or_404(Rack, pk=kwargs['rack'])
  335. obj.user = request.user
  336. return obj
  337. def get_return_url(self, request, obj):
  338. return obj.rack.get_absolute_url()
  339. class RackReservationDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  340. permission_required = 'dcim.delete_rackreservation'
  341. model = RackReservation
  342. def get_return_url(self, request, obj):
  343. return obj.rack.get_absolute_url()
  344. class RackReservationBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  345. permission_required = 'dcim.delete_rackreservation'
  346. cls = RackReservation
  347. default_return_url = 'dcim:rackreservation_list'
  348. #
  349. # Manufacturers
  350. #
  351. class ManufacturerListView(ObjectListView):
  352. queryset = Manufacturer.objects.annotate(devicetype_count=Count('device_types'))
  353. table = tables.ManufacturerTable
  354. template_name = 'dcim/manufacturer_list.html'
  355. class ManufacturerEditView(PermissionRequiredMixin, ObjectEditView):
  356. permission_required = 'dcim.change_manufacturer'
  357. model = Manufacturer
  358. form_class = forms.ManufacturerForm
  359. def get_return_url(self, request, obj):
  360. return reverse('dcim:manufacturer_list')
  361. class ManufacturerBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  362. permission_required = 'dcim.delete_manufacturer'
  363. cls = Manufacturer
  364. default_return_url = 'dcim:manufacturer_list'
  365. #
  366. # Device types
  367. #
  368. class DeviceTypeListView(ObjectListView):
  369. queryset = DeviceType.objects.select_related('manufacturer').annotate(instance_count=Count('instances'))
  370. filter = filters.DeviceTypeFilter
  371. filter_form = forms.DeviceTypeFilterForm
  372. table = tables.DeviceTypeTable
  373. template_name = 'dcim/devicetype_list.html'
  374. class DeviceTypeView(View):
  375. def get(self, request, pk):
  376. devicetype = get_object_or_404(DeviceType, pk=pk)
  377. # Component tables
  378. consoleport_table = tables.ConsolePortTemplateTable(
  379. natsorted(ConsolePortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
  380. )
  381. consoleserverport_table = tables.ConsoleServerPortTemplateTable(
  382. natsorted(ConsoleServerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
  383. )
  384. powerport_table = tables.PowerPortTemplateTable(
  385. natsorted(PowerPortTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
  386. )
  387. poweroutlet_table = tables.PowerOutletTemplateTable(
  388. natsorted(PowerOutletTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
  389. )
  390. mgmt_interface_table = tables.InterfaceTemplateTable(
  391. list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
  392. device_type=devicetype, mgmt_only=True
  393. ))
  394. )
  395. interface_table = tables.InterfaceTemplateTable(
  396. list(InterfaceTemplate.objects.order_naturally(devicetype.interface_ordering).filter(
  397. device_type=devicetype, mgmt_only=False
  398. ))
  399. )
  400. devicebay_table = tables.DeviceBayTemplateTable(
  401. natsorted(DeviceBayTemplate.objects.filter(device_type=devicetype), key=attrgetter('name'))
  402. )
  403. if request.user.has_perm('dcim.change_devicetype'):
  404. consoleport_table.base_columns['pk'].visible = True
  405. consoleserverport_table.base_columns['pk'].visible = True
  406. powerport_table.base_columns['pk'].visible = True
  407. poweroutlet_table.base_columns['pk'].visible = True
  408. mgmt_interface_table.base_columns['pk'].visible = True
  409. interface_table.base_columns['pk'].visible = True
  410. devicebay_table.base_columns['pk'].visible = True
  411. return render(request, 'dcim/devicetype.html', {
  412. 'devicetype': devicetype,
  413. 'consoleport_table': consoleport_table,
  414. 'consoleserverport_table': consoleserverport_table,
  415. 'powerport_table': powerport_table,
  416. 'poweroutlet_table': poweroutlet_table,
  417. 'mgmt_interface_table': mgmt_interface_table,
  418. 'interface_table': interface_table,
  419. 'devicebay_table': devicebay_table,
  420. })
  421. class DeviceTypeEditView(PermissionRequiredMixin, ObjectEditView):
  422. permission_required = 'dcim.change_devicetype'
  423. model = DeviceType
  424. form_class = forms.DeviceTypeForm
  425. template_name = 'dcim/devicetype_edit.html'
  426. default_return_url = 'dcim:devicetype_list'
  427. class DeviceTypeDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  428. permission_required = 'dcim.delete_devicetype'
  429. model = DeviceType
  430. default_return_url = 'dcim:devicetype_list'
  431. class DeviceTypeBulkEditView(PermissionRequiredMixin, BulkEditView):
  432. permission_required = 'dcim.change_devicetype'
  433. cls = DeviceType
  434. filter = filters.DeviceTypeFilter
  435. form = forms.DeviceTypeBulkEditForm
  436. template_name = 'dcim/devicetype_bulk_edit.html'
  437. default_return_url = 'dcim:devicetype_list'
  438. class DeviceTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  439. permission_required = 'dcim.delete_devicetype'
  440. cls = DeviceType
  441. filter = filters.DeviceTypeFilter
  442. default_return_url = 'dcim:devicetype_list'
  443. #
  444. # Device type components
  445. #
  446. class ConsolePortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  447. permission_required = 'dcim.add_consoleporttemplate'
  448. parent_model = DeviceType
  449. parent_field = 'device_type'
  450. model = ConsolePortTemplate
  451. form = forms.ConsolePortTemplateCreateForm
  452. model_form = forms.ConsolePortTemplateForm
  453. class ConsolePortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  454. permission_required = 'dcim.delete_consoleporttemplate'
  455. parent_model = DeviceType
  456. parent_field = 'device_type'
  457. cls = ConsolePortTemplate
  458. parent_cls = DeviceType
  459. class ConsoleServerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  460. permission_required = 'dcim.add_consoleserverporttemplate'
  461. parent_model = DeviceType
  462. parent_field = 'device_type'
  463. model = ConsoleServerPortTemplate
  464. form = forms.ConsoleServerPortTemplateCreateForm
  465. model_form = forms.ConsoleServerPortTemplateForm
  466. class ConsoleServerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  467. permission_required = 'dcim.delete_consoleserverporttemplate'
  468. cls = ConsoleServerPortTemplate
  469. parent_cls = DeviceType
  470. class PowerPortTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  471. permission_required = 'dcim.add_powerporttemplate'
  472. parent_model = DeviceType
  473. parent_field = 'device_type'
  474. model = PowerPortTemplate
  475. form = forms.PowerPortTemplateCreateForm
  476. model_form = forms.PowerPortTemplateForm
  477. class PowerPortTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  478. permission_required = 'dcim.delete_powerporttemplate'
  479. cls = PowerPortTemplate
  480. parent_cls = DeviceType
  481. class PowerOutletTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  482. permission_required = 'dcim.add_poweroutlettemplate'
  483. parent_model = DeviceType
  484. parent_field = 'device_type'
  485. model = PowerOutletTemplate
  486. form = forms.PowerOutletTemplateCreateForm
  487. model_form = forms.PowerOutletTemplateForm
  488. class PowerOutletTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  489. permission_required = 'dcim.delete_poweroutlettemplate'
  490. cls = PowerOutletTemplate
  491. parent_cls = DeviceType
  492. class InterfaceTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  493. permission_required = 'dcim.add_interfacetemplate'
  494. parent_model = DeviceType
  495. parent_field = 'device_type'
  496. model = InterfaceTemplate
  497. form = forms.InterfaceTemplateCreateForm
  498. model_form = forms.InterfaceTemplateForm
  499. class InterfaceTemplateBulkEditView(PermissionRequiredMixin, BulkEditView):
  500. permission_required = 'dcim.change_interfacetemplate'
  501. cls = InterfaceTemplate
  502. parent_cls = DeviceType
  503. form = forms.InterfaceTemplateBulkEditForm
  504. template_name = 'dcim/interfacetemplate_bulk_edit.html'
  505. class InterfaceTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  506. permission_required = 'dcim.delete_interfacetemplate'
  507. cls = InterfaceTemplate
  508. parent_cls = DeviceType
  509. class DeviceBayTemplateAddView(PermissionRequiredMixin, ComponentCreateView):
  510. permission_required = 'dcim.add_devicebaytemplate'
  511. parent_model = DeviceType
  512. parent_field = 'device_type'
  513. model = DeviceBayTemplate
  514. form = forms.DeviceBayTemplateCreateForm
  515. model_form = forms.DeviceBayTemplateForm
  516. class DeviceBayTemplateBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  517. permission_required = 'dcim.delete_devicebaytemplate'
  518. cls = DeviceBayTemplate
  519. parent_cls = DeviceType
  520. #
  521. # Device roles
  522. #
  523. class DeviceRoleListView(ObjectListView):
  524. queryset = DeviceRole.objects.annotate(device_count=Count('devices'))
  525. table = tables.DeviceRoleTable
  526. template_name = 'dcim/devicerole_list.html'
  527. class DeviceRoleEditView(PermissionRequiredMixin, ObjectEditView):
  528. permission_required = 'dcim.change_devicerole'
  529. model = DeviceRole
  530. form_class = forms.DeviceRoleForm
  531. def get_return_url(self, request, obj):
  532. return reverse('dcim:devicerole_list')
  533. class DeviceRoleBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  534. permission_required = 'dcim.delete_devicerole'
  535. cls = DeviceRole
  536. default_return_url = 'dcim:devicerole_list'
  537. #
  538. # Platforms
  539. #
  540. class PlatformListView(ObjectListView):
  541. queryset = Platform.objects.annotate(device_count=Count('devices'))
  542. table = tables.PlatformTable
  543. template_name = 'dcim/platform_list.html'
  544. class PlatformEditView(PermissionRequiredMixin, ObjectEditView):
  545. permission_required = 'dcim.change_platform'
  546. model = Platform
  547. form_class = forms.PlatformForm
  548. def get_return_url(self, request, obj):
  549. return reverse('dcim:platform_list')
  550. class PlatformBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  551. permission_required = 'dcim.delete_platform'
  552. cls = Platform
  553. default_return_url = 'dcim:platform_list'
  554. #
  555. # Devices
  556. #
  557. class DeviceListView(ObjectListView):
  558. queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'site', 'rack',
  559. 'primary_ip4', 'primary_ip6')
  560. filter = filters.DeviceFilter
  561. filter_form = forms.DeviceFilterForm
  562. table = tables.DeviceTable
  563. template_name = 'dcim/device_list.html'
  564. class DeviceView(View):
  565. def get(self, request, pk):
  566. device = get_object_or_404(Device.objects.select_related(
  567. 'site__region', 'rack__group', 'tenant__group', 'device_role', 'platform'
  568. ), pk=pk)
  569. console_ports = natsorted(
  570. ConsolePort.objects.filter(device=device).select_related('cs_port__device'), key=attrgetter('name')
  571. )
  572. cs_ports = natsorted(
  573. ConsoleServerPort.objects.filter(device=device).select_related('connected_console'), key=attrgetter('name')
  574. )
  575. power_ports = natsorted(
  576. PowerPort.objects.filter(device=device).select_related('power_outlet__device'), key=attrgetter('name')
  577. )
  578. power_outlets = natsorted(
  579. PowerOutlet.objects.filter(device=device).select_related('connected_port'), key=attrgetter('name')
  580. )
  581. interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
  582. device=device, mgmt_only=False
  583. ).select_related(
  584. 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
  585. 'circuit_termination__circuit'
  586. ).prefetch_related('ip_addresses')
  587. mgmt_interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(
  588. device=device, mgmt_only=True
  589. ).select_related(
  590. 'connected_as_a__interface_b__device', 'connected_as_b__interface_a__device',
  591. 'circuit_termination__circuit'
  592. ).prefetch_related('ip_addresses')
  593. device_bays = natsorted(
  594. DeviceBay.objects.filter(device=device).select_related('installed_device__device_type__manufacturer'),
  595. key=attrgetter('name')
  596. )
  597. services = Service.objects.filter(device=device)
  598. secrets = device.secrets.all()
  599. # Find up to ten devices in the same site with the same functional role for quick reference.
  600. related_devices = Device.objects.filter(
  601. site=device.site, device_role=device.device_role
  602. ).exclude(
  603. pk=device.pk
  604. ).select_related(
  605. 'rack', 'device_type__manufacturer'
  606. )[:10]
  607. # Show graph button on interfaces only if at least one graph has been created.
  608. show_graphs = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE).exists()
  609. return render(request, 'dcim/device.html', {
  610. 'device': device,
  611. 'console_ports': console_ports,
  612. 'cs_ports': cs_ports,
  613. 'power_ports': power_ports,
  614. 'power_outlets': power_outlets,
  615. 'interfaces': interfaces,
  616. 'mgmt_interfaces': mgmt_interfaces,
  617. 'device_bays': device_bays,
  618. 'services': services,
  619. 'secrets': secrets,
  620. 'related_devices': related_devices,
  621. 'show_graphs': show_graphs,
  622. })
  623. class DeviceInventoryView(View):
  624. def get(self, request, pk):
  625. device = get_object_or_404(Device, pk=pk)
  626. inventory_items = InventoryItem.objects.filter(
  627. device=device, parent=None
  628. ).select_related(
  629. 'manufacturer'
  630. ).prefetch_related(
  631. 'child_items'
  632. )
  633. return render(request, 'dcim/device_inventory.html', {
  634. 'device': device,
  635. 'inventory_items': inventory_items,
  636. })
  637. class DeviceLLDPNeighborsView(View):
  638. def get(self, request, pk):
  639. device = get_object_or_404(Device, pk=pk)
  640. interfaces = Interface.objects.order_naturally(
  641. device.device_type.interface_ordering
  642. ).filter(
  643. device=device
  644. ).select_related(
  645. 'connected_as_a', 'connected_as_b'
  646. )
  647. return render(request, 'dcim/device_lldp_neighbors.html', {
  648. 'device': device,
  649. 'interfaces': interfaces,
  650. })
  651. class DeviceEditView(PermissionRequiredMixin, ObjectEditView):
  652. permission_required = 'dcim.change_device'
  653. model = Device
  654. form_class = forms.DeviceForm
  655. template_name = 'dcim/device_edit.html'
  656. default_return_url = 'dcim:device_list'
  657. class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView):
  658. permission_required = 'dcim.delete_device'
  659. model = Device
  660. default_return_url = 'dcim:device_list'
  661. class DeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
  662. permission_required = 'dcim.add_device'
  663. model_form = forms.DeviceCSVForm
  664. table = tables.DeviceImportTable
  665. template_name = 'dcim/device_import.html'
  666. default_return_url = 'dcim:device_list'
  667. class ChildDeviceBulkImportView(PermissionRequiredMixin, BulkImportView):
  668. permission_required = 'dcim.add_device'
  669. model_form = forms.ChildDeviceCSVForm
  670. table = tables.DeviceImportTable
  671. template_name = 'dcim/device_import_child.html'
  672. default_return_url = 'dcim:device_list'
  673. def _save_obj(self, obj_form):
  674. obj = obj_form.save()
  675. # Save the reverse relation to the parent device bay
  676. device_bay = obj.parent_bay
  677. device_bay.installed_device = obj
  678. device_bay.save()
  679. return obj
  680. class DeviceBulkEditView(PermissionRequiredMixin, BulkEditView):
  681. permission_required = 'dcim.change_device'
  682. cls = Device
  683. filter = filters.DeviceFilter
  684. form = forms.DeviceBulkEditForm
  685. template_name = 'dcim/device_bulk_edit.html'
  686. default_return_url = 'dcim:device_list'
  687. class DeviceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  688. permission_required = 'dcim.delete_device'
  689. cls = Device
  690. filter = filters.DeviceFilter
  691. default_return_url = 'dcim:device_list'
  692. #
  693. # Console ports
  694. #
  695. class ConsolePortAddView(PermissionRequiredMixin, ComponentCreateView):
  696. permission_required = 'dcim.add_consoleport'
  697. parent_model = Device
  698. parent_field = 'device'
  699. model = ConsolePort
  700. form = forms.ConsolePortCreateForm
  701. model_form = forms.ConsolePortForm
  702. @permission_required('dcim.change_consoleport')
  703. def consoleport_connect(request, pk):
  704. consoleport = get_object_or_404(ConsolePort, pk=pk)
  705. if request.method == 'POST':
  706. form = forms.ConsolePortConnectionForm(request.POST, instance=consoleport)
  707. if form.is_valid():
  708. consoleport = form.save()
  709. msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
  710. consoleport.device.get_absolute_url(),
  711. escape(consoleport.device),
  712. escape(consoleport.name),
  713. consoleport.cs_port.device.get_absolute_url(),
  714. escape(consoleport.cs_port.device),
  715. escape(consoleport.cs_port.name),
  716. )
  717. messages.success(request, mark_safe(msg))
  718. UserAction.objects.log_edit(request.user, consoleport, msg)
  719. return redirect('dcim:device', pk=consoleport.device.pk)
  720. else:
  721. form = forms.ConsolePortConnectionForm(instance=consoleport, initial={
  722. 'site': request.GET.get('site'),
  723. 'rack': request.GET.get('rack'),
  724. 'console_server': request.GET.get('console_server'),
  725. 'connection_status': CONNECTION_STATUS_CONNECTED,
  726. })
  727. return render(request, 'dcim/consoleport_connect.html', {
  728. 'consoleport': consoleport,
  729. 'form': form,
  730. 'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
  731. })
  732. @permission_required('dcim.change_consoleport')
  733. def consoleport_disconnect(request, pk):
  734. consoleport = get_object_or_404(ConsolePort, pk=pk)
  735. if not consoleport.cs_port:
  736. messages.warning(
  737. request, "Cannot disconnect console port {}: It is not connected to anything.".format(consoleport)
  738. )
  739. return redirect('dcim:device', pk=consoleport.device.pk)
  740. if request.method == 'POST':
  741. form = ConfirmationForm(request.POST)
  742. if form.is_valid():
  743. cs_port = consoleport.cs_port
  744. consoleport.cs_port = None
  745. consoleport.connection_status = None
  746. consoleport.save()
  747. msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
  748. consoleport.device.get_absolute_url(),
  749. escape(consoleport.device),
  750. escape(consoleport.name),
  751. cs_port.device.get_absolute_url(),
  752. escape(cs_port.device),
  753. escape(cs_port.name),
  754. )
  755. messages.success(request, mark_safe(msg))
  756. UserAction.objects.log_edit(request.user, consoleport, msg)
  757. return redirect('dcim:device', pk=consoleport.device.pk)
  758. else:
  759. form = ConfirmationForm()
  760. return render(request, 'dcim/consoleport_disconnect.html', {
  761. 'consoleport': consoleport,
  762. 'form': form,
  763. 'return_url': reverse('dcim:device', kwargs={'pk': consoleport.device.pk}),
  764. })
  765. class ConsolePortEditView(PermissionRequiredMixin, ComponentEditView):
  766. permission_required = 'dcim.change_consoleport'
  767. model = ConsolePort
  768. form_class = forms.ConsolePortForm
  769. class ConsolePortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  770. permission_required = 'dcim.delete_consoleport'
  771. model = ConsolePort
  772. class ConsolePortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  773. permission_required = 'dcim.delete_consoleport'
  774. cls = ConsolePort
  775. parent_cls = Device
  776. class ConsoleConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
  777. permission_required = 'dcim.change_consoleport'
  778. model_form = forms.ConsoleConnectionCSVForm
  779. table = tables.ConsoleConnectionTable
  780. default_return_url = 'dcim:console_connections_list'
  781. #
  782. # Console server ports
  783. #
  784. class ConsoleServerPortAddView(PermissionRequiredMixin, ComponentCreateView):
  785. permission_required = 'dcim.add_consoleserverport'
  786. parent_model = Device
  787. parent_field = 'device'
  788. model = ConsoleServerPort
  789. form = forms.ConsoleServerPortCreateForm
  790. model_form = forms.ConsoleServerPortForm
  791. @permission_required('dcim.change_consoleserverport')
  792. def consoleserverport_connect(request, pk):
  793. consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
  794. if request.method == 'POST':
  795. form = forms.ConsoleServerPortConnectionForm(request.POST)
  796. if form.is_valid():
  797. consoleport = form.cleaned_data['port']
  798. consoleport.cs_port = consoleserverport
  799. consoleport.connection_status = form.cleaned_data['connection_status']
  800. consoleport.save()
  801. msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
  802. consoleport.device.get_absolute_url(),
  803. escape(consoleport.device),
  804. escape(consoleport.name),
  805. consoleserverport.device.get_absolute_url(),
  806. escape(consoleserverport.device),
  807. escape(consoleserverport.name),
  808. )
  809. messages.success(request, mark_safe(msg))
  810. UserAction.objects.log_edit(request.user, consoleport, msg)
  811. return redirect('dcim:device', pk=consoleserverport.device.pk)
  812. else:
  813. form = forms.ConsoleServerPortConnectionForm(initial={
  814. 'site': request.GET.get('site'),
  815. 'rack': request.GET.get('rack'),
  816. 'device': request.GET.get('device'),
  817. 'connection_status': CONNECTION_STATUS_CONNECTED,
  818. })
  819. return render(request, 'dcim/consoleserverport_connect.html', {
  820. 'consoleserverport': consoleserverport,
  821. 'form': form,
  822. 'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
  823. })
  824. @permission_required('dcim.change_consoleserverport')
  825. def consoleserverport_disconnect(request, pk):
  826. consoleserverport = get_object_or_404(ConsoleServerPort, pk=pk)
  827. if not hasattr(consoleserverport, 'connected_console'):
  828. messages.warning(
  829. request, "Cannot disconnect console server port {}: Nothing is connected to it.".format(consoleserverport)
  830. )
  831. return redirect('dcim:device', pk=consoleserverport.device.pk)
  832. if request.method == 'POST':
  833. form = ConfirmationForm(request.POST)
  834. if form.is_valid():
  835. consoleport = consoleserverport.connected_console
  836. consoleport.cs_port = None
  837. consoleport.connection_status = None
  838. consoleport.save()
  839. msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
  840. consoleport.device.get_absolute_url(),
  841. escape(consoleport.device),
  842. escape(consoleport.name),
  843. consoleserverport.device.get_absolute_url(),
  844. escape(consoleserverport.device),
  845. escape(consoleserverport.name),
  846. )
  847. messages.success(request, mark_safe(msg))
  848. UserAction.objects.log_edit(request.user, consoleport, msg)
  849. return redirect('dcim:device', pk=consoleserverport.device.pk)
  850. else:
  851. form = ConfirmationForm()
  852. return render(request, 'dcim/consoleserverport_disconnect.html', {
  853. 'consoleserverport': consoleserverport,
  854. 'form': form,
  855. 'return_url': reverse('dcim:device', kwargs={'pk': consoleserverport.device.pk}),
  856. })
  857. class ConsoleServerPortEditView(PermissionRequiredMixin, ComponentEditView):
  858. permission_required = 'dcim.change_consoleserverport'
  859. model = ConsoleServerPort
  860. form_class = forms.ConsoleServerPortForm
  861. class ConsoleServerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  862. permission_required = 'dcim.delete_consoleserverport'
  863. model = ConsoleServerPort
  864. class ConsoleServerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  865. permission_required = 'dcim.delete_consoleserverport'
  866. cls = ConsoleServerPort
  867. parent_cls = Device
  868. #
  869. # Power ports
  870. #
  871. class PowerPortAddView(PermissionRequiredMixin, ComponentCreateView):
  872. permission_required = 'dcim.add_powerport'
  873. parent_model = Device
  874. parent_field = 'device'
  875. model = PowerPort
  876. form = forms.PowerPortCreateForm
  877. model_form = forms.PowerPortForm
  878. @permission_required('dcim.change_powerport')
  879. def powerport_connect(request, pk):
  880. powerport = get_object_or_404(PowerPort, pk=pk)
  881. if request.method == 'POST':
  882. form = forms.PowerPortConnectionForm(request.POST, instance=powerport)
  883. if form.is_valid():
  884. powerport = form.save()
  885. msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
  886. powerport.device.get_absolute_url(),
  887. escape(powerport.device),
  888. escape(powerport.name),
  889. powerport.power_outlet.device.get_absolute_url(),
  890. escape(powerport.power_outlet.device),
  891. escape(powerport.power_outlet.name),
  892. )
  893. messages.success(request, mark_safe(msg))
  894. UserAction.objects.log_edit(request.user, powerport, msg)
  895. return redirect('dcim:device', pk=powerport.device.pk)
  896. else:
  897. form = forms.PowerPortConnectionForm(instance=powerport, initial={
  898. 'site': request.GET.get('site'),
  899. 'rack': request.GET.get('rack'),
  900. 'pdu': request.GET.get('pdu'),
  901. 'connection_status': CONNECTION_STATUS_CONNECTED,
  902. })
  903. return render(request, 'dcim/powerport_connect.html', {
  904. 'powerport': powerport,
  905. 'form': form,
  906. 'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
  907. })
  908. @permission_required('dcim.change_powerport')
  909. def powerport_disconnect(request, pk):
  910. powerport = get_object_or_404(PowerPort, pk=pk)
  911. if not powerport.power_outlet:
  912. messages.warning(
  913. request, "Cannot disconnect power port {}: It is not connected to an outlet.".format(powerport)
  914. )
  915. return redirect('dcim:device', pk=powerport.device.pk)
  916. if request.method == 'POST':
  917. form = ConfirmationForm(request.POST)
  918. if form.is_valid():
  919. power_outlet = powerport.power_outlet
  920. powerport.power_outlet = None
  921. powerport.connection_status = None
  922. powerport.save()
  923. msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
  924. powerport.device.get_absolute_url(),
  925. escape(powerport.device),
  926. escape(powerport.name),
  927. power_outlet.device.get_absolute_url(),
  928. escape(power_outlet.device),
  929. escape(power_outlet.name),
  930. )
  931. messages.success(request, mark_safe(msg))
  932. UserAction.objects.log_edit(request.user, powerport, msg)
  933. return redirect('dcim:device', pk=powerport.device.pk)
  934. else:
  935. form = ConfirmationForm()
  936. return render(request, 'dcim/powerport_disconnect.html', {
  937. 'powerport': powerport,
  938. 'form': form,
  939. 'return_url': reverse('dcim:device', kwargs={'pk': powerport.device.pk}),
  940. })
  941. class PowerPortEditView(PermissionRequiredMixin, ComponentEditView):
  942. permission_required = 'dcim.change_powerport'
  943. model = PowerPort
  944. form_class = forms.PowerPortForm
  945. class PowerPortDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  946. permission_required = 'dcim.delete_powerport'
  947. model = PowerPort
  948. class PowerPortBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  949. permission_required = 'dcim.delete_powerport'
  950. cls = PowerPort
  951. parent_cls = Device
  952. class PowerConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
  953. permission_required = 'dcim.change_powerport'
  954. model_form = forms.PowerConnectionCSVForm
  955. table = tables.PowerConnectionTable
  956. default_return_url = 'dcim:power_connections_list'
  957. #
  958. # Power outlets
  959. #
  960. class PowerOutletAddView(PermissionRequiredMixin, ComponentCreateView):
  961. permission_required = 'dcim.add_poweroutlet'
  962. parent_model = Device
  963. parent_field = 'device'
  964. model = PowerOutlet
  965. form = forms.PowerOutletCreateForm
  966. model_form = forms.PowerOutletForm
  967. @permission_required('dcim.change_poweroutlet')
  968. def poweroutlet_connect(request, pk):
  969. poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
  970. if request.method == 'POST':
  971. form = forms.PowerOutletConnectionForm(request.POST)
  972. if form.is_valid():
  973. powerport = form.cleaned_data['port']
  974. powerport.power_outlet = poweroutlet
  975. powerport.connection_status = form.cleaned_data['connection_status']
  976. powerport.save()
  977. msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
  978. powerport.device.get_absolute_url(),
  979. escape(powerport.device),
  980. escape(powerport.name),
  981. poweroutlet.device.get_absolute_url(),
  982. escape(poweroutlet.device),
  983. escape(poweroutlet.name),
  984. )
  985. messages.success(request, mark_safe(msg))
  986. UserAction.objects.log_edit(request.user, powerport, msg)
  987. return redirect('dcim:device', pk=poweroutlet.device.pk)
  988. else:
  989. form = forms.PowerOutletConnectionForm(initial={
  990. 'site': request.GET.get('site'),
  991. 'rack': request.GET.get('rack'),
  992. 'device': request.GET.get('device'),
  993. 'connection_status': CONNECTION_STATUS_CONNECTED,
  994. })
  995. return render(request, 'dcim/poweroutlet_connect.html', {
  996. 'poweroutlet': poweroutlet,
  997. 'form': form,
  998. 'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
  999. })
  1000. @permission_required('dcim.change_poweroutlet')
  1001. def poweroutlet_disconnect(request, pk):
  1002. poweroutlet = get_object_or_404(PowerOutlet, pk=pk)
  1003. if not hasattr(poweroutlet, 'connected_port'):
  1004. messages.warning(
  1005. request, "Cannot disconnect power outlet {}: Nothing is connected to it.".format(poweroutlet)
  1006. )
  1007. return redirect('dcim:device', pk=poweroutlet.device.pk)
  1008. if request.method == 'POST':
  1009. form = ConfirmationForm(request.POST)
  1010. if form.is_valid():
  1011. powerport = poweroutlet.connected_port
  1012. powerport.power_outlet = None
  1013. powerport.connection_status = None
  1014. powerport.save()
  1015. msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
  1016. powerport.device.get_absolute_url(),
  1017. escape(powerport.device),
  1018. escape(powerport.name),
  1019. poweroutlet.device.get_absolute_url(),
  1020. escape(poweroutlet.device),
  1021. escape(poweroutlet.name),
  1022. )
  1023. messages.success(request, mark_safe(msg))
  1024. UserAction.objects.log_edit(request.user, powerport, msg)
  1025. return redirect('dcim:device', pk=poweroutlet.device.pk)
  1026. else:
  1027. form = ConfirmationForm()
  1028. return render(request, 'dcim/poweroutlet_disconnect.html', {
  1029. 'poweroutlet': poweroutlet,
  1030. 'form': form,
  1031. 'return_url': reverse('dcim:device', kwargs={'pk': poweroutlet.device.pk}),
  1032. })
  1033. class PowerOutletEditView(PermissionRequiredMixin, ComponentEditView):
  1034. permission_required = 'dcim.change_poweroutlet'
  1035. model = PowerOutlet
  1036. form_class = forms.PowerOutletForm
  1037. class PowerOutletDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  1038. permission_required = 'dcim.delete_poweroutlet'
  1039. model = PowerOutlet
  1040. class PowerOutletBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  1041. permission_required = 'dcim.delete_poweroutlet'
  1042. cls = PowerOutlet
  1043. parent_cls = Device
  1044. #
  1045. # Interfaces
  1046. #
  1047. class InterfaceAddView(PermissionRequiredMixin, ComponentCreateView):
  1048. permission_required = 'dcim.add_interface'
  1049. parent_model = Device
  1050. parent_field = 'device'
  1051. model = Interface
  1052. form = forms.InterfaceCreateForm
  1053. model_form = forms.InterfaceForm
  1054. class InterfaceEditView(PermissionRequiredMixin, ComponentEditView):
  1055. permission_required = 'dcim.change_interface'
  1056. model = Interface
  1057. form_class = forms.InterfaceForm
  1058. class InterfaceDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  1059. permission_required = 'dcim.delete_interface'
  1060. model = Interface
  1061. class InterfaceBulkEditView(PermissionRequiredMixin, BulkEditView):
  1062. permission_required = 'dcim.change_interface'
  1063. cls = Interface
  1064. parent_cls = Device
  1065. form = forms.InterfaceBulkEditForm
  1066. template_name = 'dcim/interface_bulk_edit.html'
  1067. class InterfaceBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  1068. permission_required = 'dcim.delete_interface'
  1069. cls = Interface
  1070. parent_cls = Device
  1071. #
  1072. # Device bays
  1073. #
  1074. class DeviceBayAddView(PermissionRequiredMixin, ComponentCreateView):
  1075. permission_required = 'dcim.add_devicebay'
  1076. parent_model = Device
  1077. parent_field = 'device'
  1078. model = DeviceBay
  1079. form = forms.DeviceBayCreateForm
  1080. model_form = forms.DeviceBayForm
  1081. class DeviceBayEditView(PermissionRequiredMixin, ComponentEditView):
  1082. permission_required = 'dcim.change_devicebay'
  1083. model = DeviceBay
  1084. form_class = forms.DeviceBayForm
  1085. class DeviceBayDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  1086. permission_required = 'dcim.delete_devicebay'
  1087. model = DeviceBay
  1088. @permission_required('dcim.change_devicebay')
  1089. def devicebay_populate(request, pk):
  1090. device_bay = get_object_or_404(DeviceBay, pk=pk)
  1091. if request.method == 'POST':
  1092. form = forms.PopulateDeviceBayForm(device_bay, request.POST)
  1093. if form.is_valid():
  1094. device_bay.installed_device = form.cleaned_data['installed_device']
  1095. device_bay.save()
  1096. if not form.errors:
  1097. messages.success(request, "Added {} to {}.".format(device_bay.installed_device, device_bay))
  1098. return redirect('dcim:device', pk=device_bay.device.pk)
  1099. else:
  1100. form = forms.PopulateDeviceBayForm(device_bay)
  1101. return render(request, 'dcim/devicebay_populate.html', {
  1102. 'device_bay': device_bay,
  1103. 'form': form,
  1104. 'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
  1105. })
  1106. @permission_required('dcim.change_devicebay')
  1107. def devicebay_depopulate(request, pk):
  1108. device_bay = get_object_or_404(DeviceBay, pk=pk)
  1109. if request.method == 'POST':
  1110. form = ConfirmationForm(request.POST)
  1111. if form.is_valid():
  1112. removed_device = device_bay.installed_device
  1113. device_bay.installed_device = None
  1114. device_bay.save()
  1115. messages.success(request, "{} has been removed from {}.".format(removed_device, device_bay))
  1116. return redirect('dcim:device', pk=device_bay.device.pk)
  1117. else:
  1118. form = ConfirmationForm()
  1119. return render(request, 'dcim/devicebay_depopulate.html', {
  1120. 'device_bay': device_bay,
  1121. 'form': form,
  1122. 'return_url': reverse('dcim:device', kwargs={'pk': device_bay.device.pk}),
  1123. })
  1124. class DeviceBayBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
  1125. permission_required = 'dcim.delete_devicebay'
  1126. cls = DeviceBay
  1127. parent_cls = Device
  1128. #
  1129. # Bulk device component creation
  1130. #
  1131. class DeviceBulkAddComponentView(View):
  1132. """
  1133. Add one or more components (e.g. interfaces) to a selected set of Devices.
  1134. """
  1135. form = forms.DeviceBulkAddComponentForm
  1136. model = None
  1137. model_form = None
  1138. def get(self):
  1139. return redirect('dcim:device_list')
  1140. def post(self, request):
  1141. # Are we editing *all* objects in the queryset or just a selected subset?
  1142. if request.POST.get('_all'):
  1143. pk_list = [obj.pk for obj in filters.DeviceFilter(request.GET, Device.objects.all())]
  1144. else:
  1145. pk_list = [int(pk) for pk in request.POST.getlist('pk')]
  1146. if '_create' in request.POST:
  1147. form = self.form(request.POST)
  1148. if form.is_valid():
  1149. new_components = []
  1150. data = deepcopy(form.cleaned_data)
  1151. for device in data['pk']:
  1152. names = data['name_pattern']
  1153. for name in names:
  1154. component_data = {
  1155. 'device': device.pk,
  1156. 'name': name,
  1157. }
  1158. component_data.update(data)
  1159. component_form = self.model_form(component_data)
  1160. if component_form.is_valid():
  1161. new_components.append(component_form.save(commit=False))
  1162. else:
  1163. for field, errors in component_form.errors.as_data().items():
  1164. for e in errors:
  1165. form.add_error(field, '{} {}: {}'.format(device, name, ', '.join(e)))
  1166. if not form.errors:
  1167. self.model.objects.bulk_create(new_components)
  1168. messages.success(request, "Added {} {} to {} devices.".format(
  1169. len(new_components), self.model._meta.verbose_name_plural, len(form.cleaned_data['pk'])
  1170. ))
  1171. return redirect('dcim:device_list')
  1172. else:
  1173. form = self.form(initial={'pk': pk_list})
  1174. selected_devices = Device.objects.filter(pk__in=pk_list)
  1175. if not selected_devices:
  1176. messages.warning(request, "No devices were selected.")
  1177. return redirect('dcim:device_list')
  1178. return render(request, 'dcim/device_bulk_add_component.html', {
  1179. 'form': form,
  1180. 'component_name': self.model._meta.verbose_name_plural,
  1181. 'selected_devices': selected_devices,
  1182. 'return_url': reverse('dcim:device_list'),
  1183. })
  1184. class DeviceBulkAddConsolePortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1185. permission_required = 'dcim.add_consoleport'
  1186. model = ConsolePort
  1187. model_form = forms.ConsolePortForm
  1188. class DeviceBulkAddConsoleServerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1189. permission_required = 'dcim.add_consoleserverport'
  1190. model = ConsoleServerPort
  1191. model_form = forms.ConsoleServerPortForm
  1192. class DeviceBulkAddPowerPortView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1193. permission_required = 'dcim.add_powerport'
  1194. model = PowerPort
  1195. model_form = forms.PowerPortForm
  1196. class DeviceBulkAddPowerOutletView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1197. permission_required = 'dcim.add_poweroutlet'
  1198. model = PowerOutlet
  1199. model_form = forms.PowerOutletForm
  1200. class DeviceBulkAddInterfaceView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1201. permission_required = 'dcim.add_interface'
  1202. form = forms.DeviceBulkAddInterfaceForm
  1203. model = Interface
  1204. model_form = forms.InterfaceForm
  1205. class DeviceBulkAddDeviceBayView(PermissionRequiredMixin, DeviceBulkAddComponentView):
  1206. permission_required = 'dcim.add_devicebay'
  1207. model = DeviceBay
  1208. model_form = forms.DeviceBayForm
  1209. #
  1210. # Interface connections
  1211. #
  1212. @permission_required('dcim.add_interfaceconnection')
  1213. def interfaceconnection_add(request, pk):
  1214. device = get_object_or_404(Device, pk=pk)
  1215. if request.method == 'POST':
  1216. form = forms.InterfaceConnectionForm(device, request.POST)
  1217. if form.is_valid():
  1218. interfaceconnection = form.save()
  1219. msg = 'Connected <a href="{}">{}</a> {} to <a href="{}">{}</a> {}'.format(
  1220. interfaceconnection.interface_a.device.get_absolute_url(),
  1221. escape(interfaceconnection.interface_a.device),
  1222. escape(interfaceconnection.interface_a.name),
  1223. interfaceconnection.interface_b.device.get_absolute_url(),
  1224. escape(interfaceconnection.interface_b.device),
  1225. escape(interfaceconnection.interface_b.name),
  1226. )
  1227. messages.success(request, mark_safe(msg))
  1228. UserAction.objects.log_edit(request.user, interfaceconnection, msg)
  1229. if '_addanother' in request.POST:
  1230. base_url = reverse('dcim:interfaceconnection_add', kwargs={'pk': device.pk})
  1231. device_b = interfaceconnection.interface_b.device
  1232. params = urlencode({
  1233. 'rack_b': device_b.rack.pk if device_b.rack else '',
  1234. 'device_b': device_b.pk,
  1235. })
  1236. return HttpResponseRedirect('{}?{}'.format(base_url, params))
  1237. else:
  1238. return redirect('dcim:device', pk=device.pk)
  1239. else:
  1240. form = forms.InterfaceConnectionForm(device, initial={
  1241. 'interface_a': request.GET.get('interface_a'),
  1242. 'site_b': request.GET.get('site_b'),
  1243. 'rack_b': request.GET.get('rack_b'),
  1244. 'device_b': request.GET.get('device_b'),
  1245. 'interface_b': request.GET.get('interface_b'),
  1246. })
  1247. return render(request, 'dcim/interfaceconnection_edit.html', {
  1248. 'device': device,
  1249. 'form': form,
  1250. 'return_url': reverse('dcim:device', kwargs={'pk': device.pk}),
  1251. })
  1252. @permission_required('dcim.delete_interfaceconnection')
  1253. def interfaceconnection_delete(request, pk):
  1254. interfaceconnection = get_object_or_404(InterfaceConnection, pk=pk)
  1255. device_id = request.GET.get('device', None)
  1256. if request.method == 'POST':
  1257. form = forms.InterfaceConnectionDeletionForm(request.POST)
  1258. if form.is_valid():
  1259. interfaceconnection.delete()
  1260. msg = 'Disconnected <a href="{}">{}</a> {} from <a href="{}">{}</a> {}'.format(
  1261. interfaceconnection.interface_a.device.get_absolute_url(),
  1262. escape(interfaceconnection.interface_a.device),
  1263. escape(interfaceconnection.interface_a.name),
  1264. interfaceconnection.interface_b.device.get_absolute_url(),
  1265. escape(interfaceconnection.interface_b.device),
  1266. escape(interfaceconnection.interface_b.name),
  1267. )
  1268. messages.success(request, mark_safe(msg))
  1269. UserAction.objects.log_edit(request.user, interfaceconnection, msg)
  1270. if form.cleaned_data['device']:
  1271. return redirect('dcim:device', pk=form.cleaned_data['device'].pk)
  1272. else:
  1273. return redirect('dcim:device_list')
  1274. else:
  1275. form = forms.InterfaceConnectionDeletionForm(initial={
  1276. 'device': device_id,
  1277. })
  1278. # Determine where to direct user upon cancellation
  1279. if device_id:
  1280. return_url = reverse('dcim:device', kwargs={'pk': device_id})
  1281. else:
  1282. return_url = reverse('dcim:device_list')
  1283. return render(request, 'dcim/interfaceconnection_delete.html', {
  1284. 'interfaceconnection': interfaceconnection,
  1285. 'device_id': device_id,
  1286. 'form': form,
  1287. 'return_url': return_url,
  1288. })
  1289. class InterfaceConnectionsBulkImportView(PermissionRequiredMixin, BulkImportView):
  1290. permission_required = 'dcim.change_interface'
  1291. model_form = forms.InterfaceConnectionCSVForm
  1292. table = tables.InterfaceConnectionTable
  1293. default_return_url = 'dcim:interface_connections_list'
  1294. #
  1295. # Connections
  1296. #
  1297. class ConsoleConnectionsListView(ObjectListView):
  1298. queryset = ConsolePort.objects.select_related('device', 'cs_port__device').filter(cs_port__isnull=False)\
  1299. .order_by('cs_port__device__name', 'cs_port__name')
  1300. filter = filters.ConsoleConnectionFilter
  1301. filter_form = forms.ConsoleConnectionFilterForm
  1302. table = tables.ConsoleConnectionTable
  1303. template_name = 'dcim/console_connections_list.html'
  1304. class PowerConnectionsListView(ObjectListView):
  1305. queryset = PowerPort.objects.select_related('device', 'power_outlet__device').filter(power_outlet__isnull=False)\
  1306. .order_by('power_outlet__device__name', 'power_outlet__name')
  1307. filter = filters.PowerConnectionFilter
  1308. filter_form = forms.PowerConnectionFilterForm
  1309. table = tables.PowerConnectionTable
  1310. template_name = 'dcim/power_connections_list.html'
  1311. class InterfaceConnectionsListView(ObjectListView):
  1312. queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device')\
  1313. .order_by('interface_a__device__name', 'interface_a__name')
  1314. filter = filters.InterfaceConnectionFilter
  1315. filter_form = forms.InterfaceConnectionFilterForm
  1316. table = tables.InterfaceConnectionTable
  1317. template_name = 'dcim/interface_connections_list.html'
  1318. #
  1319. # Inventory items
  1320. #
  1321. class InventoryItemEditView(PermissionRequiredMixin, ComponentEditView):
  1322. permission_required = 'dcim.change_inventoryitem'
  1323. model = InventoryItem
  1324. form_class = forms.InventoryItemForm
  1325. def alter_obj(self, obj, request, url_args, url_kwargs):
  1326. if 'device' in url_kwargs:
  1327. obj.device = get_object_or_404(Device, pk=url_kwargs['device'])
  1328. return obj
  1329. class InventoryItemDeleteView(PermissionRequiredMixin, ComponentDeleteView):
  1330. permission_required = 'dcim.delete_inventoryitem'
  1331. model = InventoryItem