views.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697
  1. import socket
  2. from collections import OrderedDict
  3. from django.conf import settings
  4. from django.db.models import F
  5. from django.http import HttpResponseForbidden, HttpResponse
  6. from django.shortcuts import get_object_or_404
  7. from drf_yasg import openapi
  8. from drf_yasg.openapi import Parameter
  9. from drf_yasg.utils import swagger_auto_schema
  10. from rest_framework.decorators import action
  11. from rest_framework.mixins import ListModelMixin
  12. from rest_framework.response import Response
  13. from rest_framework.routers import APIRootView
  14. from rest_framework.viewsets import GenericViewSet, ViewSet
  15. from circuits.models import Circuit
  16. from dcim import filters
  17. from dcim.models import (
  18. Cable, CablePath, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay,
  19. DeviceBayTemplate, DeviceRole, DeviceType, FrontPort, FrontPortTemplate, Interface, InterfaceTemplate,
  20. Manufacturer, InventoryItem, Platform, PowerFeed, PowerOutlet, PowerOutletTemplate, PowerPanel, PowerPort,
  21. PowerPortTemplate, Rack, RackGroup, RackReservation, RackRole, RearPort, RearPortTemplate, Region, Site,
  22. VirtualChassis,
  23. )
  24. from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
  25. from ipam.models import Prefix, VLAN
  26. from netbox.api.views import ModelViewSet
  27. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  28. from netbox.api.exceptions import ServiceUnavailable
  29. from netbox.api.metadata import ContentTypeMetadata
  30. from utilities.api import get_serializer_for_model
  31. from utilities.utils import count_related
  32. from virtualization.models import VirtualMachine
  33. from . import serializers
  34. from .exceptions import MissingFilterException
  35. class DCIMRootView(APIRootView):
  36. """
  37. DCIM API root view
  38. """
  39. def get_view_name(self):
  40. return 'DCIM'
  41. # Mixins
  42. class PathEndpointMixin(object):
  43. @action(detail=True, url_path='trace')
  44. def trace(self, request, pk):
  45. """
  46. Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
  47. """
  48. obj = get_object_or_404(self.queryset, pk=pk)
  49. # Initialize the path array
  50. path = []
  51. for near_end, cable, far_end in obj.trace():
  52. if near_end is None:
  53. # Split paths
  54. break
  55. # Serialize each object
  56. serializer_a = get_serializer_for_model(near_end, prefix='Nested')
  57. x = serializer_a(near_end, context={'request': request}).data
  58. if cable is not None:
  59. y = serializers.TracedCableSerializer(cable, context={'request': request}).data
  60. else:
  61. y = None
  62. if far_end is not None:
  63. serializer_b = get_serializer_for_model(far_end, prefix='Nested')
  64. z = serializer_b(far_end, context={'request': request}).data
  65. else:
  66. z = None
  67. path.append((x, y, z))
  68. return Response(path)
  69. class PassThroughPortMixin(object):
  70. @action(detail=True, url_path='paths')
  71. def paths(self, request, pk):
  72. """
  73. Return all CablePaths which traverse a given pass-through port.
  74. """
  75. obj = get_object_or_404(self.queryset, pk=pk)
  76. cablepaths = CablePath.objects.filter(path__contains=obj).prefetch_related('origin', 'destination')
  77. serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
  78. return Response(serializer.data)
  79. #
  80. # Regions
  81. #
  82. class RegionViewSet(ModelViewSet):
  83. queryset = Region.objects.add_related_count(
  84. Region.objects.all(),
  85. Site,
  86. 'region',
  87. 'site_count',
  88. cumulative=True
  89. )
  90. serializer_class = serializers.RegionSerializer
  91. filterset_class = filters.RegionFilterSet
  92. #
  93. # Sites
  94. #
  95. class SiteViewSet(CustomFieldModelViewSet):
  96. queryset = Site.objects.prefetch_related(
  97. 'region', 'tenant', 'tags'
  98. ).annotate(
  99. device_count=count_related(Device, 'site'),
  100. rack_count=count_related(Rack, 'site'),
  101. prefix_count=count_related(Prefix, 'site'),
  102. vlan_count=count_related(VLAN, 'site'),
  103. circuit_count=count_related(Circuit, 'terminations__site'),
  104. virtualmachine_count=count_related(VirtualMachine, 'cluster__site')
  105. )
  106. serializer_class = serializers.SiteSerializer
  107. filterset_class = filters.SiteFilterSet
  108. #
  109. # Rack groups
  110. #
  111. class RackGroupViewSet(ModelViewSet):
  112. queryset = RackGroup.objects.add_related_count(
  113. RackGroup.objects.all(),
  114. Rack,
  115. 'group',
  116. 'rack_count',
  117. cumulative=True
  118. ).prefetch_related('site')
  119. serializer_class = serializers.RackGroupSerializer
  120. filterset_class = filters.RackGroupFilterSet
  121. #
  122. # Rack roles
  123. #
  124. class RackRoleViewSet(ModelViewSet):
  125. queryset = RackRole.objects.annotate(
  126. rack_count=count_related(Rack, 'role')
  127. )
  128. serializer_class = serializers.RackRoleSerializer
  129. filterset_class = filters.RackRoleFilterSet
  130. #
  131. # Racks
  132. #
  133. class RackViewSet(CustomFieldModelViewSet):
  134. queryset = Rack.objects.prefetch_related(
  135. 'site', 'group__site', 'role', 'tenant', 'tags'
  136. ).annotate(
  137. device_count=count_related(Device, 'rack'),
  138. powerfeed_count=count_related(PowerFeed, 'rack')
  139. )
  140. serializer_class = serializers.RackSerializer
  141. filterset_class = filters.RackFilterSet
  142. @swagger_auto_schema(
  143. responses={200: serializers.RackUnitSerializer(many=True)},
  144. query_serializer=serializers.RackElevationDetailFilterSerializer
  145. )
  146. @action(detail=True)
  147. def elevation(self, request, pk=None):
  148. """
  149. Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
  150. """
  151. rack = get_object_or_404(self.queryset, pk=pk)
  152. serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
  153. if not serializer.is_valid():
  154. return Response(serializer.errors, 400)
  155. data = serializer.validated_data
  156. if data['render'] == 'svg':
  157. # Render and return the elevation as an SVG drawing with the correct content type
  158. drawing = rack.get_elevation_svg(
  159. face=data['face'],
  160. user=request.user,
  161. unit_width=data['unit_width'],
  162. unit_height=data['unit_height'],
  163. legend_width=data['legend_width'],
  164. include_images=data['include_images'],
  165. base_url=request.build_absolute_uri('/')
  166. )
  167. return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
  168. else:
  169. # Return a JSON representation of the rack units in the elevation
  170. elevation = rack.get_rack_units(
  171. face=data['face'],
  172. user=request.user,
  173. exclude=data['exclude'],
  174. expand_devices=data['expand_devices']
  175. )
  176. # Enable filtering rack units by ID
  177. q = data['q']
  178. if q:
  179. elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name'])]
  180. page = self.paginate_queryset(elevation)
  181. if page is not None:
  182. rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
  183. return self.get_paginated_response(rack_units.data)
  184. #
  185. # Rack reservations
  186. #
  187. class RackReservationViewSet(ModelViewSet):
  188. queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
  189. serializer_class = serializers.RackReservationSerializer
  190. filterset_class = filters.RackReservationFilterSet
  191. # Assign user from request
  192. def perform_create(self, serializer):
  193. serializer.save(user=self.request.user)
  194. #
  195. # Manufacturers
  196. #
  197. class ManufacturerViewSet(ModelViewSet):
  198. queryset = Manufacturer.objects.annotate(
  199. devicetype_count=count_related(DeviceType, 'manufacturer'),
  200. inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
  201. platform_count=count_related(Platform, 'manufacturer')
  202. )
  203. serializer_class = serializers.ManufacturerSerializer
  204. filterset_class = filters.ManufacturerFilterSet
  205. #
  206. # Device types
  207. #
  208. class DeviceTypeViewSet(CustomFieldModelViewSet):
  209. queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
  210. device_count=count_related(Device, 'device_type')
  211. )
  212. serializer_class = serializers.DeviceTypeSerializer
  213. filterset_class = filters.DeviceTypeFilterSet
  214. brief_prefetch_fields = ['manufacturer']
  215. #
  216. # Device type components
  217. #
  218. class ConsolePortTemplateViewSet(ModelViewSet):
  219. queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
  220. serializer_class = serializers.ConsolePortTemplateSerializer
  221. filterset_class = filters.ConsolePortTemplateFilterSet
  222. class ConsoleServerPortTemplateViewSet(ModelViewSet):
  223. queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
  224. serializer_class = serializers.ConsoleServerPortTemplateSerializer
  225. filterset_class = filters.ConsoleServerPortTemplateFilterSet
  226. class PowerPortTemplateViewSet(ModelViewSet):
  227. queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
  228. serializer_class = serializers.PowerPortTemplateSerializer
  229. filterset_class = filters.PowerPortTemplateFilterSet
  230. class PowerOutletTemplateViewSet(ModelViewSet):
  231. queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
  232. serializer_class = serializers.PowerOutletTemplateSerializer
  233. filterset_class = filters.PowerOutletTemplateFilterSet
  234. class InterfaceTemplateViewSet(ModelViewSet):
  235. queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
  236. serializer_class = serializers.InterfaceTemplateSerializer
  237. filterset_class = filters.InterfaceTemplateFilterSet
  238. class FrontPortTemplateViewSet(ModelViewSet):
  239. queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
  240. serializer_class = serializers.FrontPortTemplateSerializer
  241. filterset_class = filters.FrontPortTemplateFilterSet
  242. class RearPortTemplateViewSet(ModelViewSet):
  243. queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
  244. serializer_class = serializers.RearPortTemplateSerializer
  245. filterset_class = filters.RearPortTemplateFilterSet
  246. class DeviceBayTemplateViewSet(ModelViewSet):
  247. queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
  248. serializer_class = serializers.DeviceBayTemplateSerializer
  249. filterset_class = filters.DeviceBayTemplateFilterSet
  250. #
  251. # Device roles
  252. #
  253. class DeviceRoleViewSet(ModelViewSet):
  254. queryset = DeviceRole.objects.annotate(
  255. device_count=count_related(Device, 'device_role'),
  256. virtualmachine_count=count_related(VirtualMachine, 'role')
  257. )
  258. serializer_class = serializers.DeviceRoleSerializer
  259. filterset_class = filters.DeviceRoleFilterSet
  260. #
  261. # Platforms
  262. #
  263. class PlatformViewSet(ModelViewSet):
  264. queryset = Platform.objects.annotate(
  265. device_count=count_related(Device, 'platform'),
  266. virtualmachine_count=count_related(VirtualMachine, 'platform')
  267. )
  268. serializer_class = serializers.PlatformSerializer
  269. filterset_class = filters.PlatformFilterSet
  270. #
  271. # Devices
  272. #
  273. class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
  274. queryset = Device.objects.prefetch_related(
  275. 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay',
  276. 'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
  277. )
  278. filterset_class = filters.DeviceFilterSet
  279. def get_serializer_class(self):
  280. """
  281. Select the specific serializer based on the request context.
  282. If the `brief` query param equates to True, return the NestedDeviceSerializer
  283. If the `exclude` query param includes `config_context` as a value, return the DeviceSerializer
  284. Else, return the DeviceWithConfigContextSerializer
  285. """
  286. request = self.get_serializer_context()['request']
  287. if request.query_params.get('brief', False):
  288. return serializers.NestedDeviceSerializer
  289. elif 'config_context' in request.query_params.get('exclude', []):
  290. return serializers.DeviceSerializer
  291. return serializers.DeviceWithConfigContextSerializer
  292. @swagger_auto_schema(
  293. manual_parameters=[
  294. Parameter(
  295. name='method',
  296. in_='query',
  297. required=True,
  298. type=openapi.TYPE_STRING
  299. )
  300. ],
  301. responses={'200': serializers.DeviceNAPALMSerializer}
  302. )
  303. @action(detail=True, url_path='napalm')
  304. def napalm(self, request, pk):
  305. """
  306. Execute a NAPALM method on a Device
  307. """
  308. device = get_object_or_404(self.queryset, pk=pk)
  309. if not device.primary_ip:
  310. raise ServiceUnavailable("This device does not have a primary IP address configured.")
  311. if device.platform is None:
  312. raise ServiceUnavailable("No platform is configured for this device.")
  313. if not device.platform.napalm_driver:
  314. raise ServiceUnavailable(f"No NAPALM driver is configured for this device's platform: {device.platform}.")
  315. # Check for primary IP address from NetBox object
  316. if device.primary_ip:
  317. host = str(device.primary_ip.address.ip)
  318. else:
  319. # Raise exception for no IP address and no Name if device.name does not exist
  320. if not device.name:
  321. raise ServiceUnavailable(
  322. "This device does not have a primary IP address or device name to lookup configured."
  323. )
  324. try:
  325. # Attempt to complete a DNS name resolution if no primary_ip is set
  326. host = socket.gethostbyname(device.name)
  327. except socket.gaierror:
  328. # Name lookup failure
  329. raise ServiceUnavailable(
  330. f"Name lookup failure, unable to resolve IP address for {device.name}. Please set Primary IP or "
  331. f"setup name resolution.")
  332. # Check that NAPALM is installed
  333. try:
  334. import napalm
  335. from napalm.base.exceptions import ModuleImportError
  336. except ModuleNotFoundError as e:
  337. if getattr(e, 'name') == 'napalm':
  338. raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
  339. raise e
  340. # Validate the configured driver
  341. try:
  342. driver = napalm.get_network_driver(device.platform.napalm_driver)
  343. except ModuleImportError:
  344. raise ServiceUnavailable("NAPALM driver for platform {} not found: {}.".format(
  345. device.platform, device.platform.napalm_driver
  346. ))
  347. # Verify user permission
  348. if not request.user.has_perm('dcim.napalm_read_device'):
  349. return HttpResponseForbidden()
  350. napalm_methods = request.GET.getlist('method')
  351. response = OrderedDict([(m, None) for m in napalm_methods])
  352. username = settings.NAPALM_USERNAME
  353. password = settings.NAPALM_PASSWORD
  354. optional_args = settings.NAPALM_ARGS.copy()
  355. if device.platform.napalm_args is not None:
  356. optional_args.update(device.platform.napalm_args)
  357. # Update NAPALM parameters according to the request headers
  358. for header in request.headers:
  359. if header[:9].lower() != 'x-napalm-':
  360. continue
  361. key = header[9:]
  362. if key.lower() == 'username':
  363. username = request.headers[header]
  364. elif key.lower() == 'password':
  365. password = request.headers[header]
  366. elif key:
  367. optional_args[key.lower()] = request.headers[header]
  368. # Connect to the device
  369. d = driver(
  370. hostname=host,
  371. username=username,
  372. password=password,
  373. timeout=settings.NAPALM_TIMEOUT,
  374. optional_args=optional_args
  375. )
  376. try:
  377. d.open()
  378. except Exception as e:
  379. raise ServiceUnavailable("Error connecting to the device at {}: {}".format(host, e))
  380. # Validate and execute each specified NAPALM method
  381. for method in napalm_methods:
  382. if not hasattr(driver, method):
  383. response[method] = {'error': 'Unknown NAPALM method'}
  384. continue
  385. if not method.startswith('get_'):
  386. response[method] = {'error': 'Only get_* NAPALM methods are supported'}
  387. continue
  388. try:
  389. response[method] = getattr(d, method)()
  390. except NotImplementedError:
  391. response[method] = {'error': 'Method {} not implemented for NAPALM driver {}'.format(method, driver)}
  392. except Exception as e:
  393. response[method] = {'error': 'Method {} failed: {}'.format(method, e)}
  394. d.close()
  395. return Response(response)
  396. #
  397. # Device components
  398. #
  399. class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
  400. queryset = ConsolePort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
  401. serializer_class = serializers.ConsolePortSerializer
  402. filterset_class = filters.ConsolePortFilterSet
  403. brief_prefetch_fields = ['device']
  404. class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
  405. queryset = ConsoleServerPort.objects.prefetch_related(
  406. 'device', '_path__destination', 'cable', '_cable_peer', 'tags'
  407. )
  408. serializer_class = serializers.ConsoleServerPortSerializer
  409. filterset_class = filters.ConsoleServerPortFilterSet
  410. brief_prefetch_fields = ['device']
  411. class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
  412. queryset = PowerPort.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
  413. serializer_class = serializers.PowerPortSerializer
  414. filterset_class = filters.PowerPortFilterSet
  415. brief_prefetch_fields = ['device']
  416. class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
  417. queryset = PowerOutlet.objects.prefetch_related('device', '_path__destination', 'cable', '_cable_peer', 'tags')
  418. serializer_class = serializers.PowerOutletSerializer
  419. filterset_class = filters.PowerOutletFilterSet
  420. brief_prefetch_fields = ['device']
  421. class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
  422. queryset = Interface.objects.prefetch_related(
  423. 'device', '_path__destination', 'cable', '_cable_peer', 'ip_addresses', 'tags'
  424. )
  425. serializer_class = serializers.InterfaceSerializer
  426. filterset_class = filters.InterfaceFilterSet
  427. brief_prefetch_fields = ['device']
  428. class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
  429. queryset = FrontPort.objects.prefetch_related('device__device_type__manufacturer', 'rear_port', 'cable', 'tags')
  430. serializer_class = serializers.FrontPortSerializer
  431. filterset_class = filters.FrontPortFilterSet
  432. brief_prefetch_fields = ['device']
  433. class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
  434. queryset = RearPort.objects.prefetch_related('device__device_type__manufacturer', 'cable', 'tags')
  435. serializer_class = serializers.RearPortSerializer
  436. filterset_class = filters.RearPortFilterSet
  437. brief_prefetch_fields = ['device']
  438. class DeviceBayViewSet(ModelViewSet):
  439. queryset = DeviceBay.objects.prefetch_related('installed_device').prefetch_related('tags')
  440. serializer_class = serializers.DeviceBaySerializer
  441. filterset_class = filters.DeviceBayFilterSet
  442. brief_prefetch_fields = ['device']
  443. class InventoryItemViewSet(ModelViewSet):
  444. queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer').prefetch_related('tags')
  445. serializer_class = serializers.InventoryItemSerializer
  446. filterset_class = filters.InventoryItemFilterSet
  447. brief_prefetch_fields = ['device']
  448. #
  449. # Connections
  450. #
  451. class ConsoleConnectionViewSet(ListModelMixin, GenericViewSet):
  452. queryset = ConsolePort.objects.prefetch_related('device', '_path').filter(
  453. _path__destination_id__isnull=False
  454. )
  455. serializer_class = serializers.ConsolePortSerializer
  456. filterset_class = filters.ConsoleConnectionFilterSet
  457. class PowerConnectionViewSet(ListModelMixin, GenericViewSet):
  458. queryset = PowerPort.objects.prefetch_related('device', '_path').filter(
  459. _path__destination_id__isnull=False
  460. )
  461. serializer_class = serializers.PowerPortSerializer
  462. filterset_class = filters.PowerConnectionFilterSet
  463. class InterfaceConnectionViewSet(ListModelMixin, GenericViewSet):
  464. queryset = Interface.objects.prefetch_related('device', '_path').filter(
  465. # Avoid duplicate connections by only selecting the lower PK in a connected pair
  466. _path__destination_id__isnull=False,
  467. pk__lt=F('_path__destination_id')
  468. )
  469. serializer_class = serializers.InterfaceConnectionSerializer
  470. filterset_class = filters.InterfaceConnectionFilterSet
  471. #
  472. # Cables
  473. #
  474. class CableViewSet(ModelViewSet):
  475. metadata_class = ContentTypeMetadata
  476. queryset = Cable.objects.prefetch_related(
  477. 'termination_a', 'termination_b'
  478. )
  479. serializer_class = serializers.CableSerializer
  480. filterset_class = filters.CableFilterSet
  481. #
  482. # Virtual chassis
  483. #
  484. class VirtualChassisViewSet(ModelViewSet):
  485. queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
  486. member_count=count_related(Device, 'virtual_chassis')
  487. )
  488. serializer_class = serializers.VirtualChassisSerializer
  489. filterset_class = filters.VirtualChassisFilterSet
  490. brief_prefetch_fields = ['master']
  491. #
  492. # Power panels
  493. #
  494. class PowerPanelViewSet(ModelViewSet):
  495. queryset = PowerPanel.objects.prefetch_related(
  496. 'site', 'rack_group'
  497. ).annotate(
  498. powerfeed_count=count_related(PowerFeed, 'power_panel')
  499. )
  500. serializer_class = serializers.PowerPanelSerializer
  501. filterset_class = filters.PowerPanelFilterSet
  502. #
  503. # Power feeds
  504. #
  505. class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
  506. queryset = PowerFeed.objects.prefetch_related(
  507. 'power_panel', 'rack', '_path__destination', 'cable', '_cable_peer', 'tags'
  508. )
  509. serializer_class = serializers.PowerFeedSerializer
  510. filterset_class = filters.PowerFeedFilterSet
  511. #
  512. # Miscellaneous
  513. #
  514. class ConnectedDeviceViewSet(ViewSet):
  515. """
  516. This endpoint allows a user to determine what device (if any) is connected to a given peer device and peer
  517. interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
  518. via a protocol such as LLDP. Two query parameters must be included in the request:
  519. * `peer_device`: The name of the peer device
  520. * `peer_interface`: The name of the peer interface
  521. """
  522. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  523. _device_param = Parameter(
  524. name='peer_device',
  525. in_='query',
  526. description='The name of the peer device',
  527. required=True,
  528. type=openapi.TYPE_STRING
  529. )
  530. _interface_param = Parameter(
  531. name='peer_interface',
  532. in_='query',
  533. description='The name of the peer interface',
  534. required=True,
  535. type=openapi.TYPE_STRING
  536. )
  537. def get_view_name(self):
  538. return "Connected Device Locator"
  539. @swagger_auto_schema(
  540. manual_parameters=[_device_param, _interface_param],
  541. responses={'200': serializers.DeviceSerializer}
  542. )
  543. def list(self, request):
  544. peer_device_name = request.query_params.get(self._device_param.name)
  545. peer_interface_name = request.query_params.get(self._interface_param.name)
  546. if not peer_device_name or not peer_interface_name:
  547. raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
  548. # Determine local interface from peer interface's connection
  549. peer_interface = get_object_or_404(
  550. Interface.objects.all(),
  551. device__name=peer_device_name,
  552. name=peer_interface_name
  553. )
  554. local_interface = peer_interface.connected_endpoint
  555. if local_interface is None:
  556. return Response()
  557. return Response(serializers.DeviceSerializer(local_interface.device, context={'request': request}).data)