views.py 24 KB

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