views.py 56 KB

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