views.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. from django.contrib.contenttypes.prefetch import GenericPrefetch
  2. from django.http import Http404, HttpResponse
  3. from django.shortcuts import get_object_or_404
  4. from drf_spectacular.types import OpenApiTypes
  5. from drf_spectacular.utils import extend_schema, OpenApiParameter
  6. from rest_framework.decorators import action
  7. from rest_framework.response import Response
  8. from rest_framework.routers import APIRootView
  9. from rest_framework.viewsets import ViewSet
  10. from dcim import filtersets
  11. from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH
  12. from dcim.models import *
  13. from dcim.svg import CableTraceSVG
  14. from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
  15. from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
  16. from netbox.api.metadata import ContentTypeMetadata
  17. from netbox.api.pagination import StripCountAnnotationsPaginator
  18. from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin
  19. from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin
  20. from utilities.api import get_serializer_for_model
  21. from utilities.query_functions import CollateAsChar
  22. from virtualization.models import VirtualMachine
  23. from . import serializers
  24. from .exceptions import MissingFilterException
  25. class DCIMRootView(APIRootView):
  26. """
  27. DCIM API root view
  28. """
  29. def get_view_name(self):
  30. return 'DCIM'
  31. # Mixins
  32. class PathEndpointMixin(object):
  33. @action(detail=True, url_path='trace')
  34. def trace(self, request, pk):
  35. """
  36. Trace a complete cable path and return each segment as a three-tuple of (termination, cable, termination).
  37. """
  38. obj = get_object_or_404(self.queryset, pk=pk)
  39. # Initialize the path array
  40. path = []
  41. # Render SVG image if requested
  42. if request.GET.get('render', None) == 'svg':
  43. try:
  44. width = int(request.GET.get('width', CABLE_TRACE_SVG_DEFAULT_WIDTH))
  45. except (ValueError, TypeError):
  46. width = CABLE_TRACE_SVG_DEFAULT_WIDTH
  47. drawing = CableTraceSVG(obj, base_url=request.build_absolute_uri('/'), width=width)
  48. return HttpResponse(drawing.render().tostring(), content_type='image/svg+xml')
  49. # Serialize path objects, iterating over each three-tuple in the path
  50. for near_ends, cable, far_ends in obj.trace():
  51. if near_ends:
  52. serializer_a = get_serializer_for_model(near_ends[0])
  53. near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data
  54. else:
  55. # Path is split; stop here
  56. break
  57. if cable:
  58. cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data
  59. if far_ends:
  60. serializer_b = get_serializer_for_model(far_ends[0])
  61. far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data
  62. path.append((near_ends, cable, far_ends))
  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(_nodes__contains=obj)
  72. serializer = serializers.CablePathSerializer(cablepaths, context={'request': request}, many=True)
  73. return Response(serializer.data)
  74. #
  75. # Regions
  76. #
  77. class RegionViewSet(MPTTLockedMixin, NetBoxModelViewSet):
  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(MPTTLockedMixin, NetBoxModelViewSet):
  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(NetBoxModelViewSet):
  104. queryset = Site.objects.all()
  105. serializer_class = serializers.SiteSerializer
  106. filterset_class = filtersets.SiteFilterSet
  107. #
  108. # Locations
  109. #
  110. class LocationViewSet(MPTTLockedMixin, NetBoxModelViewSet):
  111. queryset = Location.objects.add_related_count(
  112. Location.objects.add_related_count(
  113. Location.objects.all(),
  114. Device,
  115. 'location',
  116. 'device_count',
  117. cumulative=True
  118. ),
  119. Rack,
  120. 'location',
  121. 'rack_count',
  122. cumulative=True
  123. )
  124. serializer_class = serializers.LocationSerializer
  125. filterset_class = filtersets.LocationFilterSet
  126. #
  127. # Rack roles
  128. #
  129. class RackRoleViewSet(NetBoxModelViewSet):
  130. queryset = RackRole.objects.all()
  131. serializer_class = serializers.RackRoleSerializer
  132. filterset_class = filtersets.RackRoleFilterSet
  133. #
  134. # Rack Types
  135. #
  136. class RackTypeViewSet(NetBoxModelViewSet):
  137. queryset = RackType.objects.all()
  138. serializer_class = serializers.RackTypeSerializer
  139. filterset_class = filtersets.RackTypeFilterSet
  140. #
  141. # Racks
  142. #
  143. class RackViewSet(NetBoxModelViewSet):
  144. queryset = Rack.objects.all()
  145. serializer_class = serializers.RackSerializer
  146. filterset_class = filtersets.RackFilterSet
  147. @extend_schema(
  148. operation_id='dcim_racks_elevation_retrieve',
  149. filters=False,
  150. parameters=[serializers.RackElevationDetailFilterSerializer],
  151. responses={200: serializers.RackUnitSerializer(many=True)}
  152. )
  153. @action(detail=True)
  154. def elevation(self, request, pk=None):
  155. """
  156. Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
  157. """
  158. rack = get_object_or_404(self.queryset, pk=pk)
  159. serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
  160. if not serializer.is_valid():
  161. return Response(serializer.errors, 400)
  162. data = serializer.validated_data
  163. if data['render'] == 'svg':
  164. # Determine attributes for highlighting devices (if any)
  165. highlight_params = []
  166. for param in request.GET.getlist('highlight'):
  167. try:
  168. highlight_params.append(param.split(':', 1))
  169. except ValueError:
  170. pass
  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. highlight_params=highlight_params
  181. )
  182. return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
  183. else:
  184. # Return a JSON representation of the rack units in the elevation
  185. elevation = rack.get_rack_units(
  186. face=data['face'],
  187. user=request.user,
  188. exclude=data['exclude'],
  189. expand_devices=data['expand_devices']
  190. )
  191. # Enable filtering rack units by ID
  192. if q := data['q']:
  193. q = q.lower()
  194. elevation = [u for u in elevation if q in str(u['id']) or q in str(u['name']).lower()]
  195. page = self.paginate_queryset(elevation)
  196. if page is not None:
  197. rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
  198. return self.get_paginated_response(rack_units.data)
  199. #
  200. # Rack reservations
  201. #
  202. class RackReservationViewSet(NetBoxModelViewSet):
  203. queryset = RackReservation.objects.all()
  204. serializer_class = serializers.RackReservationSerializer
  205. filterset_class = filtersets.RackReservationFilterSet
  206. #
  207. # Manufacturers
  208. #
  209. class ManufacturerViewSet(NetBoxModelViewSet):
  210. queryset = Manufacturer.objects.all()
  211. serializer_class = serializers.ManufacturerSerializer
  212. filterset_class = filtersets.ManufacturerFilterSet
  213. #
  214. # Device/module types
  215. #
  216. class DeviceTypeViewSet(NetBoxModelViewSet):
  217. queryset = DeviceType.objects.all()
  218. serializer_class = serializers.DeviceTypeSerializer
  219. filterset_class = filtersets.DeviceTypeFilterSet
  220. class ModuleTypeProfileViewSet(NetBoxModelViewSet):
  221. queryset = ModuleTypeProfile.objects.all()
  222. serializer_class = serializers.ModuleTypeProfileSerializer
  223. filterset_class = filtersets.ModuleTypeProfileFilterSet
  224. class ModuleTypeViewSet(NetBoxModelViewSet):
  225. queryset = ModuleType.objects.all()
  226. serializer_class = serializers.ModuleTypeSerializer
  227. filterset_class = filtersets.ModuleTypeFilterSet
  228. #
  229. # Device type components
  230. #
  231. class ConsolePortTemplateViewSet(NetBoxModelViewSet):
  232. queryset = ConsolePortTemplate.objects.all()
  233. serializer_class = serializers.ConsolePortTemplateSerializer
  234. filterset_class = filtersets.ConsolePortTemplateFilterSet
  235. class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
  236. queryset = ConsoleServerPortTemplate.objects.all()
  237. serializer_class = serializers.ConsoleServerPortTemplateSerializer
  238. filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
  239. class PowerPortTemplateViewSet(NetBoxModelViewSet):
  240. queryset = PowerPortTemplate.objects.all()
  241. serializer_class = serializers.PowerPortTemplateSerializer
  242. filterset_class = filtersets.PowerPortTemplateFilterSet
  243. class PowerOutletTemplateViewSet(NetBoxModelViewSet):
  244. queryset = PowerOutletTemplate.objects.all()
  245. serializer_class = serializers.PowerOutletTemplateSerializer
  246. filterset_class = filtersets.PowerOutletTemplateFilterSet
  247. class InterfaceTemplateViewSet(NetBoxModelViewSet):
  248. queryset = InterfaceTemplate.objects.all()
  249. serializer_class = serializers.InterfaceTemplateSerializer
  250. filterset_class = filtersets.InterfaceTemplateFilterSet
  251. class FrontPortTemplateViewSet(NetBoxModelViewSet):
  252. queryset = FrontPortTemplate.objects.all()
  253. serializer_class = serializers.FrontPortTemplateSerializer
  254. filterset_class = filtersets.FrontPortTemplateFilterSet
  255. class RearPortTemplateViewSet(NetBoxModelViewSet):
  256. queryset = RearPortTemplate.objects.all()
  257. serializer_class = serializers.RearPortTemplateSerializer
  258. filterset_class = filtersets.RearPortTemplateFilterSet
  259. class ModuleBayTemplateViewSet(NetBoxModelViewSet):
  260. queryset = ModuleBayTemplate.objects.all()
  261. serializer_class = serializers.ModuleBayTemplateSerializer
  262. filterset_class = filtersets.ModuleBayTemplateFilterSet
  263. class DeviceBayTemplateViewSet(NetBoxModelViewSet):
  264. queryset = DeviceBayTemplate.objects.all()
  265. serializer_class = serializers.DeviceBayTemplateSerializer
  266. filterset_class = filtersets.DeviceBayTemplateFilterSet
  267. class InventoryItemTemplateViewSet(MPTTLockedMixin, NetBoxModelViewSet):
  268. queryset = InventoryItemTemplate.objects.all()
  269. serializer_class = serializers.InventoryItemTemplateSerializer
  270. filterset_class = filtersets.InventoryItemTemplateFilterSet
  271. #
  272. # Device roles
  273. #
  274. class DeviceRoleViewSet(NetBoxModelViewSet):
  275. queryset = DeviceRole.objects.add_related_count(
  276. DeviceRole.objects.add_related_count(
  277. DeviceRole.objects.all(),
  278. VirtualMachine,
  279. 'role',
  280. 'virtualmachine_count',
  281. cumulative=True
  282. ),
  283. Device,
  284. 'role',
  285. 'device_count',
  286. cumulative=True
  287. )
  288. serializer_class = serializers.DeviceRoleSerializer
  289. filterset_class = filtersets.DeviceRoleFilterSet
  290. #
  291. # Platforms
  292. #
  293. class PlatformViewSet(MPTTLockedMixin, NetBoxModelViewSet):
  294. queryset = Platform.objects.add_related_count(
  295. Platform.objects.add_related_count(
  296. Platform.objects.all(),
  297. VirtualMachine,
  298. 'platform',
  299. 'virtualmachine_count',
  300. cumulative=True
  301. ),
  302. Device,
  303. 'platform',
  304. 'device_count',
  305. cumulative=True
  306. )
  307. serializer_class = serializers.PlatformSerializer
  308. filterset_class = filtersets.PlatformFilterSet
  309. #
  310. # Devices/modules
  311. #
  312. class DeviceViewSet(
  313. SequentialBulkCreatesMixin,
  314. ConfigContextQuerySetMixin,
  315. RenderConfigMixin,
  316. NetBoxModelViewSet
  317. ):
  318. queryset = Device.objects.prefetch_related(
  319. 'parent_bay', # Referenced by DeviceSerializer.get_parent_device()
  320. )
  321. filterset_class = filtersets.DeviceFilterSet
  322. pagination_class = StripCountAnnotationsPaginator
  323. def get_serializer_class(self):
  324. """
  325. Select the specific serializer based on the request context.
  326. If the `brief` query param equates to True, return the NestedDeviceSerializer
  327. If the `exclude` query param includes `config_context` as a value, return the DeviceSerializer
  328. Else, return the DeviceWithConfigContextSerializer
  329. """
  330. request = self.get_serializer_context()['request']
  331. if self.brief or 'config_context' in request.query_params.get('exclude', []):
  332. return serializers.DeviceSerializer
  333. return serializers.DeviceWithConfigContextSerializer
  334. class VirtualDeviceContextViewSet(NetBoxModelViewSet):
  335. queryset = VirtualDeviceContext.objects.all()
  336. serializer_class = serializers.VirtualDeviceContextSerializer
  337. filterset_class = filtersets.VirtualDeviceContextFilterSet
  338. class ModuleViewSet(NetBoxModelViewSet):
  339. queryset = Module.objects.all()
  340. serializer_class = serializers.ModuleSerializer
  341. filterset_class = filtersets.ModuleFilterSet
  342. #
  343. # Device components
  344. #
  345. class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
  346. queryset = ConsolePort.objects.prefetch_related(
  347. '_path', 'cable__terminations',
  348. )
  349. serializer_class = serializers.ConsolePortSerializer
  350. filterset_class = filtersets.ConsolePortFilterSet
  351. class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
  352. queryset = ConsoleServerPort.objects.prefetch_related(
  353. '_path', 'cable__terminations',
  354. )
  355. serializer_class = serializers.ConsoleServerPortSerializer
  356. filterset_class = filtersets.ConsoleServerPortFilterSet
  357. class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
  358. queryset = PowerPort.objects.prefetch_related(
  359. '_path', 'cable__terminations',
  360. )
  361. serializer_class = serializers.PowerPortSerializer
  362. filterset_class = filtersets.PowerPortFilterSet
  363. class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
  364. queryset = PowerOutlet.objects.prefetch_related(
  365. '_path', 'cable__terminations',
  366. )
  367. serializer_class = serializers.PowerOutletSerializer
  368. filterset_class = filtersets.PowerOutletFilterSet
  369. class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
  370. queryset = Interface.objects.prefetch_related(
  371. GenericPrefetch(
  372. "cable__terminations__termination",
  373. [
  374. Interface.objects.select_related("device", "cable"),
  375. ],
  376. ),
  377. GenericPrefetch(
  378. "_path__path_objects",
  379. [
  380. Interface.objects.select_related("device", "cable"),
  381. ],
  382. ),
  383. 'virtual_circuit_termination',
  384. 'l2vpn_terminations', # Referenced by InterfaceSerializer.l2vpn_termination
  385. 'ip_addresses', # Referenced by Interface.count_ipaddresses()
  386. 'fhrp_group_assignments', # Referenced by Interface.count_fhrp_groups()
  387. )
  388. serializer_class = serializers.InterfaceSerializer
  389. filterset_class = filtersets.InterfaceFilterSet
  390. def get_bulk_destroy_queryset(self):
  391. # Ensure child interfaces are deleted prior to their parents
  392. return self.get_queryset().order_by('device', 'parent', CollateAsChar('_name'))
  393. class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
  394. queryset = FrontPort.objects.prefetch_related(
  395. 'cable__terminations',
  396. )
  397. serializer_class = serializers.FrontPortSerializer
  398. filterset_class = filtersets.FrontPortFilterSet
  399. class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
  400. queryset = RearPort.objects.prefetch_related(
  401. 'cable__terminations',
  402. )
  403. serializer_class = serializers.RearPortSerializer
  404. filterset_class = filtersets.RearPortFilterSet
  405. class ModuleBayViewSet(NetBoxModelViewSet):
  406. queryset = ModuleBay.objects.all()
  407. serializer_class = serializers.ModuleBaySerializer
  408. filterset_class = filtersets.ModuleBayFilterSet
  409. class DeviceBayViewSet(NetBoxModelViewSet):
  410. queryset = DeviceBay.objects.all()
  411. serializer_class = serializers.DeviceBaySerializer
  412. filterset_class = filtersets.DeviceBayFilterSet
  413. class InventoryItemViewSet(MPTTLockedMixin, NetBoxModelViewSet):
  414. queryset = InventoryItem.objects.all()
  415. serializer_class = serializers.InventoryItemSerializer
  416. filterset_class = filtersets.InventoryItemFilterSet
  417. #
  418. # Device component roles
  419. #
  420. class InventoryItemRoleViewSet(NetBoxModelViewSet):
  421. queryset = InventoryItemRole.objects.all()
  422. serializer_class = serializers.InventoryItemRoleSerializer
  423. filterset_class = filtersets.InventoryItemRoleFilterSet
  424. #
  425. # Addressing
  426. #
  427. class MACAddressViewSet(NetBoxModelViewSet):
  428. queryset = MACAddress.objects.all()
  429. serializer_class = serializers.MACAddressSerializer
  430. filterset_class = filtersets.MACAddressFilterSet
  431. #
  432. # Cables
  433. #
  434. class CableViewSet(NetBoxModelViewSet):
  435. queryset = Cable.objects.prefetch_related('terminations__termination')
  436. serializer_class = serializers.CableSerializer
  437. filterset_class = filtersets.CableFilterSet
  438. class CableTerminationViewSet(NetBoxModelViewSet):
  439. metadata_class = ContentTypeMetadata
  440. queryset = CableTermination.objects.all()
  441. serializer_class = serializers.CableTerminationSerializer
  442. filterset_class = filtersets.CableTerminationFilterSet
  443. #
  444. # Virtual chassis
  445. #
  446. class VirtualChassisViewSet(NetBoxModelViewSet):
  447. queryset = VirtualChassis.objects.prefetch_related(
  448. # Prefetch related object for the display of unnamed devices
  449. 'master__virtual_chassis',
  450. )
  451. serializer_class = serializers.VirtualChassisSerializer
  452. filterset_class = filtersets.VirtualChassisFilterSet
  453. #
  454. # Power panels
  455. #
  456. class PowerPanelViewSet(NetBoxModelViewSet):
  457. queryset = PowerPanel.objects.all()
  458. serializer_class = serializers.PowerPanelSerializer
  459. filterset_class = filtersets.PowerPanelFilterSet
  460. #
  461. # Power feeds
  462. #
  463. class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
  464. queryset = PowerFeed.objects.prefetch_related(
  465. '_path', 'cable__terminations',
  466. )
  467. serializer_class = serializers.PowerFeedSerializer
  468. filterset_class = filtersets.PowerFeedFilterSet
  469. #
  470. # Miscellaneous
  471. #
  472. class ConnectedDeviceViewSet(ViewSet):
  473. """
  474. This endpoint allows a user to determine what device (if any) is connected to a given peer device and peer
  475. interface. This is useful in a situation where a device boots with no configuration, but can detect its neighbors
  476. via a protocol such as LLDP. Two query parameters must be included in the request:
  477. * `peer_device`: The name of the peer device
  478. * `peer_interface`: The name of the peer interface
  479. """
  480. permission_classes = [IsAuthenticatedOrLoginNotRequired]
  481. _device_param = OpenApiParameter(
  482. name='peer_device',
  483. location='query',
  484. description='The name of the peer device',
  485. required=True,
  486. type=OpenApiTypes.STR
  487. )
  488. _interface_param = OpenApiParameter(
  489. name='peer_interface',
  490. location='query',
  491. description='The name of the peer interface',
  492. required=True,
  493. type=OpenApiTypes.STR
  494. )
  495. serializer_class = serializers.DeviceSerializer
  496. def get_view_name(self):
  497. return "Connected Device Locator"
  498. @extend_schema(
  499. parameters=[_device_param, _interface_param],
  500. responses={200: serializers.DeviceSerializer}
  501. )
  502. def list(self, request):
  503. peer_device_name = request.query_params.get(self._device_param.name)
  504. peer_interface_name = request.query_params.get(self._interface_param.name)
  505. if not peer_device_name or not peer_interface_name:
  506. raise MissingFilterException(detail='Request must include "peer_device" and "peer_interface" filters.')
  507. # Determine local endpoint from peer interface's connection
  508. peer_device = get_object_or_404(
  509. Device.objects.restrict(request.user, 'view'),
  510. name=peer_device_name
  511. )
  512. peer_interface = get_object_or_404(
  513. Interface.objects.restrict(request.user, 'view'),
  514. device=peer_device,
  515. name=peer_interface_name
  516. )
  517. endpoints = peer_interface.connected_endpoints
  518. # If an Interface, return the parent device
  519. if endpoints and type(endpoints[0]) is Interface:
  520. device = get_object_or_404(
  521. Device.objects.restrict(request.user, 'view'),
  522. pk=endpoints[0].device_id
  523. )
  524. return Response(serializers.DeviceSerializer(device, context={'request': request}).data)
  525. # Connected endpoint is none or not an Interface
  526. raise Http404