views.py 24 KB

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