views.py 19 KB

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