From 062a5bfe8d3ec3aea653d71dcfa4a715dd9dd86f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 24 Jan 2017 17:12:16 -0500 Subject: [PATCH 001/206] Initial work on API v2.0 --- netbox/circuits/api/urls.py | 12 +- netbox/circuits/api/views.py | 54 ++++----- netbox/dcim/api/urls.py | 52 ++++----- netbox/dcim/api/views.py | 193 ++++++++++--------------------- netbox/dcim/tests/test_apis.py | 16 +-- netbox/extras/api/renderers.py | 2 + netbox/extras/api/serializers.py | 10 +- netbox/extras/api/views.py | 27 +++-- netbox/ipam/api/urls.py | 36 +++--- netbox/ipam/api/views.py | 128 ++++---------------- netbox/secrets/api/urls.py | 8 +- netbox/secrets/api/views.py | 20 ++-- netbox/tenancy/api/urls.py | 8 +- netbox/tenancy/api/views.py | 36 +++--- 14 files changed, 216 insertions(+), 386 deletions(-) diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index afc03414193..956b87207fb 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -9,17 +9,17 @@ urlpatterns = [ # Providers - url(r'^providers/$', ProviderListView.as_view(), name='provider_list'), - url(r'^providers/(?P\d+)/$', ProviderDetailView.as_view(), name='provider_detail'), + url(r'^providers/$', ProviderViewSet.as_view({'get': 'list'}), name='provider_list'), + url(r'^providers/(?P\d+)/$', ProviderViewSet.as_view({'get': 'retrieve'}), name='provider_detail'), url(r'^providers/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, name='provider_graphs'), # Circuit types - url(r'^circuit-types/$', CircuitTypeListView.as_view(), name='circuittype_list'), - url(r'^circuit-types/(?P\d+)/$', CircuitTypeDetailView.as_view(), name='circuittype_detail'), + url(r'^circuit-types/$', CircuitTypeViewSet.as_view({'get': 'list'}), name='circuittype_list'), + url(r'^circuit-types/(?P\d+)/$', CircuitTypeViewSet.as_view({'get': 'retrieve'}), name='circuittype_detail'), # Circuits - url(r'^circuits/$', CircuitListView.as_view(), name='circuit_list'), - url(r'^circuits/(?P\d+)/$', CircuitDetailView.as_view(), name='circuit_detail'), + url(r'^circuits/$', CircuitViewSet.as_view({'get': 'list'}), name='circuit_list'), + url(r'^circuits/(?P\d+)/$', CircuitViewSet.as_view({'get': 'retrieve'}), name='circuit_detail'), ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index d8928603647..6c64da3293d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,58 +1,44 @@ -from rest_framework import generics +from rest_framework.viewsets import ModelViewSet from circuits.models import Provider, CircuitType, Circuit from circuits.filters import CircuitFilter -from extras.api.views import CustomFieldModelAPIView +from extras.api.views import CustomFieldModelViewSet from . import serializers -class ProviderListView(CustomFieldModelAPIView, generics.ListAPIView): - """ - List all providers - """ - queryset = Provider.objects.prefetch_related('custom_field_values__field') - serializer_class = serializers.ProviderSerializer - +# +# Providers +# -class ProviderDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): +class ProviderViewSet(CustomFieldModelViewSet): """ - Retrieve a single provider + List and retrieve circuit providers """ - queryset = Provider.objects.prefetch_related('custom_field_values__field') + queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer -class CircuitTypeListView(generics.ListAPIView): - """ - List all circuit types - """ - queryset = CircuitType.objects.all() - serializer_class = serializers.CircuitTypeSerializer +# +# Circuit Types +# - -class CircuitTypeDetailView(generics.RetrieveAPIView): +class CircuitTypeViewSet(ModelViewSet): """ - Retrieve a single circuit type + List and retrieve circuit types """ queryset = CircuitType.objects.all() serializer_class = serializers.CircuitTypeSerializer -class CircuitListView(CustomFieldModelAPIView, generics.ListAPIView): - """ - List circuits (filterable) - """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\ - .prefetch_related('custom_field_values__field') - serializer_class = serializers.CircuitSerializer - filter_class = CircuitFilter - +# +# Circuits +# -class CircuitDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): +class CircuitViewSet(CustomFieldModelViewSet): """ - Retrieve a single circuit + List and retrieve circuits """ - queryset = Circuit.objects.select_related('type', 'tenant', 'provider')\ - .prefetch_related('custom_field_values__field') + queryset = Circuit.objects.select_related('type', 'tenant', 'provider') serializer_class = serializers.CircuitSerializer + filter_class = CircuitFilter diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 23787f4b422..a0ea5796b3a 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -9,52 +9,50 @@ urlpatterns = [ # Sites - url(r'^sites/$', SiteListView.as_view(), name='site_list'), - url(r'^sites/(?P\d+)/$', SiteDetailView.as_view(), name='site_detail'), + url(r'^sites/$', SiteViewSet.as_view({'get': 'list'}), name='site_list'), + url(r'^sites/(?P\d+)/$', SiteViewSet.as_view({'get': 'retrieve'}), name='site_detail'), url(r'^sites/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'), - url(r'^sites/(?P\d+)/racks/$', RackListView.as_view(), name='site_racks'), # Rack groups - url(r'^rack-groups/$', RackGroupListView.as_view(), name='rackgroup_list'), - url(r'^rack-groups/(?P\d+)/$', RackGroupDetailView.as_view(), name='rackgroup_detail'), + url(r'^rack-groups/$', RackGroupViewSet.as_view({'get': 'list'}), name='rackgroup_list'), + url(r'^rack-groups/(?P\d+)/$', RackGroupViewSet.as_view({'get': 'retrieve'}), name='rackgroup_detail'), # Rack roles - url(r'^rack-roles/$', RackRoleListView.as_view(), name='rackrole_list'), - url(r'^rack-roles/(?P\d+)/$', RackRoleDetailView.as_view(), name='rackrole_detail'), + url(r'^rack-roles/$', RackRoleViewSet.as_view({'get': 'list'}), name='rackrole_list'), + url(r'^rack-roles/(?P\d+)/$', RackRoleViewSet.as_view({'get': 'retrieve'}), name='rackrole_detail'), # Racks - url(r'^racks/$', RackListView.as_view(), name='rack_list'), - url(r'^racks/(?P\d+)/$', RackDetailView.as_view(), name='rack_detail'), + url(r'^racks/$', RackViewSet.as_view({'get': 'list'}), name='rack_list'), + url(r'^racks/(?P\d+)/$', RackViewSet.as_view({'get': 'retrieve'}), name='rack_detail'), url(r'^racks/(?P\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'), # Manufacturers - url(r'^manufacturers/$', ManufacturerListView.as_view(), name='manufacturer_list'), - url(r'^manufacturers/(?P\d+)/$', ManufacturerDetailView.as_view(), name='manufacturer_detail'), + url(r'^manufacturers/$', ManufacturerViewSet.as_view({'get': 'list'}), name='manufacturer_list'), + url(r'^manufacturers/(?P\d+)/$', ManufacturerViewSet.as_view({'get': 'retrieve'}), name='manufacturer_detail'), # Device types - url(r'^device-types/$', DeviceTypeListView.as_view(), name='devicetype_list'), - url(r'^device-types/(?P\d+)/$', DeviceTypeDetailView.as_view(), name='devicetype_detail'), + url(r'^device-types/$', DeviceTypeViewSet.as_view({'get': 'list'}), name='devicetype_list'), + url(r'^device-types/(?P\d+)/$', DeviceTypeViewSet.as_view({'get': 'retrieve'}), name='devicetype_detail'), # Device roles - url(r'^device-roles/$', DeviceRoleListView.as_view(), name='devicerole_list'), - url(r'^device-roles/(?P\d+)/$', DeviceRoleDetailView.as_view(), name='devicerole_detail'), + url(r'^device-roles/$', DeviceRoleViewSet.as_view({'get': 'list'}), name='devicerole_list'), + url(r'^device-roles/(?P\d+)/$', DeviceRoleViewSet.as_view({'get': 'retrieve'}), name='devicerole_detail'), # Platforms - url(r'^platforms/$', PlatformListView.as_view(), name='platform_list'), - url(r'^platforms/(?P\d+)/$', PlatformDetailView.as_view(), name='platform_detail'), + url(r'^platforms/$', PlatformViewSet.as_view({'get': 'list'}), name='platform_list'), + url(r'^platforms/(?P\d+)/$', PlatformViewSet.as_view({'get': 'retrieve'}), name='platform_detail'), # Devices - url(r'^devices/$', DeviceListView.as_view(), name='device_list'), - url(r'^devices/(?P\d+)/$', DeviceDetailView.as_view(), name='device_detail'), + url(r'^devices/$', DeviceViewSet.as_view({'get': 'list'}), name='device_list'), + url(r'^devices/(?P\d+)/$', DeviceViewSet.as_view({'get': 'retrieve'}), name='device_detail'), url(r'^devices/(?P\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - url(r'^devices/(?P\d+)/console-ports/$', ConsolePortListView.as_view(), name='device_consoleports'), - url(r'^devices/(?P\d+)/console-server-ports/$', ConsoleServerPortListView.as_view(), - name='device_consoleserverports'), - url(r'^devices/(?P\d+)/power-ports/$', PowerPortListView.as_view(), name='device_powerports'), - url(r'^devices/(?P\d+)/power-outlets/$', PowerOutletListView.as_view(), name='device_poweroutlets'), - url(r'^devices/(?P\d+)/interfaces/$', InterfaceListView.as_view(), name='device_interfaces'), - url(r'^devices/(?P\d+)/device-bays/$', DeviceBayListView.as_view(), name='device_devicebays'), - url(r'^devices/(?P\d+)/modules/$', ModuleListView.as_view(), name='device_modules'), + url(r'^devices/(?P\d+)/console-ports/$', ConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'), + url(r'^devices/(?P\d+)/console-server-ports/$', ConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'), + url(r'^devices/(?P\d+)/power-ports/$', PowerPortViewSet.as_view({'get': 'list'}), name='device_powerports'), + url(r'^devices/(?P\d+)/power-outlets/$', PowerOutletViewSet.as_view({'get': 'list'}), name='device_poweroutlets'), + url(r'^devices/(?P\d+)/interfaces/$', InterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'), + url(r'^devices/(?P\d+)/device-bays/$', DeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'), + url(r'^devices/(?P\d+)/modules/$', ModuleViewSet.as_view({'get': 'list'}), name='device_modules'), # Console ports url(r'^console-ports/(?P\d+)/$', ConsolePortView.as_view(), name='consoleport'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 70ca17bbc0a..565593e9c86 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,10 +3,10 @@ from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet from django.conf import settings from django.contrib.contenttypes.models import ContentType -from django.db.models import Count from django.http import Http404 from django.shortcuts import get_object_or_404 @@ -15,7 +15,7 @@ InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from dcim import filters -from extras.api.views import CustomFieldModelAPIView +from extras.api.views import CustomFieldModelViewSet from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer from utilities.api import ServiceUnavailable from .exceptions import MissingFilterException @@ -26,19 +26,11 @@ # Sites # -class SiteListView(CustomFieldModelAPIView, generics.ListAPIView): +class SiteViewSet(CustomFieldModelViewSet): """ - List all sites + List and retrieve sites """ - queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field') - serializer_class = serializers.SiteSerializer - - -class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single site - """ - queryset = Site.objects.select_related('tenant').prefetch_related('custom_field_values__field') + queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer @@ -46,38 +38,22 @@ class SiteDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): # Rack groups # -class RackGroupListView(generics.ListAPIView): +class RackGroupViewSet(ModelViewSet): """ - List all rack groups + List and retrieve rack groups """ queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer filter_class = filters.RackGroupFilter -class RackGroupDetailView(generics.RetrieveAPIView): - """ - Retrieve a single rack group - """ - queryset = RackGroup.objects.select_related('site') - serializer_class = serializers.RackGroupSerializer - - # # Rack roles # -class RackRoleListView(generics.ListAPIView): - """ - List all rack roles - """ - queryset = RackRole.objects.all() - serializer_class = serializers.RackRoleSerializer - - -class RackRoleDetailView(generics.RetrieveAPIView): +class RackRoleViewSet(ModelViewSet): """ - Retrieve a single rack role + List and retrieve rack roles """ queryset = RackRole.objects.all() serializer_class = serializers.RackRoleSerializer @@ -87,28 +63,18 @@ class RackRoleDetailView(generics.RetrieveAPIView): # Racks # -class RackListView(CustomFieldModelAPIView, generics.ListAPIView): +class RackViewSet(CustomFieldModelViewSet): """ - List racks (filterable) + List and retrieve racks """ - queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\ - .prefetch_related('custom_field_values__field') - serializer_class = serializers.RackSerializer + queryset = Rack.objects.select_related('site', 'group__site', 'tenant') filter_class = filters.RackFilter + def get_serializer_class(self): + if self.action == 'retrieve': + return serializers.RackDetailSerializer + return serializers.RackSerializer -class RackDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single rack - """ - queryset = Rack.objects.select_related('site', 'group__site', 'tenant')\ - .prefetch_related('custom_field_values__field') - serializer_class = serializers.RackDetailSerializer - - -# -# Rack units -# class RackUnitListView(APIView): """ @@ -139,17 +105,9 @@ def get(self, request, pk): # Manufacturers # -class ManufacturerListView(generics.ListAPIView): - """ - List all hardware manufacturers - """ - queryset = Manufacturer.objects.all() - serializer_class = serializers.ManufacturerSerializer - - -class ManufacturerDetailView(generics.RetrieveAPIView): +class ManufacturerViewSet(ModelViewSet): """ - Retrieve a single hardware manufacturers + List and retrieve manufacturers """ queryset = Manufacturer.objects.all() serializer_class = serializers.ManufacturerSerializer @@ -159,38 +117,26 @@ class ManufacturerDetailView(generics.RetrieveAPIView): # Device Types # -class DeviceTypeListView(CustomFieldModelAPIView, generics.ListAPIView): +class DeviceTypeViewSet(CustomFieldModelViewSet): """ - List device types (filterable) + List and retrieve device types """ - queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field') - serializer_class = serializers.DeviceTypeSerializer + queryset = DeviceType.objects.select_related('manufacturer') filter_class = filters.DeviceTypeFilter - -class DeviceTypeDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single device type - """ - queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('custom_field_values__field') - serializer_class = serializers.DeviceTypeDetailSerializer + def get_serializer_class(self): + if self.action == 'retrieve': + return serializers.DeviceTypeDetailSerializer + return serializers.DeviceTypeSerializer # # Device roles # -class DeviceRoleListView(generics.ListAPIView): - """ - List all device roles - """ - queryset = DeviceRole.objects.all() - serializer_class = serializers.DeviceRoleSerializer - - -class DeviceRoleDetailView(generics.RetrieveAPIView): +class DeviceRoleViewSet(ModelViewSet): """ - Retrieve a single device role + List and retrieve device roles """ queryset = DeviceRole.objects.all() serializer_class = serializers.DeviceRoleSerializer @@ -200,17 +146,9 @@ class DeviceRoleDetailView(generics.RetrieveAPIView): # Platforms # -class PlatformListView(generics.ListAPIView): - """ - List all platforms - """ - queryset = Platform.objects.all() - serializer_class = serializers.PlatformSerializer - - -class PlatformDetailView(generics.RetrieveAPIView): +class PlatformViewSet(ModelViewSet): """ - Retrieve a single platform + List and retrieve platforms """ queryset = Platform.objects.all() serializer_class = serializers.PlatformSerializer @@ -220,40 +158,31 @@ class PlatformDetailView(generics.RetrieveAPIView): # Devices # -class DeviceListView(CustomFieldModelAPIView, generics.ListAPIView): +class DeviceViewSet(CustomFieldModelViewSet): """ - List devices (filterable) + List and retrieve devices """ - queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform', - 'rack__site', 'parent_bay').prefetch_related('primary_ip4__nat_outside', - 'primary_ip6__nat_outside', - 'custom_field_values__field') + queryset = Device.objects.select_related( + 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay', + ).prefetch_related( + 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', + ) serializer_class = serializers.DeviceSerializer filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] -class DeviceDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single device - """ - queryset = Device.objects.select_related('device_type__manufacturer', 'device_role', 'tenant', 'platform', - 'rack__site', 'parent_bay').prefetch_related('custom_field_values__field') - serializer_class = serializers.DeviceSerializer - - # # Console ports # -class ConsolePortListView(generics.ListAPIView): +class ConsolePortViewSet(ModelViewSet): """ - List console ports (by device) + List and retrieve console ports (by device) """ serializer_class = serializers.ConsolePortSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return ConsolePort.objects.filter(device=device).select_related('cs_port') @@ -268,14 +197,13 @@ class ConsolePortView(generics.RetrieveUpdateDestroyAPIView): # Console server ports # -class ConsoleServerPortListView(generics.ListAPIView): +class ConsoleServerPortViewSet(ModelViewSet): """ - List console server ports (by device) + List and retrieve console server ports (by device) """ serializer_class = serializers.ConsoleServerPortSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return ConsoleServerPort.objects.filter(device=device).select_related('connected_console') @@ -284,14 +212,13 @@ def get_queryset(self): # Power ports # -class PowerPortListView(generics.ListAPIView): +class PowerPortViewSet(ModelViewSet): """ - List power ports (by device) + List and retrieve power ports (by device) """ serializer_class = serializers.PowerPortSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return PowerPort.objects.filter(device=device).select_related('power_outlet') @@ -306,14 +233,13 @@ class PowerPortView(generics.RetrieveUpdateDestroyAPIView): # Power outlets # -class PowerOutletListView(generics.ListAPIView): +class PowerOutletViewSet(ModelViewSet): """ - List power outlets (by device) + List and retrieve power outlets (by device) """ serializer_class = serializers.PowerOutletSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return PowerOutlet.objects.filter(device=device).select_related('connected_port') @@ -322,9 +248,9 @@ def get_queryset(self): # Interfaces # -class InterfaceListView(generics.ListAPIView): +class InterfaceViewSet(ModelViewSet): """ - List interfaces (by device) + List and retrieve interfaces (by device) """ serializer_class = serializers.InterfaceSerializer filter_class = filters.InterfaceFilter @@ -372,14 +298,13 @@ class InterfaceConnectionListView(generics.ListAPIView): # Device bays # -class DeviceBayListView(generics.ListAPIView): +class DeviceBayViewSet(ModelViewSet): """ - List device bays (by device) + List and retrieve device bays (by device) """ serializer_class = serializers.DeviceBayNestedSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return DeviceBay.objects.filter(device=device).select_related('installed_device') @@ -388,14 +313,13 @@ def get_queryset(self): # Modules # -class ModuleListView(generics.ListAPIView): +class ModuleViewSet(ModelViewSet): """ - List device modules (by device) + List and retrieve modules (by device) """ serializer_class = serializers.ModuleSerializer def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) return Module.objects.filter(device=device).select_related('device', 'manufacturer') @@ -442,8 +366,19 @@ def __init__(self): super(RelatedConnectionsView, self).__init__() # Custom fields - self.content_type = ContentType.objects.get_for_model(Device) - self.custom_fields = self.content_type.custom_fields.prefetch_related('choices') + content_type = ContentType.objects.get_for_model(Device) + custom_fields = content_type.custom_fields.prefetch_related('choices') + + # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object. + custom_field_choices = {} + for field in custom_fields: + for cfc in field.choices.all(): + custom_field_choices[cfc.id] = cfc.value + + self.context = { + 'custom_fields': custom_fields, + 'custom_field_choices': custom_field_choices, + } def get(self, request): @@ -469,7 +404,7 @@ def get(self, request): # Initialize response skeleton response = { - 'device': serializers.DeviceSerializer(device, context={'view': self}).data, + 'device': serializers.DeviceSerializer(device, context=self.context).data, 'console-ports': [], 'power-ports': [], 'interfaces': [], diff --git a/netbox/dcim/tests/test_apis.py b/netbox/dcim/tests/test_apis.py index 0739b86ce36..7545a80ba6a 100644 --- a/netbox/dcim/tests/test_apis.py +++ b/netbox/dcim/tests/test_apis.py @@ -82,21 +82,6 @@ def test_get_detail(self, endpoint='/{}api/dcim/sites/1/'.format(settings.BASE_P sorted(self.standard_fields), ) - def test_get_site_list_rack(self, endpoint='/{}api/dcim/sites/1/racks/'.format(settings.BASE_PATH)): - response = self.client.get(endpoint) - content = json.loads(response.content.decode('utf-8')) - self.assertEqual(response.status_code, status.HTTP_200_OK) - for i in json.loads(response.content.decode('utf-8')): - self.assertEqual( - sorted(i.keys()), - sorted(self.rack_fields), - ) - # Check Nested Serializer. - self.assertEqual( - sorted(i.get('site').keys()), - sorted(self.nested_fields), - ) - def test_get_site_list_graphs(self, endpoint='/{}api/dcim/sites/1/graphs/'.format(settings.BASE_PATH)): response = self.client.get(endpoint) content = json.loads(response.content.decode('utf-8')) @@ -239,6 +224,7 @@ class DeviceTypeTest(APITestCase): 'subdevice_role', 'comments', 'custom_fields', + 'instance_count', ] nested_fields = [ diff --git a/netbox/extras/api/renderers.py b/netbox/extras/api/renderers.py index 0fd35c76259..2e85ed3a864 100644 --- a/netbox/extras/api/renderers.py +++ b/netbox/extras/api/renderers.py @@ -27,6 +27,8 @@ class BINDZoneRenderer(renderers.BaseRenderer): def render(self, data, media_type=None, renderer_context=None): records = [] + if not isinstance(data, (list, tuple)): + data = (data,) for record in data: if record.get('name') and record.get('primary_ip'): try: diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 4e82b40277c..01d348b0a35 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -12,22 +12,20 @@ class CustomFieldSerializer(serializers.Serializer): def get_custom_fields(self, obj): # Gather all CustomFields applicable to this object - fields = {cf.name: None for cf in self.context['view'].custom_fields} + fields = {cf.name: None for cf in self.context['custom_fields']} + custom_field_choices = self.context['custom_field_choices'] # Attach any defined CustomFieldValues to their respective CustomFields for cfv in obj.custom_field_values.all(): # Attempt to suppress database lookups for CustomFieldChoices by using the cached choice set from the view # context. - if cfv.field.type == CF_TYPE_SELECT and hasattr(self, 'custom_field_choices'): + if cfv.field.type == CF_TYPE_SELECT: cfc = { 'id': int(cfv.serialized_value), - 'value': self.context['view'].custom_field_choices[int(cfv.serialized_value)] + 'value': custom_field_choices[int(cfv.serialized_value)] } fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfc).data - # Fall back to hitting the database in case we're in a view that doesn't inherit CustomFieldModelAPIView. - elif cfv.field.type == CF_TYPE_SELECT: - fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfv.value).data else: fields[cfv.field.name] = cfv.value diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 19d7fab5fc2..1ee82ace422 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -1,6 +1,7 @@ import graphviz from rest_framework import generics from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet from django.contrib.contenttypes.models import ContentType from django.db.models import Q @@ -14,22 +15,32 @@ from .serializers import GraphSerializer -class CustomFieldModelAPIView(object): +class CustomFieldModelViewSet(ModelViewSet): """ - Include the applicable set of CustomField in the view context. + Include the applicable set of CustomField in the ModelViewSet context. """ - def __init__(self): - super(CustomFieldModelAPIView, self).__init__() - self.content_type = ContentType.objects.get_for_model(self.queryset.model) - self.custom_fields = self.content_type.custom_fields.prefetch_related('choices') + def get_serializer_context(self): + + # Gather all custom fields for the model + content_type = ContentType.objects.get_for_model(self.queryset.model) + custom_fields = content_type.custom_fields.prefetch_related('choices') # Cache all relevant CustomFieldChoices. This saves us from having to do a lookup per select field per object. custom_field_choices = {} - for field in self.custom_fields: + for field in custom_fields: for cfc in field.choices.all(): custom_field_choices[cfc.id] = cfc.value - self.custom_field_choices = custom_field_choices + custom_field_choices = custom_field_choices + + return { + 'custom_fields': custom_fields, + 'custom_field_choices': custom_field_choices, + } + + def get_queryset(self): + # Prefetch custom field values + return super(CustomFieldModelViewSet, self).get_queryset().prefetch_related('custom_field_values__field') class GraphListView(generics.ListAPIView): diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 598545ddf9a..19aef27985a 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -6,39 +6,39 @@ urlpatterns = [ # VRFs - url(r'^vrfs/$', VRFListView.as_view(), name='vrf_list'), - url(r'^vrfs/(?P\d+)/$', VRFDetailView.as_view(), name='vrf_detail'), + url(r'^vrfs/$', VRFViewSet.as_view({'get': 'list'}), name='vrf_list'), + url(r'^vrfs/(?P\d+)/$', VRFViewSet.as_view({'get': 'retrieve'}), name='vrf_detail'), # Roles - url(r'^roles/$', RoleListView.as_view(), name='role_list'), - url(r'^roles/(?P\d+)/$', RoleDetailView.as_view(), name='role_detail'), + url(r'^roles/$', RoleViewSet.as_view({'get': 'list'}), name='role_list'), + url(r'^roles/(?P\d+)/$', RoleViewSet.as_view({'get': 'retrieve'}), name='role_detail'), # RIRs - url(r'^rirs/$', RIRListView.as_view(), name='rir_list'), - url(r'^rirs/(?P\d+)/$', RIRDetailView.as_view(), name='rir_detail'), + url(r'^rirs/$', RIRViewSet.as_view({'get': 'list'}), name='rir_list'), + url(r'^rirs/(?P\d+)/$', RIRViewSet.as_view({'get': 'retrieve'}), name='rir_detail'), # Aggregates - url(r'^aggregates/$', AggregateListView.as_view(), name='aggregate_list'), - url(r'^aggregates/(?P\d+)/$', AggregateDetailView.as_view(), name='aggregate_detail'), + url(r'^aggregates/$', AggregateViewSet.as_view({'get': 'list'}), name='aggregate_list'), + url(r'^aggregates/(?P\d+)/$', AggregateViewSet.as_view({'get': 'retrieve'}), name='aggregate_detail'), # Prefixes - url(r'^prefixes/$', PrefixListView.as_view(), name='prefix_list'), - url(r'^prefixes/(?P\d+)/$', PrefixDetailView.as_view(), name='prefix_detail'), + url(r'^prefixes/$', PrefixViewSet.as_view({'get': 'list'}), name='prefix_list'), + url(r'^prefixes/(?P\d+)/$', PrefixViewSet.as_view({'get': 'retrieve'}), name='prefix_detail'), # IP addresses - url(r'^ip-addresses/$', IPAddressListView.as_view(), name='ipaddress_list'), - url(r'^ip-addresses/(?P\d+)/$', IPAddressDetailView.as_view(), name='ipaddress_detail'), + url(r'^ip-addresses/$', IPAddressViewSet.as_view({'get': 'list'}), name='ipaddress_list'), + url(r'^ip-addresses/(?P\d+)/$', IPAddressViewSet.as_view({'get': 'retrieve'}), name='ipaddress_detail'), # VLAN groups - url(r'^vlan-groups/$', VLANGroupListView.as_view(), name='vlangroup_list'), - url(r'^vlan-groups/(?P\d+)/$', VLANGroupDetailView.as_view(), name='vlangroup_detail'), + url(r'^vlan-groups/$', VLANGroupViewSet.as_view({'get': 'list'}), name='vlangroup_list'), + url(r'^vlan-groups/(?P\d+)/$', VLANGroupViewSet.as_view({'get': 'retrieve'}), name='vlangroup_detail'), # VLANs - url(r'^vlans/$', VLANListView.as_view(), name='vlan_list'), - url(r'^vlans/(?P\d+)/$', VLANDetailView.as_view(), name='vlan_detail'), + url(r'^vlans/$', VLANViewSet.as_view({'get': 'list'}), name='vlan_list'), + url(r'^vlans/(?P\d+)/$', VLANViewSet.as_view({'get': 'retrieve'}), name='vlan_detail'), # Services - url(r'^services/$', ServiceListView.as_view(), name='service_list'), - url(r'^services/(?P\d+)/$', ServiceDetailView.as_view(), name='service_detail'), + url(r'^services/$', ServiceViewSet.as_view({'get': 'list'}), name='service_list'), + url(r'^services/(?P\d+)/$', ServiceViewSet.as_view({'get': 'retrieve'}), name='service_detail'), ] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 10b9c46e40d..87200fe3a35 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,9 +1,9 @@ -from rest_framework import generics +from rest_framework.viewsets import ModelViewSet from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam import filters -from extras.api.views import CustomFieldModelAPIView +from extras.api.views import CustomFieldModelViewSet from . import serializers @@ -11,38 +11,22 @@ # VRFs # -class VRFListView(CustomFieldModelAPIView, generics.ListAPIView): +class VRFViewSet(CustomFieldModelViewSet): """ - List all VRFs + List and retrieve VRFs """ - queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values__field') + queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter -class VRFDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single VRF - """ - queryset = VRF.objects.select_related('tenant').prefetch_related('custom_field_values__field') - serializer_class = serializers.VRFSerializer - - # # Roles # -class RoleListView(generics.ListAPIView): +class RoleViewSet(ModelViewSet): """ - List all roles - """ - queryset = Role.objects.all() - serializer_class = serializers.RoleSerializer - - -class RoleDetailView(generics.RetrieveAPIView): - """ - Retrieve a single role + List and retrieve prefix/VLAN roles """ queryset = Role.objects.all() serializer_class = serializers.RoleSerializer @@ -52,17 +36,9 @@ class RoleDetailView(generics.RetrieveAPIView): # RIRs # -class RIRListView(generics.ListAPIView): +class RIRViewSet(ModelViewSet): """ - List all RIRs - """ - queryset = RIR.objects.all() - serializer_class = serializers.RIRSerializer - - -class RIRDetailView(generics.RetrieveAPIView): - """ - Retrieve a single RIR + List and retrieve RIRs """ queryset = RIR.objects.all() serializer_class = serializers.RIRSerializer @@ -72,129 +48,75 @@ class RIRDetailView(generics.RetrieveAPIView): # Aggregates # -class AggregateListView(CustomFieldModelAPIView, generics.ListAPIView): +class AggregateViewSet(CustomFieldModelViewSet): """ - List aggregates (filterable) + List and retrieve aggregates """ - queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values__field') + queryset = Aggregate.objects.select_related('rir') serializer_class = serializers.AggregateSerializer filter_class = filters.AggregateFilter -class AggregateDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single aggregate - """ - queryset = Aggregate.objects.select_related('rir').prefetch_related('custom_field_values__field') - serializer_class = serializers.AggregateSerializer - - # # Prefixes # -class PrefixListView(CustomFieldModelAPIView, generics.ListAPIView): +class PrefixViewSet(CustomFieldModelViewSet): """ - List prefixes (filterable) + List and retrieve prefixes """ - queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\ - .prefetch_related('custom_field_values__field') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter -class PrefixDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single prefix - """ - queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role')\ - .prefetch_related('custom_field_values__field') - serializer_class = serializers.PrefixSerializer - - # # IP addresses # -class IPAddressListView(CustomFieldModelAPIView, generics.ListAPIView): +class IPAddressViewSet(CustomFieldModelViewSet): """ - List IP addresses (filterable) + List and retrieve IP addresses """ - queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ - .prefetch_related('nat_outside', 'custom_field_values__field') + queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter -class IPAddressDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single IP address - """ - queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside')\ - .prefetch_related('nat_outside', 'custom_field_values__field') - serializer_class = serializers.IPAddressSerializer - - # # VLAN groups # -class VLANGroupListView(generics.ListAPIView): +class VLANGroupViewSet(ModelViewSet): """ - List all VLAN groups + List and retrieve VLAN groups """ queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer filter_class = filters.VLANGroupFilter -class VLANGroupDetailView(generics.RetrieveAPIView): - """ - Retrieve a single VLAN group - """ - queryset = VLANGroup.objects.select_related('site') - serializer_class = serializers.VLANGroupSerializer - - # # VLANs # -class VLANListView(CustomFieldModelAPIView, generics.ListAPIView): +class VLANViewSet(CustomFieldModelViewSet): """ - List VLANs (filterable) + List and retrieve VLANs """ - queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\ - .prefetch_related('custom_field_values__field') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter -class VLANDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): - """ - Retrieve a single VLAN - """ - queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role')\ - .prefetch_related('custom_field_values__field') - serializer_class = serializers.VLANSerializer - - # # Services # -class ServiceListView(generics.ListAPIView): +class ServiceViewSet(ModelViewSet): """ - List services (filterable) + List and retrieve services """ queryset = Service.objects.select_related('device').prefetch_related('ipaddresses') serializer_class = serializers.ServiceSerializer filter_class = filters.ServiceFilter - - -class ServiceDetailView(generics.RetrieveAPIView): - """ - Retrieve a single service - """ - queryset = Service.objects.select_related('device').prefetch_related('ipaddresses') - serializer_class = serializers.ServiceSerializer diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index 6acae580d2c..2ad2abc1e8d 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -5,14 +5,14 @@ urlpatterns = [ + # Secret roles + url(r'^secret-roles/$', SecretRoleViewSet.as_view({'get': 'list'}), name='secretrole_list'), + url(r'^secret-roles/(?P\d+)/$', SecretRoleViewSet.as_view({'get': 'retrieve'}), name='secretrole_detail'), + # Secrets url(r'^secrets/$', SecretListView.as_view(), name='secret_list'), url(r'^secrets/(?P\d+)/$', SecretDetailView.as_view(), name='secret_detail'), - # Secret roles - url(r'^secret-roles/$', SecretRoleListView.as_view(), name='secretrole_list'), - url(r'^secret-roles/(?P\d+)/$', SecretRoleDetailView.as_view(), name='secretrole_detail'), - # Miscellaneous url(r'^generate-keys/$', RSAKeyGeneratorView.as_view(), name='generate_keys'), diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 672165da3cd..10e95d51b4c 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -9,6 +9,7 @@ from rest_framework.renderers import JSONRenderer from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework.viewsets import ModelViewSet from extras.api.renderers import FormlessBrowsableAPIRenderer, FreeRADIUSClientsRenderer from secrets.filters import SecretFilter @@ -22,24 +23,23 @@ ERR_PRIVKEY_INVALID = "Invalid private key." -class SecretRoleListView(generics.ListAPIView): - """ - List all secret roles - """ - queryset = SecretRole.objects.all() - serializer_class = serializers.SecretRoleSerializer - permission_classes = [IsAuthenticated] - +# +# Secret Roles +# -class SecretRoleDetailView(generics.RetrieveAPIView): +class SecretRoleViewSet(ModelViewSet): """ - Retrieve a single secret role + List and retrieve secret roles """ queryset = SecretRole.objects.all() serializer_class = serializers.SecretRoleSerializer permission_classes = [IsAuthenticated] +# +# Secrets +# + class SecretListView(generics.GenericAPIView): """ List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index af1d1d6aa58..4573278556b 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -6,11 +6,11 @@ urlpatterns = [ # Tenant groups - url(r'^tenant-groups/$', TenantGroupListView.as_view(), name='tenantgroup_list'), - url(r'^tenant-groups/(?P\d+)/$', TenantGroupDetailView.as_view(), name='tenantgroup_detail'), + url(r'^tenant-groups/$', TenantGroupViewSet.as_view({'get': 'list'}), name='tenantgroup_list'), + url(r'^tenant-groups/(?P\d+)/$', TenantGroupViewSet.as_view({'get': 'retrieve'}), name='tenantgroup_detail'), # Tenants - url(r'^tenants/$', TenantListView.as_view(), name='tenant_list'), - url(r'^tenants/(?P\d+)/$', TenantDetailView.as_view(), name='tenant_detail'), + url(r'^tenants/$', TenantViewSet.as_view({'get': 'list'}), name='tenant_list'), + url(r'^tenants/(?P\d+)/$', TenantViewSet.as_view({'get': 'retrieve'}), name='tenant_detail'), ] diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index ce08eb05880..6cfeed2ef03 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -1,40 +1,32 @@ -from rest_framework import generics +from rest_framework.viewsets import ModelViewSet from tenancy.models import Tenant, TenantGroup from tenancy.filters import TenantFilter -from extras.api.views import CustomFieldModelAPIView +from extras.api.views import CustomFieldModelViewSet from . import serializers -class TenantGroupListView(generics.ListAPIView): - """ - List all tenant groups - """ - queryset = TenantGroup.objects.all() - serializer_class = serializers.TenantGroupSerializer - +# +# Tenant Groups +# -class TenantGroupDetailView(generics.RetrieveAPIView): +class TenantGroupViewSet(ModelViewSet): """ - Retrieve a single circuit type + List and retrieve tenant groups """ queryset = TenantGroup.objects.all() serializer_class = serializers.TenantGroupSerializer -class TenantListView(CustomFieldModelAPIView, generics.ListAPIView): - """ - List tenants (filterable) - """ - queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values__field') - serializer_class = serializers.TenantSerializer - filter_class = TenantFilter +# +# Tenants +# - -class TenantDetailView(CustomFieldModelAPIView, generics.RetrieveAPIView): +class TenantViewSet(CustomFieldModelViewSet): """ - Retrieve a single tenant + List and retrieve tenants """ - queryset = Tenant.objects.select_related('group').prefetch_related('custom_field_values__field') + queryset = Tenant.objects.select_related('group') serializer_class = serializers.TenantSerializer + filter_class = TenantFilter From 791a641eef20cf129ebd6ac80b04f6dc0b2853e0 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 15:33:41 -0500 Subject: [PATCH 002/206] Created CircuitDetailSerializer --- netbox/circuits/api/serializers.py | 18 +++++++++++++++--- netbox/circuits/api/views.py | 6 +++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 947aa9860bc..ba1ba80a98f 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -42,7 +42,7 @@ class Meta(CircuitTypeSerializer.Meta): # -# Circuits +# Circuit Terminations # class CircuitTerminationSerializer(serializers.ModelSerializer): @@ -54,19 +54,31 @@ class Meta: fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info'] +# +# Circuits +# + + class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer): provider = ProviderNestedSerializer() type = CircuitTypeNestedSerializer() tenant = TenantNestedSerializer() - terminations = CircuitTerminationSerializer(many=True) class Meta: model = Circuit fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - 'terminations', 'custom_fields'] + 'custom_fields'] class CircuitNestedSerializer(CircuitSerializer): class Meta(CircuitSerializer.Meta): fields = ['id', 'cid'] + + +class CircuitDetailSerializer(CircuitSerializer): + terminations = CircuitTerminationSerializer(many=True) + + class Meta(CircuitSerializer.Meta): + fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + 'terminations', 'custom_fields'] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 6c64da3293d..ec1f1c8ac22 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -40,5 +40,9 @@ class CircuitViewSet(CustomFieldModelViewSet): List and retrieve circuits """ queryset = Circuit.objects.select_related('type', 'tenant', 'provider') - serializer_class = serializers.CircuitSerializer filter_class = CircuitFilter + + def get_serializer_class(self): + if self.action == 'retrieve': + return serializers.CircuitDetailSerializer + return serializers.CircuitSerializer From 0f9fe8648e0b0e73e42ac9ea855793a936f2b732 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 15:34:07 -0500 Subject: [PATCH 003/206] Converted static URL definitions to routers --- netbox/circuits/api/urls.py | 23 +++++++------- netbox/dcim/api/urls.py | 60 +++++++++++++++++-------------------- netbox/ipam/api/urls.py | 54 +++++++++++---------------------- netbox/secrets/api/urls.py | 21 +++++++++---- netbox/tenancy/api/urls.py | 18 +++++------ 5 files changed, 82 insertions(+), 94 deletions(-) diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 956b87207fb..58c370449f1 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -1,25 +1,24 @@ -from django.conf.urls import url +from django.conf.urls import include, url + +from rest_framework import routers from extras.models import GRAPH_TYPE_PROVIDER from extras.api.views import GraphListView -from .views import * +from .views import CircuitViewSet, CircuitTypeViewSet, ProviderViewSet + +router = routers.DefaultRouter() +router.register(r'providers', ProviderViewSet) +router.register(r'circuit-types', CircuitTypeViewSet) +router.register(r'circuits', CircuitViewSet) urlpatterns = [ + url(r'', include(router.urls)), + # Providers - url(r'^providers/$', ProviderViewSet.as_view({'get': 'list'}), name='provider_list'), - url(r'^providers/(?P\d+)/$', ProviderViewSet.as_view({'get': 'retrieve'}), name='provider_detail'), url(r'^providers/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, name='provider_graphs'), - # Circuit types - url(r'^circuit-types/$', CircuitTypeViewSet.as_view({'get': 'list'}), name='circuittype_list'), - url(r'^circuit-types/(?P\d+)/$', CircuitTypeViewSet.as_view({'get': 'retrieve'}), name='circuittype_detail'), - - # Circuits - url(r'^circuits/$', CircuitViewSet.as_view({'get': 'list'}), name='circuit_list'), - url(r'^circuits/(?P\d+)/$', CircuitViewSet.as_view({'get': 'retrieve'}), name='circuit_detail'), - ] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index a0ea5796b3a..d31d901c67f 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -1,50 +1,45 @@ -from django.conf.urls import url +from django.conf.urls import include, url + +from rest_framework import routers from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.api.views import GraphListView, TopologyMapView -from .views import * +from .views import ( + + # Viewsets + ConsolePortViewSet, ConsoleServerPortViewSet, DeviceViewSet, DeviceBayViewSet, DeviceRoleViewSet, DeviceTypeViewSet, + InterfaceViewSet, ManufacturerViewSet, ModuleViewSet, PlatformViewSet, PowerPortViewSet, PowerOutletViewSet, + RackViewSet, RackGroupViewSet, RackRoleViewSet, SiteViewSet, + + # Legacy views + ConsolePortView, InterfaceConnectionView, InterfaceConnectionListView, InterfaceDetailView, PowerPortView, + LLDPNeighborsView, RackUnitListView, RelatedConnectionsView, +) +router = routers.DefaultRouter() +router.register(r'sites', SiteViewSet) +router.register(r'rack-groups', RackGroupViewSet) +router.register(r'rack-roles', RackRoleViewSet) +router.register(r'racks', RackViewSet) +router.register(r'manufacturers', ManufacturerViewSet) +router.register(r'device-types', DeviceTypeViewSet) +router.register(r'device-roles', DeviceRoleViewSet) +router.register(r'platforms', PlatformViewSet) +router.register(r'devices', DeviceViewSet) + urlpatterns = [ + url(r'', include(router.urls)), + # Sites - url(r'^sites/$', SiteViewSet.as_view({'get': 'list'}), name='site_list'), - url(r'^sites/(?P\d+)/$', SiteViewSet.as_view({'get': 'retrieve'}), name='site_detail'), url(r'^sites/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'), - # Rack groups - url(r'^rack-groups/$', RackGroupViewSet.as_view({'get': 'list'}), name='rackgroup_list'), - url(r'^rack-groups/(?P\d+)/$', RackGroupViewSet.as_view({'get': 'retrieve'}), name='rackgroup_detail'), - - # Rack roles - url(r'^rack-roles/$', RackRoleViewSet.as_view({'get': 'list'}), name='rackrole_list'), - url(r'^rack-roles/(?P\d+)/$', RackRoleViewSet.as_view({'get': 'retrieve'}), name='rackrole_detail'), - # Racks - url(r'^racks/$', RackViewSet.as_view({'get': 'list'}), name='rack_list'), - url(r'^racks/(?P\d+)/$', RackViewSet.as_view({'get': 'retrieve'}), name='rack_detail'), url(r'^racks/(?P\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'), - # Manufacturers - url(r'^manufacturers/$', ManufacturerViewSet.as_view({'get': 'list'}), name='manufacturer_list'), - url(r'^manufacturers/(?P\d+)/$', ManufacturerViewSet.as_view({'get': 'retrieve'}), name='manufacturer_detail'), - - # Device types - url(r'^device-types/$', DeviceTypeViewSet.as_view({'get': 'list'}), name='devicetype_list'), - url(r'^device-types/(?P\d+)/$', DeviceTypeViewSet.as_view({'get': 'retrieve'}), name='devicetype_detail'), - - # Device roles - url(r'^device-roles/$', DeviceRoleViewSet.as_view({'get': 'list'}), name='devicerole_list'), - url(r'^device-roles/(?P\d+)/$', DeviceRoleViewSet.as_view({'get': 'retrieve'}), name='devicerole_detail'), - - # Platforms - url(r'^platforms/$', PlatformViewSet.as_view({'get': 'list'}), name='platform_list'), - url(r'^platforms/(?P\d+)/$', PlatformViewSet.as_view({'get': 'retrieve'}), name='platform_detail'), - # Devices - url(r'^devices/$', DeviceViewSet.as_view({'get': 'list'}), name='device_list'), - url(r'^devices/(?P\d+)/$', DeviceViewSet.as_view({'get': 'retrieve'}), name='device_detail'), url(r'^devices/(?P\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), url(r'^devices/(?P\d+)/console-ports/$', ConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'), url(r'^devices/(?P\d+)/console-server-ports/$', ConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'), @@ -53,6 +48,7 @@ url(r'^devices/(?P\d+)/interfaces/$', InterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'), url(r'^devices/(?P\d+)/device-bays/$', DeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'), url(r'^devices/(?P\d+)/modules/$', ModuleViewSet.as_view({'get': 'list'}), name='device_modules'), + # TODO: Services # Console ports url(r'^console-ports/(?P\d+)/$', ConsolePortView.as_view(), name='consoleport'), diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 19aef27985a..b74739d0d6b 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,44 +1,26 @@ -from django.conf.urls import url +from django.conf.urls import include, url -from .views import * +from rest_framework import routers +from .views import ( + AggregateViewSet, IPAddressViewSet, PrefixViewSet, RIRViewSet, RoleViewSet, ServiceViewSet, VLANViewSet, + VLANGroupViewSet, VRFViewSet, +) -urlpatterns = [ - - # VRFs - url(r'^vrfs/$', VRFViewSet.as_view({'get': 'list'}), name='vrf_list'), - url(r'^vrfs/(?P\d+)/$', VRFViewSet.as_view({'get': 'retrieve'}), name='vrf_detail'), - - # Roles - url(r'^roles/$', RoleViewSet.as_view({'get': 'list'}), name='role_list'), - url(r'^roles/(?P\d+)/$', RoleViewSet.as_view({'get': 'retrieve'}), name='role_detail'), - - # RIRs - url(r'^rirs/$', RIRViewSet.as_view({'get': 'list'}), name='rir_list'), - url(r'^rirs/(?P\d+)/$', RIRViewSet.as_view({'get': 'retrieve'}), name='rir_detail'), - # Aggregates - url(r'^aggregates/$', AggregateViewSet.as_view({'get': 'list'}), name='aggregate_list'), - url(r'^aggregates/(?P\d+)/$', AggregateViewSet.as_view({'get': 'retrieve'}), name='aggregate_detail'), +router = routers.DefaultRouter() +router.register(r'vrfs', VRFViewSet) +router.register(r'rirs', RIRViewSet) +router.register(r'aggregates', AggregateViewSet) +router.register(r'roles', RoleViewSet) +router.register(r'prefixes', PrefixViewSet) +router.register(r'ip-addresses', IPAddressViewSet) +router.register(r'vlan-groups', VLANGroupViewSet) +router.register(r'vlans', VLANViewSet) +router.register(r'services', ServiceViewSet) - # Prefixes - url(r'^prefixes/$', PrefixViewSet.as_view({'get': 'list'}), name='prefix_list'), - url(r'^prefixes/(?P\d+)/$', PrefixViewSet.as_view({'get': 'retrieve'}), name='prefix_detail'), - - # IP addresses - url(r'^ip-addresses/$', IPAddressViewSet.as_view({'get': 'list'}), name='ipaddress_list'), - url(r'^ip-addresses/(?P\d+)/$', IPAddressViewSet.as_view({'get': 'retrieve'}), name='ipaddress_detail'), - - # VLAN groups - url(r'^vlan-groups/$', VLANGroupViewSet.as_view({'get': 'list'}), name='vlangroup_list'), - url(r'^vlan-groups/(?P\d+)/$', VLANGroupViewSet.as_view({'get': 'retrieve'}), name='vlangroup_detail'), - - # VLANs - url(r'^vlans/$', VLANViewSet.as_view({'get': 'list'}), name='vlan_list'), - url(r'^vlans/(?P\d+)/$', VLANViewSet.as_view({'get': 'retrieve'}), name='vlan_detail'), +urlpatterns = [ - # Services - url(r'^services/$', ServiceViewSet.as_view({'get': 'list'}), name='service_list'), - url(r'^services/(?P\d+)/$', ServiceViewSet.as_view({'get': 'retrieve'}), name='service_detail'), + url(r'', include(router.urls)), ] diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index 2ad2abc1e8d..14579dd3fbb 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -1,13 +1,24 @@ -from django.conf.urls import url +from django.conf.urls import include, url -from .views import * +from rest_framework import routers +from .views import ( + + # Viewsets + SecretRoleViewSet, + + # Legacy views + RSAKeyGeneratorView, SecretDetailView, SecretListView, + +) + + +router = routers.DefaultRouter() +router.register(r'secret-roles', SecretRoleViewSet) urlpatterns = [ - # Secret roles - url(r'^secret-roles/$', SecretRoleViewSet.as_view({'get': 'list'}), name='secretrole_list'), - url(r'^secret-roles/(?P\d+)/$', SecretRoleViewSet.as_view({'get': 'retrieve'}), name='secretrole_detail'), + url(r'', include(router.urls)), # Secrets url(r'^secrets/$', SecretListView.as_view(), name='secret_list'), diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index 4573278556b..c21e8ccda36 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -1,16 +1,16 @@ -from django.conf.urls import url +from django.conf.urls import include, url -from .views import * +from rest_framework import routers +from views import TenantViewSet, TenantGroupViewSet -urlpatterns = [ - # Tenant groups - url(r'^tenant-groups/$', TenantGroupViewSet.as_view({'get': 'list'}), name='tenantgroup_list'), - url(r'^tenant-groups/(?P\d+)/$', TenantGroupViewSet.as_view({'get': 'retrieve'}), name='tenantgroup_detail'), +router = routers.DefaultRouter() +router.register(r'tenant-groups', TenantGroupViewSet) +router.register(r'tenants', TenantViewSet) + +urlpatterns = [ - # Tenants - url(r'^tenants/$', TenantViewSet.as_view({'get': 'list'}), name='tenant_list'), - url(r'^tenants/(?P\d+)/$', TenantViewSet.as_view({'get': 'retrieve'}), name='tenant_detail'), + url(r'', include(router.urls)), ] From b31c097531bce78d5beacecdbf0e69c701633f35 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 15:36:19 -0500 Subject: [PATCH 004/206] Removed Swagger --- netbox/netbox/settings.py | 6 ------ netbox/netbox/urls.py | 1 - netbox/templates/_base.html | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index f5d7df4d4fd..dc87f6655d1 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -105,7 +105,6 @@ 'debug_toolbar', 'django_tables2', 'rest_framework', - 'rest_framework_swagger', 'circuits', 'dcim', 'ipam', @@ -189,11 +188,6 @@ if LOGIN_REQUIRED: REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',) -# Swagger settings (API docs) -SWAGGER_SETTINGS = { - 'base_path': '{}/{}api/docs'.format(ALLOWED_HOSTS[0], BASE_PATH), -} - # Django debug toolbar INTERNAL_IPS = ( '127.0.0.1', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index bbfdee58ddb..11787adae0a 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -31,7 +31,6 @@ url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), - url(r'^api/docs/', include('rest_framework_swagger.urls')), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), # Error testing diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index 4e63cf337d9..d94081f65d7 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -289,7 +289,7 @@

Maintenance Mode

Docs · - API · + API · Code

From b8ca530c55aca1de9b50b422c9828ab82c7bc34b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 17:18:41 -0500 Subject: [PATCH 005/206] Added an endpoint for CircuitTerminations --- netbox/circuits/api/urls.py | 12 ++++++++---- netbox/circuits/api/views.py | 35 ++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 58c370449f1..0dd2d2a9499 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -5,13 +5,14 @@ from extras.models import GRAPH_TYPE_PROVIDER from extras.api.views import GraphListView -from .views import CircuitViewSet, CircuitTypeViewSet, ProviderViewSet +from . import views router = routers.DefaultRouter() -router.register(r'providers', ProviderViewSet) -router.register(r'circuit-types', CircuitTypeViewSet) -router.register(r'circuits', CircuitViewSet) +router.register(r'providers', views.ProviderViewSet) +router.register(r'circuit-types', views.CircuitTypeViewSet) +router.register(r'circuits', views.CircuitViewSet) +router.register(r'circuit-terminations', views.CircuitTerminationViewSet) urlpatterns = [ @@ -21,4 +22,7 @@ url(r'^providers/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, name='provider_graphs'), + # Circuits + url(r'^circuits/(?P\d+)/terminations/$', views.NestedCircuitTerminationViewSet.as_view({'get': 'list'})), + ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index ec1f1c8ac22..757cc2e26f4 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,6 +1,11 @@ -from rest_framework.viewsets import ModelViewSet +from django.shortcuts import get_object_or_404 -from circuits.models import Provider, CircuitType, Circuit +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, +) +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.filters import CircuitFilter from extras.api.views import CustomFieldModelViewSet @@ -12,9 +17,6 @@ # class ProviderViewSet(CustomFieldModelViewSet): - """ - List and retrieve circuit providers - """ queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer @@ -24,9 +26,6 @@ class ProviderViewSet(CustomFieldModelViewSet): # class CircuitTypeViewSet(ModelViewSet): - """ - List and retrieve circuit types - """ queryset = CircuitType.objects.all() serializer_class = serializers.CircuitTypeSerializer @@ -36,9 +35,6 @@ class CircuitTypeViewSet(ModelViewSet): # class CircuitViewSet(CustomFieldModelViewSet): - """ - List and retrieve circuits - """ queryset = Circuit.objects.select_related('type', 'tenant', 'provider') filter_class = CircuitFilter @@ -46,3 +42,20 @@ def get_serializer_class(self): if self.action == 'retrieve': return serializers.CircuitDetailSerializer return serializers.CircuitSerializer + + +class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): + serializer_class = serializers.CircuitTerminationSerializer + + def get_queryset(self): + circuit = get_object_or_404(Circuit, pk=self.kwargs['pk']) + return CircuitTermination.objects.filter(circuit=circuit).select_related('site', 'interface__device') + + +# +# Circuit Terminations +# + +class CircuitTerminationViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = CircuitTermination.objects.select_related('site', 'interface__device') + serializer_class = serializers.CircuitTerminationSerializer From acfba410ddb64c8fc5dd8a2d983eee0c34cd2cd4 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 17:58:36 -0500 Subject: [PATCH 006/206] Standardized implementation of nested ViewSets --- netbox/circuits/api/views.py | 16 ++-- netbox/dcim/api/urls.py | 74 ++++++++------- netbox/dcim/api/views.py | 175 +++++++++++++---------------------- netbox/ipam/api/urls.py | 23 ++--- netbox/ipam/api/views.py | 27 ------ netbox/secrets/api/urls.py | 18 +--- netbox/secrets/api/views.py | 3 - netbox/tenancy/api/urls.py | 6 +- netbox/tenancy/api/views.py | 6 -- 9 files changed, 128 insertions(+), 220 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 757cc2e26f4..7316029755d 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -44,14 +44,6 @@ def get_serializer_class(self): return serializers.CircuitSerializer -class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.CircuitTerminationSerializer - - def get_queryset(self): - circuit = get_object_or_404(Circuit, pk=self.kwargs['pk']) - return CircuitTermination.objects.filter(circuit=circuit).select_related('site', 'interface__device') - - # # Circuit Terminations # @@ -59,3 +51,11 @@ def get_queryset(self): class CircuitTerminationViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): queryset = CircuitTermination.objects.select_related('site', 'interface__device') serializer_class = serializers.CircuitTerminationSerializer + + +class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): + serializer_class = serializers.CircuitTerminationSerializer + + def get_queryset(self): + circuit = get_object_or_404(Circuit, pk=self.kwargs['pk']) + return CircuitTermination.objects.filter(circuit=circuit).select_related('site', 'interface__device') diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index d31d901c67f..d75ecb083b1 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -5,29 +5,20 @@ from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.api.views import GraphListView, TopologyMapView -from .views import ( - - # Viewsets - ConsolePortViewSet, ConsoleServerPortViewSet, DeviceViewSet, DeviceBayViewSet, DeviceRoleViewSet, DeviceTypeViewSet, - InterfaceViewSet, ManufacturerViewSet, ModuleViewSet, PlatformViewSet, PowerPortViewSet, PowerOutletViewSet, - RackViewSet, RackGroupViewSet, RackRoleViewSet, SiteViewSet, - - # Legacy views - ConsolePortView, InterfaceConnectionView, InterfaceConnectionListView, InterfaceDetailView, PowerPortView, - LLDPNeighborsView, RackUnitListView, RelatedConnectionsView, -) +from . import views router = routers.DefaultRouter() -router.register(r'sites', SiteViewSet) -router.register(r'rack-groups', RackGroupViewSet) -router.register(r'rack-roles', RackRoleViewSet) -router.register(r'racks', RackViewSet) -router.register(r'manufacturers', ManufacturerViewSet) -router.register(r'device-types', DeviceTypeViewSet) -router.register(r'device-roles', DeviceRoleViewSet) -router.register(r'platforms', PlatformViewSet) -router.register(r'devices', DeviceViewSet) +router.register(r'sites', views.SiteViewSet) +router.register(r'rack-groups', views.RackGroupViewSet) +router.register(r'rack-roles', views.RackRoleViewSet) +router.register(r'racks', views.RackViewSet) +router.register(r'manufacturers', views.ManufacturerViewSet) +router.register(r'device-types', views.DeviceTypeViewSet) +router.register(r'device-roles', views.DeviceRoleViewSet) +router.register(r'platforms', views.PlatformViewSet) +router.register(r'devices', views.DeviceViewSet) +router.register(r'interface-connections', views.InterfaceConnectionViewSet) urlpatterns = [ @@ -37,34 +28,47 @@ url(r'^sites/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'), # Racks - url(r'^racks/(?P\d+)/rack-units/$', RackUnitListView.as_view(), name='rack_units'), + url(r'^racks/(?P\d+)/rack-units/$', views.RackUnitListView.as_view(), name='rack_units'), + + # Device types + # TODO: Nested DeviceType components # Devices - url(r'^devices/(?P\d+)/lldp-neighbors/$', LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - url(r'^devices/(?P\d+)/console-ports/$', ConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'), - url(r'^devices/(?P\d+)/console-server-ports/$', ConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'), - url(r'^devices/(?P\d+)/power-ports/$', PowerPortViewSet.as_view({'get': 'list'}), name='device_powerports'), - url(r'^devices/(?P\d+)/power-outlets/$', PowerOutletViewSet.as_view({'get': 'list'}), name='device_poweroutlets'), - url(r'^devices/(?P\d+)/interfaces/$', InterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'), - url(r'^devices/(?P\d+)/device-bays/$', DeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'), - url(r'^devices/(?P\d+)/modules/$', ModuleViewSet.as_view({'get': 'list'}), name='device_modules'), + url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), + url(r'^devices/(?P\d+)/console-ports/$', views.NestedConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'), + url(r'^devices/(?P\d+)/console-server-ports/$', views.NestedConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'), + url(r'^devices/(?P\d+)/power-ports/$', views.NestedPowerPortViewSet.as_view({'get': 'list'}), name='device_powerports'), + url(r'^devices/(?P\d+)/power-outlets/$', views.NestedPowerOutletViewSet.as_view({'get': 'list'}), name='device_poweroutlets'), + url(r'^devices/(?P\d+)/interfaces/$', views.NestedInterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'), + url(r'^devices/(?P\d+)/device-bays/$', views.NestedDeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'), + url(r'^devices/(?P\d+)/modules/$', views.NestedModuleViewSet.as_view({'get': 'list'}), name='device_modules'), # TODO: Services # Console ports - url(r'^console-ports/(?P\d+)/$', ConsolePortView.as_view(), name='consoleport'), + url(r'^console-ports/(?P\d+)/$', views.ConsolePortViewSet.as_view({'get': 'retrieve'}), name='consoleport'), + + # Console server ports + url(r'^console-server-ports/(?P\d+)/$', views.ConsoleServerPortViewSet.as_view({'get': 'retrieve'}), name='consoleserverport'), # Power ports - url(r'^power-ports/(?P\d+)/$', PowerPortView.as_view(), name='powerport'), + url(r'^power-ports/(?P\d+)/$', views.PowerPortViewSet.as_view({'get': 'retrieve'}), name='powerport'), + + # Power outlets + url(r'^power-outlets/(?P\d+)/$', views.PowerOutletViewSet.as_view({'get': 'retrieve'}), name='poweroutlet'), # Interfaces - url(r'^interfaces/(?P\d+)/$', InterfaceDetailView.as_view(), name='interface_detail'), + url(r'^interfaces/(?P\d+)/$', views.InterfaceViewSet.as_view({'get': 'retrieve'}), name='interface'), url(r'^interfaces/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, name='interface_graphs'), - url(r'^interface-connections/$', InterfaceConnectionListView.as_view(), name='interfaceconnection_list'), - url(r'^interface-connections/(?P\d+)/$', InterfaceConnectionView.as_view(), name='interfaceconnection_detail'), + + # Device bays + url(r'^device-bays/(?P\d+)/$', views.DeviceBayViewSet.as_view({'get': 'retrieve'}), name='devicebay'), + + # Modules + url(r'^modules/(?P\d+)/$', views.ModuleViewSet.as_view({'get': 'retrieve'}), name='module'), # Miscellaneous - url(r'^related-connections/$', RelatedConnectionsView.as_view(), name='related_connections'), + url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), url(r'^topology-maps/(?P[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'), ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 01eb58dab01..2e8a4d1d0f8 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,9 +1,10 @@ -from rest_framework import generics -from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, +) from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import GenericViewSet, ModelViewSet from django.conf import settings from django.contrib.contenttypes.models import ContentType @@ -11,8 +12,8 @@ from django.shortcuts import get_object_or_404 from dcim.models import ( - ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, IFACE_FF_VIRTUAL, Interface, - InterfaceConnection, Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, + ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection, + Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from dcim import filters from extras.api.views import CustomFieldModelViewSet @@ -27,9 +28,6 @@ # class SiteViewSet(CustomFieldModelViewSet): - """ - List and retrieve sites - """ queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer @@ -39,9 +37,6 @@ class SiteViewSet(CustomFieldModelViewSet): # class RackGroupViewSet(ModelViewSet): - """ - List and retrieve rack groups - """ queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer filter_class = filters.RackGroupFilter @@ -52,9 +47,6 @@ class RackGroupViewSet(ModelViewSet): # class RackRoleViewSet(ModelViewSet): - """ - List and retrieve rack roles - """ queryset = RackRole.objects.all() serializer_class = serializers.RackRoleSerializer @@ -64,9 +56,6 @@ class RackRoleViewSet(ModelViewSet): # class RackViewSet(CustomFieldModelViewSet): - """ - List and retrieve racks - """ queryset = Rack.objects.select_related('site', 'group__site', 'tenant') filter_class = filters.RackFilter @@ -106,9 +95,6 @@ def get(self, request, pk): # class ManufacturerViewSet(ModelViewSet): - """ - List and retrieve manufacturers - """ queryset = Manufacturer.objects.all() serializer_class = serializers.ManufacturerSerializer @@ -118,9 +104,6 @@ class ManufacturerViewSet(ModelViewSet): # class DeviceTypeViewSet(CustomFieldModelViewSet): - """ - List and retrieve device types - """ queryset = DeviceType.objects.select_related('manufacturer') filter_class = filters.DeviceTypeFilter @@ -131,13 +114,10 @@ def get_serializer_class(self): # -# Device roles +# Device Roles # class DeviceRoleViewSet(ModelViewSet): - """ - List and retrieve device roles - """ queryset = DeviceRole.objects.all() serializer_class = serializers.DeviceRoleSerializer @@ -147,9 +127,6 @@ class DeviceRoleViewSet(ModelViewSet): # class PlatformViewSet(ModelViewSet): - """ - List and retrieve platforms - """ queryset = Platform.objects.all() serializer_class = serializers.PlatformSerializer @@ -159,9 +136,6 @@ class PlatformViewSet(ModelViewSet): # class DeviceViewSet(CustomFieldModelViewSet): - """ - List and retrieve devices - """ queryset = Device.objects.select_related( 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay', ).prefetch_related( @@ -173,13 +147,15 @@ class DeviceViewSet(CustomFieldModelViewSet): # -# Console ports +# Console Ports # -class ConsolePortViewSet(ModelViewSet): - """ - List and retrieve console ports (by device) - """ +class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = ConsolePort.objects.select_related('cs_port') + serializer_class = serializers.ConsolePortSerializer + + +class NestedConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.ConsolePortSerializer def get_queryset(self): @@ -187,20 +163,16 @@ def get_queryset(self): return ConsolePort.objects.filter(device=device).select_related('cs_port') -class ConsolePortView(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [DjangoModelPermissionsOrAnonReadOnly] - serializer_class = serializers.ConsolePortSerializer - queryset = ConsolePort.objects.all() - - # -# Console server ports +# Console Server Ports # -class ConsoleServerPortViewSet(ModelViewSet): - """ - List and retrieve console server ports (by device) - """ +class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = ConsoleServerPort.objects.select_related('connected_console') + serializer_class = serializers.ConsoleServerPortSerializer + + +class NestedConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.ConsoleServerPortSerializer def get_queryset(self): @@ -209,13 +181,15 @@ def get_queryset(self): # -# Power ports +# Power Ports # -class PowerPortViewSet(ModelViewSet): - """ - List and retrieve power ports (by device) - """ +class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = PowerPort.objects.select_related('power_outlet') + serializer_class = serializers.PowerPortSerializer + + +class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.PowerPortSerializer def get_queryset(self): @@ -223,20 +197,16 @@ def get_queryset(self): return PowerPort.objects.filter(device=device).select_related('power_outlet') -class PowerPortView(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [DjangoModelPermissionsOrAnonReadOnly] - serializer_class = serializers.PowerPortSerializer - queryset = PowerPort.objects.all() - - # -# Power outlets +# Power Outlets # -class PowerOutletViewSet(ModelViewSet): - """ - List and retrieve power outlets (by device) - """ +class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = PowerOutlet.objects.select_related('connected_port') + serializer_class = serializers.PowerOutletSerializer + + +class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.PowerOutletSerializer def get_queryset(self): @@ -248,61 +218,31 @@ def get_queryset(self): # Interfaces # -class InterfaceViewSet(ModelViewSet): - """ - List and retrieve interfaces (by device) - """ +class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = Interface.objects.select_related('device') + serializer_class = serializers.InterfaceDetailSerializer + + +class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.InterfaceSerializer filter_class = filters.InterfaceFilter def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - queryset = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ + return Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') - # Filter by type (physical or virtual) - iface_type = self.request.query_params.get('type') - if iface_type == 'physical': - queryset = queryset.exclude(form_factor=IFACE_FF_VIRTUAL) - elif iface_type == 'virtual': - queryset = queryset.filter(form_factor=IFACE_FF_VIRTUAL) - elif iface_type is not None: - queryset = queryset.empty() - - return queryset - - -class InterfaceDetailView(generics.RetrieveAPIView): - """ - Retrieve a single interface - """ - queryset = Interface.objects.select_related('device') - serializer_class = serializers.InterfaceDetailSerializer - - -class InterfaceConnectionView(generics.RetrieveUpdateDestroyAPIView): - permission_classes = [DjangoModelPermissionsOrAnonReadOnly] - serializer_class = serializers.InterfaceConnectionSerializer - queryset = InterfaceConnection.objects.all() - - -class InterfaceConnectionListView(generics.ListAPIView): - """ - Retrieve a list of all interface connections - """ - serializer_class = serializers.InterfaceConnectionSerializer - queryset = InterfaceConnection.objects.all() - # # Device bays # -class DeviceBayViewSet(ModelViewSet): - """ - List and retrieve device bays (by device) - """ +class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = DeviceBay.objects.select_related('installed_device') + serializer_class = serializers.DeviceBaySerializer + + +class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceBayNestedSerializer def get_queryset(self): @@ -314,10 +254,12 @@ def get_queryset(self): # Modules # -class ModuleViewSet(ModelViewSet): - """ - List and retrieve modules (by device) - """ +class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): + queryset = Module.objects.select_related('device', 'manufacturer') + serializer_class = serializers.ModuleSerializer + + +class NestedModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.ModuleSerializer def get_queryset(self): @@ -325,6 +267,15 @@ def get_queryset(self): return Module.objects.filter(device=device).select_related('device', 'manufacturer') +# +# Interface connections +# + +class InterfaceConnectionViewSet(ModelViewSet): + queryset = InterfaceConnection.objects.all() + serializer_class = serializers.InterfaceConnectionSerializer + + # # Live queries # diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index b74739d0d6b..24c97b341f8 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -2,22 +2,19 @@ from rest_framework import routers -from .views import ( - AggregateViewSet, IPAddressViewSet, PrefixViewSet, RIRViewSet, RoleViewSet, ServiceViewSet, VLANViewSet, - VLANGroupViewSet, VRFViewSet, -) +from . import views router = routers.DefaultRouter() -router.register(r'vrfs', VRFViewSet) -router.register(r'rirs', RIRViewSet) -router.register(r'aggregates', AggregateViewSet) -router.register(r'roles', RoleViewSet) -router.register(r'prefixes', PrefixViewSet) -router.register(r'ip-addresses', IPAddressViewSet) -router.register(r'vlan-groups', VLANGroupViewSet) -router.register(r'vlans', VLANViewSet) -router.register(r'services', ServiceViewSet) +router.register(r'vrfs', views.VRFViewSet) +router.register(r'rirs', views.RIRViewSet) +router.register(r'aggregates', views.AggregateViewSet) +router.register(r'roles', views.RoleViewSet) +router.register(r'prefixes', views.PrefixViewSet) +router.register(r'ip-addresses', views.IPAddressViewSet) +router.register(r'vlan-groups', views.VLANGroupViewSet) +router.register(r'vlans', views.VLANViewSet) +router.register(r'services', views.ServiceViewSet) urlpatterns = [ diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 87200fe3a35..80ff10c6f52 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -12,9 +12,6 @@ # class VRFViewSet(CustomFieldModelViewSet): - """ - List and retrieve VRFs - """ queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter @@ -25,9 +22,6 @@ class VRFViewSet(CustomFieldModelViewSet): # class RoleViewSet(ModelViewSet): - """ - List and retrieve prefix/VLAN roles - """ queryset = Role.objects.all() serializer_class = serializers.RoleSerializer @@ -37,9 +31,6 @@ class RoleViewSet(ModelViewSet): # class RIRViewSet(ModelViewSet): - """ - List and retrieve RIRs - """ queryset = RIR.objects.all() serializer_class = serializers.RIRSerializer @@ -49,9 +40,6 @@ class RIRViewSet(ModelViewSet): # class AggregateViewSet(CustomFieldModelViewSet): - """ - List and retrieve aggregates - """ queryset = Aggregate.objects.select_related('rir') serializer_class = serializers.AggregateSerializer filter_class = filters.AggregateFilter @@ -62,9 +50,6 @@ class AggregateViewSet(CustomFieldModelViewSet): # class PrefixViewSet(CustomFieldModelViewSet): - """ - List and retrieve prefixes - """ queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter @@ -75,9 +60,6 @@ class PrefixViewSet(CustomFieldModelViewSet): # class IPAddressViewSet(CustomFieldModelViewSet): - """ - List and retrieve IP addresses - """ queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter @@ -88,9 +70,6 @@ class IPAddressViewSet(CustomFieldModelViewSet): # class VLANGroupViewSet(ModelViewSet): - """ - List and retrieve VLAN groups - """ queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer filter_class = filters.VLANGroupFilter @@ -101,9 +80,6 @@ class VLANGroupViewSet(ModelViewSet): # class VLANViewSet(CustomFieldModelViewSet): - """ - List and retrieve VLANs - """ queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter @@ -114,9 +90,6 @@ class VLANViewSet(CustomFieldModelViewSet): # class ServiceViewSet(ModelViewSet): - """ - List and retrieve services - """ queryset = Service.objects.select_related('device').prefetch_related('ipaddresses') serializer_class = serializers.ServiceSerializer filter_class = filters.ServiceFilter diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index 14579dd3fbb..a030b481eb2 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -2,29 +2,21 @@ from rest_framework import routers -from .views import ( - - # Viewsets - SecretRoleViewSet, - - # Legacy views - RSAKeyGeneratorView, SecretDetailView, SecretListView, - -) +from . import views router = routers.DefaultRouter() -router.register(r'secret-roles', SecretRoleViewSet) +router.register(r'secret-roles', views.SecretRoleViewSet) urlpatterns = [ url(r'', include(router.urls)), # Secrets - url(r'^secrets/$', SecretListView.as_view(), name='secret_list'), - url(r'^secrets/(?P\d+)/$', SecretDetailView.as_view(), name='secret_detail'), + url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), + url(r'^secrets/(?P\d+)/$', views.SecretDetailView.as_view(), name='secret_detail'), # Miscellaneous - url(r'^generate-keys/$', RSAKeyGeneratorView.as_view(), name='generate_keys'), + url(r'^generate-keys/$', views.RSAKeyGeneratorView.as_view(), name='generate_keys'), ] diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 10e95d51b4c..e2ccbdb075b 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -28,9 +28,6 @@ # class SecretRoleViewSet(ModelViewSet): - """ - List and retrieve secret roles - """ queryset = SecretRole.objects.all() serializer_class = serializers.SecretRoleSerializer permission_classes = [IsAuthenticated] diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index c21e8ccda36..35e11cd6b20 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -2,12 +2,12 @@ from rest_framework import routers -from views import TenantViewSet, TenantGroupViewSet +from . import views router = routers.DefaultRouter() -router.register(r'tenant-groups', TenantGroupViewSet) -router.register(r'tenants', TenantViewSet) +router.register(r'tenant-groups', views.TenantGroupViewSet) +router.register(r'tenants', views.TenantViewSet) urlpatterns = [ diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 6cfeed2ef03..17d0e79ef2e 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -12,9 +12,6 @@ # class TenantGroupViewSet(ModelViewSet): - """ - List and retrieve tenant groups - """ queryset = TenantGroup.objects.all() serializer_class = serializers.TenantGroupSerializer @@ -24,9 +21,6 @@ class TenantGroupViewSet(ModelViewSet): # class TenantViewSet(CustomFieldModelViewSet): - """ - List and retrieve tenants - """ queryset = Tenant.objects.select_related('group') serializer_class = serializers.TenantSerializer filter_class = TenantFilter From ddc2c8d1105f991c0d4a0b20366e2bbde5eb47e8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 26 Jan 2017 22:37:17 -0500 Subject: [PATCH 007/206] Cleaned up device component nested serializers --- netbox/circuits/api/serializers.py | 4 +- netbox/dcim/api/serializers.py | 78 ++++++++++++------------------ netbox/dcim/api/views.py | 19 +++----- netbox/ipam/api/serializers.py | 4 +- 4 files changed, 42 insertions(+), 63 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ba1ba80a98f..4cdfcf91ed1 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import SiteNestedSerializer, InterfaceNestedSerializer +from dcim.api.serializers import SiteNestedSerializer, NestedInterfaceSerializer from extras.api.serializers import CustomFieldSerializer from tenancy.api.serializers import TenantNestedSerializer @@ -47,7 +47,7 @@ class Meta(CircuitTypeSerializer.Meta): class CircuitTerminationSerializer(serializers.ModelSerializer): site = SiteNestedSerializer() - interface = InterfaceNestedSerializer() + interface = NestedInterfaceSerializer() class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index f81f299af1e..64b16999aa7 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -191,20 +191,6 @@ class Meta: fields = ['id', 'name', 'form_factor', 'mgmt_only'] -class DeviceTypeDetailSerializer(DeviceTypeSerializer): - console_port_templates = ConsolePortTemplateNestedSerializer(many=True, read_only=True) - cs_port_templates = ConsoleServerPortTemplateNestedSerializer(many=True, read_only=True) - power_port_templates = PowerPortTemplateNestedSerializer(many=True, read_only=True) - power_outlet_templates = PowerPortTemplateNestedSerializer(many=True, read_only=True) - interface_templates = InterfaceTemplateNestedSerializer(many=True, read_only=True) - - class Meta(DeviceTypeSerializer.Meta): - fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', - 'comments', 'custom_fields', 'console_port_templates', 'cs_port_templates', 'power_port_templates', - 'power_outlet_templates', 'interface_templates'] - - # # Device roles # @@ -302,10 +288,11 @@ class Meta: fields = ['id', 'device', 'name', 'connected_console'] -class ConsoleServerPortNestedSerializer(ConsoleServerPortSerializer): +class NestedConsoleServerPortSerializer(ConsoleServerPortSerializer): - class Meta(ConsoleServerPortSerializer.Meta): - fields = ['id', 'device', 'name'] + class Meta: + model = ConsoleServerPort + fields = ['id', 'name', 'connected_console'] # @@ -314,17 +301,18 @@ class Meta(ConsoleServerPortSerializer.Meta): class ConsolePortSerializer(serializers.ModelSerializer): device = DeviceNestedSerializer() - cs_port = ConsoleServerPortNestedSerializer() + cs_port = ConsoleServerPortSerializer() class Meta: model = ConsolePort fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] -class ConsolePortNestedSerializer(ConsolePortSerializer): +class NestedConsolePortSerializer(ConsolePortSerializer): - class Meta(ConsolePortSerializer.Meta): - fields = ['id', 'device', 'name'] + class Meta: + model = ConsolePort + fields = ['id', 'name', 'cs_port', 'connection_status'] # @@ -339,10 +327,11 @@ class Meta: fields = ['id', 'device', 'name', 'connected_port'] -class PowerOutletNestedSerializer(PowerOutletSerializer): +class NestedPowerOutletSerializer(PowerOutletSerializer): - class Meta(PowerOutletSerializer.Meta): - fields = ['id', 'device', 'name'] + class Meta: + model = PowerOutlet + fields = ['id', 'name', 'connected_port'] # @@ -351,17 +340,18 @@ class Meta(PowerOutletSerializer.Meta): class PowerPortSerializer(serializers.ModelSerializer): device = DeviceNestedSerializer() - power_outlet = PowerOutletNestedSerializer() + power_outlet = PowerOutletSerializer() class Meta: model = PowerPort fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] -class PowerPortNestedSerializer(PowerPortSerializer): +class NestedPowerPortSerializer(PowerPortSerializer): - class Meta(PowerPortSerializer.Meta): - fields = ['id', 'device', 'name'] + class Meta: + model = PowerPort + fields = ['id', 'name', 'power_outlet', 'connection_status'] # @@ -377,15 +367,14 @@ class Meta: fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] -class InterfaceNestedSerializer(InterfaceSerializer): - form_factor = serializers.ReadOnlyField(source='get_form_factor_display') +class NestedInterfaceSerializer(InterfaceSerializer): - class Meta(InterfaceSerializer.Meta): - fields = ['id', 'device', 'name'] + class Meta: + model = Interface + fields = ['id', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] class InterfaceDetailSerializer(InterfaceSerializer): - connected_interface = InterfaceSerializer() class Meta(InterfaceSerializer.Meta): fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', @@ -398,26 +387,20 @@ class Meta(InterfaceSerializer.Meta): class DeviceBaySerializer(serializers.ModelSerializer): device = DeviceNestedSerializer() + installed_device = DeviceNestedSerializer() class Meta: model = DeviceBay - fields = ['id', 'device', 'name'] + fields = ['id', 'device', 'name', 'installed_device'] -class DeviceBayNestedSerializer(DeviceBaySerializer): - installed_device = DeviceNestedSerializer() +class NestedDeviceBaySerializer(DeviceBaySerializer): - class Meta(DeviceBaySerializer.Meta): + class Meta: + model = DeviceBay fields = ['id', 'name', 'installed_device'] -class DeviceBayDetailSerializer(DeviceBaySerializer): - installed_device = DeviceNestedSerializer() - - class Meta(DeviceBaySerializer.Meta): - fields = ['id', 'device', 'name', 'installed_device'] - - # # Modules # @@ -431,10 +414,11 @@ class Meta: fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] -class ModuleNestedSerializer(ModuleSerializer): +class NestedModuleSerializer(ModuleSerializer): - class Meta(ModuleSerializer.Meta): - fields = ['id', 'device', 'parent', 'name'] + class Meta: + model = Module + fields = ['id', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] # diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 2e8a4d1d0f8..8d4eb13c30b 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -107,11 +107,6 @@ class DeviceTypeViewSet(CustomFieldModelViewSet): queryset = DeviceType.objects.select_related('manufacturer') filter_class = filters.DeviceTypeFilter - def get_serializer_class(self): - if self.action == 'retrieve': - return serializers.DeviceTypeDetailSerializer - return serializers.DeviceTypeSerializer - # # Device Roles @@ -156,7 +151,7 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class NestedConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.ConsolePortSerializer + serializer_class = serializers.NestedConsolePortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -173,7 +168,7 @@ class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyMode class NestedConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.ConsoleServerPortSerializer + serializer_class = serializers.NestedConsoleServerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -190,7 +185,7 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.PowerPortSerializer + serializer_class = serializers.NestedPowerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -207,7 +202,7 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.PowerOutletSerializer + serializer_class = serializers.NestedPowerOutletSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -224,7 +219,7 @@ class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.InterfaceSerializer + serializer_class = serializers.NestedInterfaceSerializer filter_class = filters.InterfaceFilter def get_queryset(self): @@ -243,7 +238,7 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceBayNestedSerializer + serializer_class = serializers.NestedDeviceBaySerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -260,7 +255,7 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Gen class NestedModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.ModuleSerializer + serializer_class = serializers.NestedModuleSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index e3f902605c6..4b69711cfdf 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from dcim.api.serializers import DeviceNestedSerializer, InterfaceNestedSerializer, SiteNestedSerializer +from dcim.api.serializers import DeviceNestedSerializer, NestedInterfaceSerializer, SiteNestedSerializer from extras.api.serializers import CustomFieldSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import TenantNestedSerializer @@ -155,7 +155,7 @@ class Meta(PrefixSerializer.Meta): class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer): vrf = VRFTenantSerializer() tenant = TenantNestedSerializer() - interface = InterfaceNestedSerializer() + interface = NestedInterfaceSerializer() class Meta: model = IPAddress From fa900d5dbb01215a891a0c65f29dc46d29cdee71 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2017 12:22:29 -0500 Subject: [PATCH 008/206] Converted nested serializers to HyperlinkedModelSerializer --- netbox/circuits/api/serializers.py | 55 ++++---- netbox/dcim/api/serializers.py | 208 ++++++++++++++++------------- netbox/dcim/api/urls.py | 28 ++-- netbox/dcim/api/views.py | 31 +++-- netbox/extras/api/views.py | 11 +- netbox/ipam/api/serializers.py | 129 +++++++++--------- netbox/netbox/urls.py | 12 +- netbox/secrets/api/serializers.py | 28 +--- netbox/tenancy/api/serializers.py | 16 ++- 9 files changed, 273 insertions(+), 245 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 4cdfcf91ed1..d5f229a8284 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,9 +1,9 @@ from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import SiteNestedSerializer, NestedInterfaceSerializer +from dcim.api.serializers import NestedSiteSerializer, ChildInterfaceSerializer from extras.api.serializers import CustomFieldSerializer -from tenancy.api.serializers import TenantNestedSerializer +from tenancy.api.serializers import NestedTenantSerializer # @@ -14,14 +14,17 @@ class ProviderSerializer(CustomFieldSerializer, serializers.ModelSerializer): class Meta: model = Provider - fields = ['id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - 'custom_fields'] + fields = [ + 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + 'custom_fields', + ] -class ProviderNestedSerializer(ProviderSerializer): +class NestedProviderSerializer(serializers.HyperlinkedModelSerializer): - class Meta(ProviderSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = Provider + fields = ['id', 'url', 'name', 'slug'] # @@ -35,10 +38,11 @@ class Meta: fields = ['id', 'name', 'slug'] -class CircuitTypeNestedSerializer(CircuitTypeSerializer): +class NestedCircuitTypeSerializer(serializers.HyperlinkedModelSerializer): - class Meta(CircuitTypeSerializer.Meta): - pass + class Meta: + model = CircuitType + fields = ['id', 'url', 'name', 'slug'] # @@ -46,8 +50,8 @@ class Meta(CircuitTypeSerializer.Meta): # class CircuitTerminationSerializer(serializers.ModelSerializer): - site = SiteNestedSerializer() - interface = NestedInterfaceSerializer() + site = NestedSiteSerializer() + interface = ChildInterfaceSerializer() class Meta: model = CircuitTermination @@ -58,27 +62,32 @@ class Meta: # Circuits # - class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer): - provider = ProviderNestedSerializer() - type = CircuitTypeNestedSerializer() - tenant = TenantNestedSerializer() + provider = NestedProviderSerializer() + type = NestedCircuitTypeSerializer() + tenant = NestedTenantSerializer() class Meta: model = Circuit - fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - 'custom_fields'] + fields = [ + 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + 'custom_fields', + ] -class CircuitNestedSerializer(CircuitSerializer): +class NestedCircuitSerializer(serializers.HyperlinkedModelSerializer): - class Meta(CircuitSerializer.Meta): - fields = ['id', 'cid'] + class Meta: + model = Circuit + fields = ['id', 'url', 'cid'] +# TODO: Delete this class CircuitDetailSerializer(CircuitSerializer): terminations = CircuitTerminationSerializer(many=True) class Meta(CircuitSerializer.Meta): - fields = ['id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - 'terminations', 'custom_fields'] + fields = [ + 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', + 'terminations', 'custom_fields', + ] diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 64b16999aa7..c6e68b9cb08 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -8,7 +8,7 @@ SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, ) from extras.api.serializers import CustomFieldSerializer -from tenancy.api.serializers import TenantNestedSerializer +from tenancy.api.serializers import NestedTenantSerializer # @@ -16,19 +16,22 @@ # class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer): - tenant = TenantNestedSerializer() + tenant = NestedTenantSerializer() class Meta: model = Site - fields = ['id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', - 'contact_name', 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes', - 'count_vlans', 'count_racks', 'count_devices', 'count_circuits'] + fields = [ + 'id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', + 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes', 'count_vlans', + 'count_racks', 'count_devices', 'count_circuits', + ] -class SiteNestedSerializer(SiteSerializer): +class NestedSiteSerializer(serializers.HyperlinkedModelSerializer): - class Meta(SiteSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = Site + fields = ['id', 'url', 'name', 'slug'] # @@ -36,17 +39,18 @@ class Meta(SiteSerializer.Meta): # class RackGroupSerializer(serializers.ModelSerializer): - site = SiteNestedSerializer() + site = NestedSiteSerializer() class Meta: model = RackGroup fields = ['id', 'name', 'slug', 'site'] -class RackGroupNestedSerializer(RackGroupSerializer): +class NestedRackGroupSerializer(serializers.HyperlinkedModelSerializer): - class Meta(SiteSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = RackGroup + fields = ['id', 'url', 'name', 'slug'] # @@ -60,10 +64,11 @@ class Meta: fields = ['id', 'name', 'slug', 'color'] -class RackRoleNestedSerializer(RackRoleSerializer): +class NestedRackRoleSerializer(serializers.HyperlinkedModelSerializer): - class Meta(RackRoleSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = RackRole + fields = ['id', 'url', 'name', 'slug'] # @@ -72,21 +77,24 @@ class Meta(RackRoleSerializer.Meta): class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer): - site = SiteNestedSerializer() - group = RackGroupNestedSerializer() - tenant = TenantNestedSerializer() - role = RackRoleNestedSerializer() + site = NestedSiteSerializer() + group = NestedRackGroupSerializer() + tenant = NestedTenantSerializer() + role = NestedRackRoleSerializer() class Meta: model = Rack - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', - 'u_height', 'desc_units', 'comments', 'custom_fields'] + fields = [ + 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', + 'desc_units', 'comments', 'custom_fields', + ] -class RackNestedSerializer(RackSerializer): +class NestedRackSerializer(serializers.HyperlinkedModelSerializer): - class Meta(RackSerializer.Meta): - fields = ['id', 'name', 'facility_id', 'display_name'] + class Meta: + model = Rack + fields = ['id', 'url', 'name', 'display_name'] class RackDetailSerializer(RackSerializer): @@ -94,19 +102,21 @@ class RackDetailSerializer(RackSerializer): rear_units = serializers.SerializerMethodField() class Meta(RackSerializer.Meta): - fields = ['id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', - 'u_height', 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units'] + fields = [ + 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', + 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units', + ] def get_front_units(self, obj): units = obj.get_rack_units(face=RACK_FACE_FRONT) for u in units: - u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None + u['device'] = NestedDeviceSerializer(u['device']).data if u['device'] else None return units def get_rear_units(self, obj): units = obj.get_rack_units(face=RACK_FACE_REAR) for u in units: - u['device'] = DeviceNestedSerializer(u['device']).data if u['device'] else None + u['device'] = NestedDeviceSerializer(u['device']).data if u['device'] else None return units @@ -121,10 +131,11 @@ class Meta: fields = ['id', 'name', 'slug'] -class ManufacturerNestedSerializer(ManufacturerSerializer): +class NestedManufacturerSerializer(serializers.HyperlinkedModelSerializer): - class Meta(ManufacturerSerializer.Meta): - pass + class Meta: + model = Manufacturer + fields = ['id', 'url', 'name', 'slug'] # @@ -132,15 +143,17 @@ class Meta(ManufacturerSerializer.Meta): # class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): - manufacturer = ManufacturerNestedSerializer() + manufacturer = NestedManufacturerSerializer() subdevice_role = serializers.SerializerMethodField() instance_count = serializers.IntegerField(source='instances.count', read_only=True) class Meta: model = DeviceType - fields = ['id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', - 'interface_ordering', 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', - 'comments', 'custom_fields', 'instance_count'] + fields = [ + 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', + 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', + 'instance_count', + ] def get_subdevice_role(self, obj): return { @@ -150,47 +163,55 @@ def get_subdevice_role(self, obj): }[obj.subdevice_role] -class DeviceTypeNestedSerializer(DeviceTypeSerializer): +class NestedDeviceTypeSerializer(serializers.HyperlinkedModelSerializer): - class Meta(DeviceTypeSerializer.Meta): - fields = ['id', 'manufacturer', 'model', 'slug'] + class Meta: + model = DeviceType + fields = ['id', 'url', 'manufacturer', 'model', 'slug'] -class ConsolePortTemplateNestedSerializer(serializers.ModelSerializer): +class ConsolePortTemplateSerializer(serializers.ModelSerializer): class Meta: model = ConsolePortTemplate fields = ['id', 'name'] -class ConsoleServerPortTemplateNestedSerializer(serializers.ModelSerializer): +class ConsoleServerPortTemplateSerializer(serializers.ModelSerializer): class Meta: model = ConsoleServerPortTemplate fields = ['id', 'name'] -class PowerPortTemplateNestedSerializer(serializers.ModelSerializer): +class PowerPortTemplateSerializer(serializers.ModelSerializer): class Meta: model = PowerPortTemplate fields = ['id', 'name'] -class PowerOutletTemplateNestedSerializer(serializers.ModelSerializer): +class PowerOutletTemplateSerializer(serializers.ModelSerializer): class Meta: model = PowerOutletTemplate fields = ['id', 'name'] -class InterfaceTemplateNestedSerializer(serializers.ModelSerializer): +class InterfaceTemplateSerializer(serializers.ModelSerializer): class Meta: model = InterfaceTemplate fields = ['id', 'name', 'form_factor', 'mgmt_only'] +class DeviceBayTemplateSerializer(serializers.ModelSerializer): + + class Meta: + model = DeviceBay + fields = ['id', 'name',] + + # # Device roles # @@ -202,10 +223,11 @@ class Meta: fields = ['id', 'name', 'slug', 'color'] -class DeviceRoleNestedSerializer(DeviceRoleSerializer): +class NestedDeviceRoleSerializer(serializers.HyperlinkedModelSerializer): - class Meta(DeviceRoleSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = DeviceRole + fields = ['id', 'url', 'name', 'slug'] # @@ -219,40 +241,43 @@ class Meta: fields = ['id', 'name', 'slug', 'rpc_client'] -class PlatformNestedSerializer(PlatformSerializer): +class NestedPlatformSerializer(serializers.HyperlinkedModelSerializer): - class Meta(PlatformSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = Platform + fields = ['id', 'url', 'name', 'slug'] # # Devices # -# Cannot import ipam.api.IPAddressNestedSerializer due to circular dependency -class DeviceIPAddressNestedSerializer(serializers.ModelSerializer): +# Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency +class DeviceIPAddressSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = IPAddress - fields = ['id', 'family', 'address'] + fields = ['id', 'url', 'family', 'address'] class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer): - device_type = DeviceTypeNestedSerializer() - device_role = DeviceRoleNestedSerializer() - tenant = TenantNestedSerializer() - platform = PlatformNestedSerializer() - rack = RackNestedSerializer() - primary_ip = DeviceIPAddressNestedSerializer() - primary_ip4 = DeviceIPAddressNestedSerializer() - primary_ip6 = DeviceIPAddressNestedSerializer() + device_type = NestedDeviceTypeSerializer() + device_role = NestedDeviceRoleSerializer() + tenant = NestedTenantSerializer() + platform = NestedPlatformSerializer() + rack = NestedRackSerializer() + primary_ip = DeviceIPAddressSerializer() + primary_ip4 = DeviceIPAddressSerializer() + primary_ip6 = DeviceIPAddressSerializer() parent_device = serializers.SerializerMethodField() class Meta: model = Device - fields = ['id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'comments', 'custom_fields'] + fields = [ + 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', + 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', + 'comments', 'custom_fields', + ] def get_parent_device(self, obj): try: @@ -269,11 +294,11 @@ def get_parent_device(self, obj): } -class DeviceNestedSerializer(serializers.ModelSerializer): +class NestedDeviceSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Device - fields = ['id', 'name', 'display_name'] + fields = ['id', 'url', 'name', 'display_name'] # @@ -281,18 +306,18 @@ class Meta: # class ConsoleServerPortSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() + device = NestedDeviceSerializer() class Meta: model = ConsoleServerPort fields = ['id', 'device', 'name', 'connected_console'] -class NestedConsoleServerPortSerializer(ConsoleServerPortSerializer): +class ChildConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsoleServerPort - fields = ['id', 'name', 'connected_console'] + fields = ['id', 'url', 'name', 'connected_console'] # @@ -300,7 +325,7 @@ class Meta: # class ConsolePortSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() + device = NestedDeviceSerializer() cs_port = ConsoleServerPortSerializer() class Meta: @@ -308,11 +333,11 @@ class Meta: fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] -class NestedConsolePortSerializer(ConsolePortSerializer): +class ChildConsolePortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsolePort - fields = ['id', 'name', 'cs_port', 'connection_status'] + fields = ['id', 'url', 'name', 'cs_port', 'connection_status'] # @@ -320,18 +345,18 @@ class Meta: # class PowerOutletSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() + device = NestedDeviceSerializer() class Meta: model = PowerOutlet fields = ['id', 'device', 'name', 'connected_port'] -class NestedPowerOutletSerializer(PowerOutletSerializer): +class ChildPowerOutletSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerOutlet - fields = ['id', 'name', 'connected_port'] + fields = ['id', 'url', 'name', 'connected_port'] # @@ -339,7 +364,7 @@ class Meta: # class PowerPortSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() + device = NestedDeviceSerializer() power_outlet = PowerOutletSerializer() class Meta: @@ -347,11 +372,11 @@ class Meta: fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] -class NestedPowerPortSerializer(PowerPortSerializer): +class ChildPowerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerPort - fields = ['id', 'name', 'power_outlet', 'connection_status'] + fields = ['id', 'url', 'name', 'power_outlet', 'connection_status'] # @@ -359,7 +384,7 @@ class Meta: # class InterfaceSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() + device = NestedDeviceSerializer() form_factor = serializers.ReadOnlyField(source='get_form_factor_display') class Meta: @@ -367,18 +392,21 @@ class Meta: fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] -class NestedInterfaceSerializer(InterfaceSerializer): +class ChildInterfaceSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Interface - fields = ['id', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] + fields = ['id', 'url', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] +# TODO: Remove this class InterfaceDetailSerializer(InterfaceSerializer): class Meta(InterfaceSerializer.Meta): - fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', - 'connected_interface'] + fields = [ + 'id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', + 'connected_interface', + ] # @@ -386,19 +414,19 @@ class Meta(InterfaceSerializer.Meta): # class DeviceBaySerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() - installed_device = DeviceNestedSerializer() + device = NestedDeviceSerializer() + installed_device = NestedDeviceSerializer() class Meta: model = DeviceBay fields = ['id', 'device', 'name', 'installed_device'] -class NestedDeviceBaySerializer(DeviceBaySerializer): +class ChildDeviceBaySerializer(serializers.HyperlinkedModelSerializer): class Meta: model = DeviceBay - fields = ['id', 'name', 'installed_device'] + fields = ['id', 'url', 'name', 'installed_device'] # @@ -406,19 +434,19 @@ class Meta: # class ModuleSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() - manufacturer = ManufacturerNestedSerializer() + device = NestedDeviceSerializer() + manufacturer = NestedManufacturerSerializer() class Meta: model = Module fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] -class NestedModuleSerializer(ModuleSerializer): +class ChildModuleSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Module - fields = ['id', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] + fields = ['id', 'url', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] # diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index d75ecb083b1..7ac824bac9b 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -35,37 +35,37 @@ # Devices url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - url(r'^devices/(?P\d+)/console-ports/$', views.NestedConsolePortViewSet.as_view({'get': 'list'}), name='device_consoleports'), - url(r'^devices/(?P\d+)/console-server-ports/$', views.NestedConsoleServerPortViewSet.as_view({'get': 'list'}), name='device_consoleserverports'), - url(r'^devices/(?P\d+)/power-ports/$', views.NestedPowerPortViewSet.as_view({'get': 'list'}), name='device_powerports'), - url(r'^devices/(?P\d+)/power-outlets/$', views.NestedPowerOutletViewSet.as_view({'get': 'list'}), name='device_poweroutlets'), - url(r'^devices/(?P\d+)/interfaces/$', views.NestedInterfaceViewSet.as_view({'get': 'list'}), name='device_interfaces'), - url(r'^devices/(?P\d+)/device-bays/$', views.NestedDeviceBayViewSet.as_view({'get': 'list'}), name='device_devicebays'), - url(r'^devices/(?P\d+)/modules/$', views.NestedModuleViewSet.as_view({'get': 'list'}), name='device_modules'), + url(r'^devices/(?P\d+)/console-ports/$', views.ChildConsolePortViewSet.as_view({'get': 'list'}), name='consoleport-list'), + url(r'^devices/(?P\d+)/console-server-ports/$', views.ChildConsoleServerPortViewSet.as_view({'get': 'list'}), name='consoleserverport-list'), + url(r'^devices/(?P\d+)/power-ports/$', views.NestedPowerPortViewSet.as_view({'get': 'list'}), name='powerport-list'), + url(r'^devices/(?P\d+)/power-outlets/$', views.NestedPowerOutletViewSet.as_view({'get': 'list'}), name='poweroutlet-list'), + url(r'^devices/(?P\d+)/interfaces/$', views.NestedInterfaceViewSet.as_view({'get': 'list'}), name='interface-list'), + url(r'^devices/(?P\d+)/device-bays/$', views.NestedDeviceBayViewSet.as_view({'get': 'list'}), name='devicebay-list'), + url(r'^devices/(?P\d+)/modules/$', views.NestedModuleViewSet.as_view({'get': 'list'}), name='module-list'), # TODO: Services # Console ports - url(r'^console-ports/(?P\d+)/$', views.ConsolePortViewSet.as_view({'get': 'retrieve'}), name='consoleport'), + url(r'^console-ports/(?P\d+)/$', views.ConsolePortViewSet.as_view({'get': 'retrieve'}), name='consoleport-detail'), # Console server ports - url(r'^console-server-ports/(?P\d+)/$', views.ConsoleServerPortViewSet.as_view({'get': 'retrieve'}), name='consoleserverport'), + url(r'^console-server-ports/(?P\d+)/$', views.ConsoleServerPortViewSet.as_view({'get': 'retrieve'}), name='consoleserverport-detail'), # Power ports - url(r'^power-ports/(?P\d+)/$', views.PowerPortViewSet.as_view({'get': 'retrieve'}), name='powerport'), + url(r'^power-ports/(?P\d+)/$', views.PowerPortViewSet.as_view({'get': 'retrieve'}), name='powerport-detail'), # Power outlets - url(r'^power-outlets/(?P\d+)/$', views.PowerOutletViewSet.as_view({'get': 'retrieve'}), name='poweroutlet'), + url(r'^power-outlets/(?P\d+)/$', views.PowerOutletViewSet.as_view({'get': 'retrieve'}), name='poweroutlet-detail'), # Interfaces - url(r'^interfaces/(?P\d+)/$', views.InterfaceViewSet.as_view({'get': 'retrieve'}), name='interface'), + url(r'^interfaces/(?P\d+)/$', views.InterfaceViewSet.as_view({'get': 'retrieve'}), name='interface-detail'), url(r'^interfaces/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, name='interface_graphs'), # Device bays - url(r'^device-bays/(?P\d+)/$', views.DeviceBayViewSet.as_view({'get': 'retrieve'}), name='devicebay'), + url(r'^device-bays/(?P\d+)/$', views.DeviceBayViewSet.as_view({'get': 'retrieve'}), name='devicebay-detail'), # Modules - url(r'^modules/(?P\d+)/$', views.ModuleViewSet.as_view({'get': 'retrieve'}), name='module'), + url(r'^modules/(?P\d+)/$', views.ModuleViewSet.as_view({'get': 'retrieve'}), name='module-detail'), # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8d4eb13c30b..31103b786ba 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -57,13 +57,9 @@ class RackRoleViewSet(ModelViewSet): class RackViewSet(CustomFieldModelViewSet): queryset = Rack.objects.select_related('site', 'group__site', 'tenant') + serializer_class = serializers.RackSerializer filter_class = filters.RackFilter - def get_serializer_class(self): - if self.action == 'retrieve': - return serializers.RackDetailSerializer - return serializers.RackSerializer - class RackUnitListView(APIView): """ @@ -85,7 +81,10 @@ def get(self, request, pk): # Serialize Devices within the rack elevation for u in elevation: if u['device']: - u['device'] = serializers.DeviceNestedSerializer(instance=u['device']).data + u['device'] = serializers.NestedDeviceSerializer( + instance=u['device'], + context={'request': request}, + ).data return Response(elevation) @@ -105,7 +104,7 @@ class ManufacturerViewSet(ModelViewSet): class DeviceTypeViewSet(CustomFieldModelViewSet): queryset = DeviceType.objects.select_related('manufacturer') - filter_class = filters.DeviceTypeFilter + serializer_class = serializers.DeviceTypeSerializer # @@ -150,8 +149,8 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.ConsolePortSerializer -class NestedConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedConsolePortSerializer +class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): + serializer_class = serializers.ChildConsoleServerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -167,8 +166,8 @@ class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyMode serializer_class = serializers.ConsoleServerPortSerializer -class NestedConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedConsoleServerPortSerializer +class ChildConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): + serializer_class = serializers.ChildConsoleServerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -185,7 +184,7 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedPowerPortSerializer + serializer_class = serializers.ChildPowerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -202,7 +201,7 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedPowerOutletSerializer + serializer_class = serializers.ChildPowerOutletSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -219,7 +218,7 @@ class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedInterfaceSerializer + serializer_class = serializers.ChildInterfaceSerializer filter_class = filters.InterfaceFilter def get_queryset(self): @@ -238,7 +237,7 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedDeviceBaySerializer + serializer_class = serializers.ChildDeviceBaySerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -255,7 +254,7 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Gen class NestedModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.NestedModuleSerializer + serializer_class = serializers.ChildModuleSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) diff --git a/netbox/extras/api/views.py b/netbox/extras/api/views.py index 1ee82ace422..74ebf073c36 100644 --- a/netbox/extras/api/views.py +++ b/netbox/extras/api/views.py @@ -33,10 +33,12 @@ def get_serializer_context(self): custom_field_choices[cfc.id] = cfc.value custom_field_choices = custom_field_choices - return { + context = super(CustomFieldModelViewSet, self).get_serializer_context() + context.update({ 'custom_fields': custom_fields, 'custom_field_choices': custom_field_choices, - } + }) + return context def get_queryset(self): # Prefetch custom field values @@ -55,8 +57,11 @@ def get_serializer_context(self): GRAPH_TYPE_PROVIDER: Provider, GRAPH_TYPE_SITE: Site, } + obj = get_object_or_404(cls[self.kwargs.get('type')], pk=self.kwargs['pk']) context = super(GraphListView, self).get_serializer_context() - context.update({'graphed_object': get_object_or_404(cls[self.kwargs.get('type')], pk=self.kwargs['pk'])}) + context.update({ + 'graphed_object': obj, + }) return context def get_queryset(self): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 4b69711cfdf..bab257cc5ee 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,9 +1,9 @@ from rest_framework import serializers -from dcim.api.serializers import DeviceNestedSerializer, NestedInterfaceSerializer, SiteNestedSerializer +from dcim.api.serializers import NestedDeviceSerializer, ChildInterfaceSerializer, NestedSiteSerializer from extras.api.serializers import CustomFieldSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF -from tenancy.api.serializers import TenantNestedSerializer +from tenancy.api.serializers import NestedTenantSerializer # @@ -11,26 +11,18 @@ # class VRFSerializer(CustomFieldSerializer, serializers.ModelSerializer): - tenant = TenantNestedSerializer() + tenant = NestedTenantSerializer() class Meta: model = VRF fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] -class VRFNestedSerializer(VRFSerializer): +class NestedVRFSerializer(serializers.HyperlinkedModelSerializer): - class Meta(VRFSerializer.Meta): - fields = ['id', 'name', 'rd'] - - -class VRFTenantSerializer(VRFSerializer): - """ - Include tenant serializer. Useful for determining tenant inheritance for Prefixes and IPAddresses. - """ - - class Meta(VRFSerializer.Meta): - fields = ['id', 'name', 'rd', 'tenant'] + class Meta: + model = VRF + fields = ['id', 'url', 'name', 'rd'] # @@ -44,10 +36,11 @@ class Meta: fields = ['id', 'name', 'slug', 'weight'] -class RoleNestedSerializer(RoleSerializer): +class NestedRoleSerializer(serializers.HyperlinkedModelSerializer): - class Meta(RoleSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = Role + fields = ['id', 'url', 'name', 'slug'] # @@ -61,10 +54,11 @@ class Meta: fields = ['id', 'name', 'slug', 'is_private'] -class RIRNestedSerializer(RIRSerializer): +class NestedRIRSerializer(serializers.HyperlinkedModelSerializer): - class Meta(RIRSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = RIR + fields = ['id', 'url', 'name', 'slug'] # @@ -72,17 +66,18 @@ class Meta(RIRSerializer.Meta): # class AggregateSerializer(CustomFieldSerializer, serializers.ModelSerializer): - rir = RIRNestedSerializer() + rir = NestedRIRSerializer() class Meta: model = Aggregate fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields'] -class AggregateNestedSerializer(AggregateSerializer): +class NestedAggregateSerializer(serializers.HyperlinkedModelSerializer): class Meta(AggregateSerializer.Meta): - fields = ['id', 'family', 'prefix'] + model = Aggregate + fields = ['id', 'url', 'family', 'prefix'] # @@ -90,17 +85,18 @@ class Meta(AggregateSerializer.Meta): # class VLANGroupSerializer(serializers.ModelSerializer): - site = SiteNestedSerializer() + site = NestedSiteSerializer() class Meta: model = VLANGroup fields = ['id', 'name', 'slug', 'site'] -class VLANGroupNestedSerializer(VLANGroupSerializer): +class NestedVLANGroupSerializer(serializers.HyperlinkedModelSerializer): - class Meta(VLANGroupSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = VLANGroup + fields = ['id', 'url', 'name', 'slug'] # @@ -108,21 +104,24 @@ class Meta(VLANGroupSerializer.Meta): # class VLANSerializer(CustomFieldSerializer, serializers.ModelSerializer): - site = SiteNestedSerializer() - group = VLANGroupNestedSerializer() - tenant = TenantNestedSerializer() - role = RoleNestedSerializer() + site = NestedSiteSerializer() + group = NestedVLANGroupSerializer() + tenant = NestedTenantSerializer() + role = NestedRoleSerializer() class Meta: model = VLAN - fields = ['id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', - 'custom_fields'] + fields = [ + 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', + 'custom_fields', + ] -class VLANNestedSerializer(VLANSerializer): +class NestedVLANSerializer(serializers.HyperlinkedModelSerializer): - class Meta(VLANSerializer.Meta): - fields = ['id', 'vid', 'name', 'display_name'] + class Meta: + model = VLAN + fields = ['id', 'url', 'vid', 'name', 'display_name'] # @@ -130,22 +129,25 @@ class Meta(VLANSerializer.Meta): # class PrefixSerializer(CustomFieldSerializer, serializers.ModelSerializer): - site = SiteNestedSerializer() - vrf = VRFTenantSerializer() - tenant = TenantNestedSerializer() - vlan = VLANNestedSerializer() - role = RoleNestedSerializer() + site = NestedSiteSerializer() + vrf = NestedVRFSerializer() + tenant = NestedTenantSerializer() + vlan = NestedVLANSerializer() + role = NestedRoleSerializer() class Meta: model = Prefix - fields = ['id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', - 'custom_fields'] + fields = [ + 'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + 'custom_fields', + ] -class PrefixNestedSerializer(PrefixSerializer): +class NestedPrefixSerializer(serializers.HyperlinkedModelSerializer): - class Meta(PrefixSerializer.Meta): - fields = ['id', 'family', 'prefix'] + class Meta: + model = Prefix + fields = ['id', 'url', 'family', 'prefix'] # @@ -153,23 +155,26 @@ class Meta(PrefixSerializer.Meta): # class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer): - vrf = VRFTenantSerializer() - tenant = TenantNestedSerializer() - interface = NestedInterfaceSerializer() + vrf = NestedVRFSerializer() + tenant = NestedTenantSerializer() + interface = ChildInterfaceSerializer() class Meta: model = IPAddress - fields = ['id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', - 'nat_outside', 'custom_fields'] + fields = [ + 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', + 'nat_outside', 'custom_fields', + ] -class IPAddressNestedSerializer(IPAddressSerializer): +class NestedIPAddressSerializer(serializers.HyperlinkedModelSerializer): - class Meta(IPAddressSerializer.Meta): - fields = ['id', 'family', 'address'] + class Meta: + model = IPAddress + fields = ['id', 'url', 'family', 'address'] -IPAddressSerializer._declared_fields['nat_inside'] = IPAddressNestedSerializer() -IPAddressSerializer._declared_fields['nat_outside'] = IPAddressNestedSerializer() +IPAddressSerializer._declared_fields['nat_inside'] = NestedIPAddressSerializer() +IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer() # @@ -177,15 +182,9 @@ class Meta(IPAddressSerializer.Meta): # class ServiceSerializer(serializers.ModelSerializer): - device = DeviceNestedSerializer() - ipaddresses = IPAddressNestedSerializer(many=True) + device = NestedDeviceSerializer() + ipaddresses = NestedIPAddressSerializer(many=True) class Meta: model = Service fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] - - -class ServiceNestedSerializer(ServiceSerializer): - - class Meta(ServiceSerializer.Meta): - fields = ['id', 'name', 'port', 'protocol'] diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 11787adae0a..fe6fff333e1 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -26,12 +26,12 @@ url(r'^profile/', include('users.urls', namespace='users')), # API - url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), - url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), - url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), - url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), - url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), + url(r'^api/circuits/', include('circuits.api.urls')), + url(r'^api/dcim/', include('dcim.api.urls')), + url(r'^api/ipam/', include('ipam.api.urls')), + url(r'^api/secrets/', include('secrets.api.urls')), + url(r'^api/tenancy/', include('tenancy.api.urls')), + url(r'^api-auth/', include('rest_framework.urls')), # Error testing url(r'^500/$', trigger_500), diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index f16af39c3cf..40c024e1cce 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -1,7 +1,6 @@ from rest_framework import serializers -from dcim.models import Device -from ipam.api.serializers import IPAddressNestedSerializer +from dcim.api.serializers import NestedDeviceSerializer from secrets.models import Secret, SecretRole @@ -16,34 +15,21 @@ class Meta: fields = ['id', 'name', 'slug'] -class SecretRoleNestedSerializer(SecretRoleSerializer): +class NestedSecretRoleSerializer(serializers.HyperlinkedModelSerializer): - class Meta(SecretRoleSerializer.Meta): - pass + class Meta: + model = SecretRole + fields = ['id', 'url', 'name', 'slug'] # # Secrets # -class SecretDeviceSerializer(serializers.ModelSerializer): - primary_ip = IPAddressNestedSerializer() - - class Meta: - model = Device - fields = ['id', 'name', 'primary_ip'] - - class SecretSerializer(serializers.ModelSerializer): - device = SecretDeviceSerializer() - role = SecretRoleNestedSerializer() + device = NestedDeviceSerializer() + role = NestedSecretRoleSerializer() class Meta: model = Secret fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated'] - - -class SecretNestedSerializer(SecretSerializer): - - class Meta(SecretSerializer.Meta): - fields = ['id', 'name'] diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 6d22561ef37..afd634129d7 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -15,10 +15,11 @@ class Meta: fields = ['id', 'name', 'slug'] -class TenantGroupNestedSerializer(TenantGroupSerializer): +class NestedTenantGroupSerializer(serializers.HyperlinkedModelSerializer): - class Meta(TenantGroupSerializer.Meta): - pass + class Meta: + model = TenantGroup + fields = ['id', 'url', 'name', 'slug'] # @@ -26,14 +27,15 @@ class Meta(TenantGroupSerializer.Meta): # class TenantSerializer(CustomFieldSerializer, serializers.ModelSerializer): - group = TenantGroupNestedSerializer() + group = NestedTenantGroupSerializer() class Meta: model = Tenant fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields'] -class TenantNestedSerializer(TenantSerializer): +class NestedTenantSerializer(serializers.HyperlinkedModelSerializer): - class Meta(TenantSerializer.Meta): - fields = ['id', 'name', 'slug'] + class Meta: + model = Tenant + fields = ['id', 'url', 'name', 'slug'] From 12d263999bab1e19ad92b5236cbd2e803edcd019 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2017 14:36:13 -0500 Subject: [PATCH 009/206] Introduced WritableSerializerMixin --- netbox/circuits/api/views.py | 14 +++++------ netbox/dcim/api/views.py | 46 +++++++++++++++++++--------------- netbox/ipam/api/serializers.py | 3 ++- netbox/ipam/api/views.py | 15 +++++------ netbox/tenancy/api/views.py | 3 ++- netbox/utilities/api.py | 24 ++++++++++++++++++ 6 files changed, 68 insertions(+), 37 deletions(-) diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 7316029755d..ccfe57a4c75 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -9,6 +9,7 @@ from circuits.filters import CircuitFilter from extras.api.views import CustomFieldModelViewSet +from utilities.api import WritableSerializerMixin from . import serializers @@ -34,26 +35,23 @@ class CircuitTypeViewSet(ModelViewSet): # Circuits # -class CircuitViewSet(CustomFieldModelViewSet): +class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Circuit.objects.select_related('type', 'tenant', 'provider') + serializer_class = serializers.CircuitSerializer filter_class = CircuitFilter - def get_serializer_class(self): - if self.action == 'retrieve': - return serializers.CircuitDetailSerializer - return serializers.CircuitSerializer - # # Circuit Terminations # -class CircuitTerminationViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class CircuitTerminationViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = CircuitTermination.objects.select_related('site', 'interface__device') serializer_class = serializers.CircuitTerminationSerializer -class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin ,WritableSerializerMixin, GenericViewSet): serializer_class = serializers.CircuitTerminationSerializer def get_queryset(self): diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 31103b786ba..44ebd467783 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -18,7 +18,7 @@ from dcim import filters from extras.api.views import CustomFieldModelViewSet from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer -from utilities.api import ServiceUnavailable +from utilities.api import ServiceUnavailable, WritableSerializerMixin from .exceptions import MissingFilterException from . import serializers @@ -27,7 +27,7 @@ # Sites # -class SiteViewSet(CustomFieldModelViewSet): +class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer @@ -36,7 +36,7 @@ class SiteViewSet(CustomFieldModelViewSet): # Rack groups # -class RackGroupViewSet(ModelViewSet): +class RackGroupViewSet(WritableSerializerMixin, ModelViewSet): queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer filter_class = filters.RackGroupFilter @@ -55,7 +55,7 @@ class RackRoleViewSet(ModelViewSet): # Racks # -class RackViewSet(CustomFieldModelViewSet): +class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Rack.objects.select_related('site', 'group__site', 'tenant') serializer_class = serializers.RackSerializer filter_class = filters.RackFilter @@ -102,7 +102,7 @@ class ManufacturerViewSet(ModelViewSet): # Device Types # -class DeviceTypeViewSet(CustomFieldModelViewSet): +class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = DeviceType.objects.select_related('manufacturer') serializer_class = serializers.DeviceTypeSerializer @@ -129,7 +129,7 @@ class PlatformViewSet(ModelViewSet): # Devices # -class DeviceViewSet(CustomFieldModelViewSet): +class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Device.objects.select_related( 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'rack__site', 'parent_bay', ).prefetch_related( @@ -144,12 +144,13 @@ class DeviceViewSet(CustomFieldModelViewSet): # Console Ports # -class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = ConsolePort.objects.select_related('cs_port') serializer_class = serializers.ConsolePortSerializer -class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildConsoleServerPortSerializer def get_queryset(self): @@ -161,12 +162,13 @@ def get_queryset(self): # Console Server Ports # -class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = ConsoleServerPort.objects.select_related('connected_console') serializer_class = serializers.ConsoleServerPortSerializer -class ChildConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class ChildConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildConsoleServerPortSerializer def get_queryset(self): @@ -178,12 +180,13 @@ def get_queryset(self): # Power Ports # -class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = PowerPort.objects.select_related('power_outlet') serializer_class = serializers.PowerPortSerializer -class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildPowerPortSerializer def get_queryset(self): @@ -195,12 +198,13 @@ def get_queryset(self): # Power Outlets # -class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = PowerOutlet.objects.select_related('connected_port') serializer_class = serializers.PowerOutletSerializer -class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildPowerOutletSerializer def get_queryset(self): @@ -212,12 +216,13 @@ def get_queryset(self): # Interfaces # -class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = Interface.objects.select_related('device') serializer_class = serializers.InterfaceDetailSerializer -class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildInterfaceSerializer filter_class = filters.InterfaceFilter @@ -231,12 +236,13 @@ def get_queryset(self): # Device bays # -class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, + GenericViewSet): queryset = DeviceBay.objects.select_related('installed_device') serializer_class = serializers.DeviceBaySerializer -class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildDeviceBaySerializer def get_queryset(self): @@ -248,12 +254,12 @@ def get_queryset(self): # Modules # -class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, GenericViewSet): +class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, GenericViewSet): queryset = Module.objects.select_related('device', 'manufacturer') serializer_class = serializers.ModuleSerializer -class NestedModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): +class NestedModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildModuleSerializer def get_queryset(self): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index bab257cc5ee..4394cc1a8fb 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -4,6 +4,7 @@ from extras.api.serializers import CustomFieldSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer +from utilities.api import WritableSerializerMixin # @@ -84,7 +85,7 @@ class Meta(AggregateSerializer.Meta): # VLAN groups # -class VLANGroupSerializer(serializers.ModelSerializer): +class VLANGroupSerializer(WritableSerializerMixin, serializers.ModelSerializer): site = NestedSiteSerializer() class Meta: diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 80ff10c6f52..94d1e681422 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -4,6 +4,7 @@ from ipam import filters from extras.api.views import CustomFieldModelViewSet +from utilities.api import WritableSerializerMixin from . import serializers @@ -11,7 +12,7 @@ # VRFs # -class VRFViewSet(CustomFieldModelViewSet): +class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter @@ -39,7 +40,7 @@ class RIRViewSet(ModelViewSet): # Aggregates # -class AggregateViewSet(CustomFieldModelViewSet): +class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Aggregate.objects.select_related('rir') serializer_class = serializers.AggregateSerializer filter_class = filters.AggregateFilter @@ -49,7 +50,7 @@ class AggregateViewSet(CustomFieldModelViewSet): # Prefixes # -class PrefixViewSet(CustomFieldModelViewSet): +class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter @@ -59,7 +60,7 @@ class PrefixViewSet(CustomFieldModelViewSet): # IP addresses # -class IPAddressViewSet(CustomFieldModelViewSet): +class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside') serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter @@ -69,7 +70,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): # VLAN groups # -class VLANGroupViewSet(ModelViewSet): +class VLANGroupViewSet(WritableSerializerMixin, ModelViewSet): queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer filter_class = filters.VLANGroupFilter @@ -79,7 +80,7 @@ class VLANGroupViewSet(ModelViewSet): # VLANs # -class VLANViewSet(CustomFieldModelViewSet): +class VLANViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter @@ -89,7 +90,7 @@ class VLANViewSet(CustomFieldModelViewSet): # Services # -class ServiceViewSet(ModelViewSet): +class ServiceViewSet(WritableSerializerMixin, ModelViewSet): queryset = Service.objects.select_related('device').prefetch_related('ipaddresses') serializer_class = serializers.ServiceSerializer filter_class = filters.ServiceFilter diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 17d0e79ef2e..d288dd9f240 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -4,6 +4,7 @@ from tenancy.filters import TenantFilter from extras.api.views import CustomFieldModelViewSet +from utilities.api import WritableSerializerMixin from . import serializers @@ -20,7 +21,7 @@ class TenantGroupViewSet(ModelViewSet): # Tenants # -class TenantViewSet(CustomFieldModelViewSet): +class TenantViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Tenant.objects.select_related('group') serializer_class = serializers.TenantSerializer filter_class = TenantFilter diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index ff35fd2930e..c25f7e8424a 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -1,6 +1,30 @@ from rest_framework.exceptions import APIException +from rest_framework.serializers import ModelSerializer + + +WRITE_OPERATIONS = ['create', 'update', 'partial_update', 'delete'] class ServiceUnavailable(APIException): status_code = 503 default_detail = "Service temporarily unavailable, please try again later." + + +class WritableSerializerMixin(object): + """ + Returns a flat Serializer from the given model suitable for write operations (POST, PUT, PATCH). This is necessary + to allow write operations on objects which utilize nested serializers. + """ + + def get_serializer_class(self): + + class WritableSerializer(ModelSerializer): + + class Meta: + model = self.queryset.model + fields = '__all__' + + if self.action in WRITE_OPERATIONS: + return WritableSerializer + + return self.serializer_class From a3d0d4a5bff0a88028eec42e4b8caad32f5ddf7f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2017 14:54:12 -0500 Subject: [PATCH 010/206] Enabled pagination --- netbox/netbox/settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index dc87f6655d1..74bf6ae1054 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -181,9 +181,11 @@ # Secrets SECRETS_MIN_PUBKEY_SIZE = 2048 -# Django REST framework +# Django REST framework (API) REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) + 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',), + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': PAGINATE_COUNT, } if LOGIN_REQUIRED: REST_FRAMEWORK['DEFAULT_PERMISSION_CLASSES'] = ('rest_framework.permissions.IsAuthenticated',) From c0dac1383d8ea4847f08a06f997bbf80f1cafca1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2017 15:12:46 -0500 Subject: [PATCH 011/206] Fix retrieval of model under viewsets without a statically defined queryset --- netbox/utilities/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index c25f7e8424a..a69e4f36984 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -21,7 +21,7 @@ def get_serializer_class(self): class WritableSerializer(ModelSerializer): class Meta: - model = self.queryset.model + model = self.get_queryset().model fields = '__all__' if self.action in WRITE_OPERATIONS: From 0cf029edd478e6109a54d61f69064247a449885b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 27 Jan 2017 16:19:38 -0500 Subject: [PATCH 012/206] Added Service serializers --- netbox/dcim/api/urls.py | 13 +++++++------ netbox/dcim/api/views.py | 12 ++++++------ netbox/ipam/api/serializers.py | 8 ++++++++ netbox/ipam/api/views.py | 22 +++++++++++++++++----- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 7ac824bac9b..c649d1ab6b6 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -4,6 +4,7 @@ from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.api.views import GraphListView, TopologyMapView +from ipam.api.views import ChildServiceViewSet from . import views @@ -37,12 +38,12 @@ url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), url(r'^devices/(?P\d+)/console-ports/$', views.ChildConsolePortViewSet.as_view({'get': 'list'}), name='consoleport-list'), url(r'^devices/(?P\d+)/console-server-ports/$', views.ChildConsoleServerPortViewSet.as_view({'get': 'list'}), name='consoleserverport-list'), - url(r'^devices/(?P\d+)/power-ports/$', views.NestedPowerPortViewSet.as_view({'get': 'list'}), name='powerport-list'), - url(r'^devices/(?P\d+)/power-outlets/$', views.NestedPowerOutletViewSet.as_view({'get': 'list'}), name='poweroutlet-list'), - url(r'^devices/(?P\d+)/interfaces/$', views.NestedInterfaceViewSet.as_view({'get': 'list'}), name='interface-list'), - url(r'^devices/(?P\d+)/device-bays/$', views.NestedDeviceBayViewSet.as_view({'get': 'list'}), name='devicebay-list'), - url(r'^devices/(?P\d+)/modules/$', views.NestedModuleViewSet.as_view({'get': 'list'}), name='module-list'), - # TODO: Services + url(r'^devices/(?P\d+)/power-ports/$', views.ChildPowerPortViewSet.as_view({'get': 'list'}), name='powerport-list'), + url(r'^devices/(?P\d+)/power-outlets/$', views.ChildPowerOutletViewSet.as_view({'get': 'list'}), name='poweroutlet-list'), + url(r'^devices/(?P\d+)/interfaces/$', views.ChildInterfaceViewSet.as_view({'get': 'list'}), name='interface-list'), + url(r'^devices/(?P\d+)/device-bays/$', views.ChildDeviceBayViewSet.as_view({'get': 'list'}), name='devicebay-list'), + url(r'^devices/(?P\d+)/modules/$', views.ChildModuleViewSet.as_view({'get': 'list'}), name='module-list'), + url(r'^devices/(?P\d+)/services/$', ChildServiceViewSet.as_view({'get': 'list'}), name='service-list'), # Console ports url(r'^console-ports/(?P\d+)/$', views.ConsolePortViewSet.as_view({'get': 'retrieve'}), name='consoleport-detail'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 44ebd467783..b5946ff385a 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -151,7 +151,7 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildConsoleServerPortSerializer + serializer_class = serializers.ChildConsolePortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -186,7 +186,7 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.PowerPortSerializer -class NestedPowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class ChildPowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildPowerPortSerializer def get_queryset(self): @@ -204,7 +204,7 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.PowerOutletSerializer -class NestedPowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class ChildPowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildPowerOutletSerializer def get_queryset(self): @@ -222,7 +222,7 @@ class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.InterfaceDetailSerializer -class NestedInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class ChildInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildInterfaceSerializer filter_class = filters.InterfaceFilter @@ -242,7 +242,7 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.DeviceBaySerializer -class NestedDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class ChildDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildDeviceBaySerializer def get_queryset(self): @@ -259,7 +259,7 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wri serializer_class = serializers.ModuleSerializer -class NestedModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class ChildModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildModuleSerializer def get_queryset(self): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 4394cc1a8fb..8c7fb2fa794 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -189,3 +189,11 @@ class ServiceSerializer(serializers.ModelSerializer): class Meta: model = Service fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] + + +class ChildServiceSerializer(serializers.HyperlinkedModelSerializer): + ipaddresses = NestedIPAddressSerializer(many=True) + + class Meta: + model = Service + fields = ['id', 'url', 'name', 'port', 'protocol', 'ipaddresses', 'description'] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 94d1e681422..feedac9a737 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,8 +1,13 @@ -from rest_framework.viewsets import ModelViewSet +from django.shortcuts import get_object_or_404 +from rest_framework.mixins import ( + CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, +) +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from dcim.models import Device from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam import filters - from extras.api.views import CustomFieldModelViewSet from utilities.api import WritableSerializerMixin from . import serializers @@ -90,7 +95,14 @@ class VLANViewSet(WritableSerializerMixin, CustomFieldModelViewSet): # Services # -class ServiceViewSet(WritableSerializerMixin, ModelViewSet): - queryset = Service.objects.select_related('device').prefetch_related('ipaddresses') +class ServiceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, GenericViewSet): + queryset = Service.objects.select_related('device') serializer_class = serializers.ServiceSerializer - filter_class = filters.ServiceFilter + + +class ChildServiceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): + serializer_class = serializers.ChildServiceSerializer + + def get_queryset(self): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + return Service.objects.filter(device=device).select_related('device') From f0fef94a4fd461ff670ecd408c8027cf6f83a744 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Jan 2017 15:35:01 -0500 Subject: [PATCH 013/206] Re-implemented interface/connection serializers --- netbox/circuits/api/serializers.py | 4 +- netbox/dcim/api/serializers.py | 80 ++++++++++++++++++++---------- netbox/dcim/api/views.py | 14 +++--- netbox/ipam/api/serializers.py | 4 +- 4 files changed, 65 insertions(+), 37 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index d5f229a8284..68b26cff789 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import NestedSiteSerializer, ChildInterfaceSerializer +from dcim.api.serializers import NestedSiteSerializer, DeviceInterfaceSerializer from extras.api.serializers import CustomFieldSerializer from tenancy.api.serializers import NestedTenantSerializer @@ -51,7 +51,7 @@ class Meta: class CircuitTerminationSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() - interface = ChildInterfaceSerializer() + interface = DeviceInterfaceSerializer() class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c6e68b9cb08..e7c9ab80dc5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -313,7 +313,7 @@ class Meta: fields = ['id', 'device', 'name', 'connected_console'] -class ChildConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): +class DeviceConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsoleServerPort @@ -333,7 +333,7 @@ class Meta: fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] -class ChildConsolePortSerializer(serializers.HyperlinkedModelSerializer): +class DeviceConsolePortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsolePort @@ -352,7 +352,7 @@ class Meta: fields = ['id', 'device', 'name', 'connected_port'] -class ChildPowerOutletSerializer(serializers.HyperlinkedModelSerializer): +class DevicePowerOutletSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerOutlet @@ -372,7 +372,7 @@ class Meta: fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] -class ChildPowerPortSerializer(serializers.HyperlinkedModelSerializer): +class DevicePowerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerPort @@ -383,30 +383,69 @@ class Meta: # Interfaces # + class InterfaceSerializer(serializers.ModelSerializer): device = NestedDeviceSerializer() - form_factor = serializers.ReadOnlyField(source='get_form_factor_display') + connection = serializers.SerializerMethodField(read_only=True) + connected_interface = serializers.SerializerMethodField(read_only=True) class Meta: model = Interface - fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] + fields = [ + 'id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'connection', + 'connected_interface', + ] + + def get_connection(self, obj): + if obj.connection: + return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data + return None + def get_connected_interface(self, obj): + if obj.connected_interface: + return PeerInterfaceSerializer(obj.connected_interface, context=self.context).data + return None -class ChildInterfaceSerializer(serializers.HyperlinkedModelSerializer): + +class PeerInterfaceSerializer(serializers.HyperlinkedModelSerializer): + device = NestedDeviceSerializer() class Meta: model = Interface - fields = ['id', 'url', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected'] + fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] -# TODO: Remove this -class InterfaceDetailSerializer(InterfaceSerializer): +class DeviceInterfaceSerializer(serializers.HyperlinkedModelSerializer): + connection = serializers.SerializerMethodField() - class Meta(InterfaceSerializer.Meta): - fields = [ - 'id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'is_connected', - 'connected_interface', - ] + class Meta: + model = Interface + fields = ['id', 'url', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'connection'] + + def get_connection(self, obj): + if obj.connection: + return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data + return None + + +# +# Interface connections +# + +class InterfaceConnectionSerializer(serializers.ModelSerializer): + interface_a = PeerInterfaceSerializer() + interface_b = PeerInterfaceSerializer() + + class Meta: + model = InterfaceConnection + fields = ['id', 'interface_a', 'interface_b', 'connection_status'] + + +class NestedInterfaceConnectionSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = InterfaceConnection + fields = ['id', 'url', 'connection_status'] # @@ -447,14 +486,3 @@ class ChildModuleSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Module fields = ['id', 'url', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] - - -# -# Interface connections -# - -class InterfaceConnectionSerializer(serializers.ModelSerializer): - - class Meta: - model = InterfaceConnection - fields = ['id', 'interface_a', 'interface_b', 'connection_status'] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index b5946ff385a..9d3f5f68106 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -151,7 +151,7 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildConsolePortSerializer + serializer_class = serializers.DeviceConsolePortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -169,7 +169,7 @@ class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyMode class ChildConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildConsoleServerPortSerializer + serializer_class = serializers.DeviceConsoleServerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -187,7 +187,7 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class ChildPowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildPowerPortSerializer + serializer_class = serializers.DevicePowerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -205,7 +205,7 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin class ChildPowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildPowerOutletSerializer + serializer_class = serializers.DevicePowerOutletSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -219,11 +219,11 @@ def get_queryset(self): class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, GenericViewSet): queryset = Interface.objects.select_related('device') - serializer_class = serializers.InterfaceDetailSerializer + serializer_class = serializers.InterfaceSerializer class ChildInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildInterfaceSerializer + serializer_class = serializers.DeviceInterfaceSerializer filter_class = filters.InterfaceFilter def get_queryset(self): @@ -380,7 +380,7 @@ def get(self, request): interfaces = Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') for iface in interfaces: - data = serializers.InterfaceDetailSerializer(instance=iface).data + data = serializers.InterfaceSerializer(instance=iface).data del(data['device']) response['interfaces'].append(data) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 8c7fb2fa794..37b42af6c15 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from dcim.api.serializers import NestedDeviceSerializer, ChildInterfaceSerializer, NestedSiteSerializer +from dcim.api.serializers import NestedDeviceSerializer, DeviceInterfaceSerializer, NestedSiteSerializer from extras.api.serializers import CustomFieldSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer @@ -158,7 +158,7 @@ class Meta: class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer): vrf = NestedVRFSerializer() tenant = NestedTenantSerializer() - interface = ChildInterfaceSerializer() + interface = DeviceInterfaceSerializer() class Meta: model = IPAddress From 7beac0b105ba71d0d3b71ba62d13d7d670f6ebb5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Jan 2017 16:15:12 -0500 Subject: [PATCH 014/206] Converted device component views to a router --- netbox/dcim/api/urls.py | 57 ++++++++++++++++++++-------------------- netbox/dcim/api/views.py | 14 +++++----- netbox/ipam/api/views.py | 2 +- 3 files changed, 37 insertions(+), 36 deletions(-) diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index c649d1ab6b6..a7c17b36880 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -4,26 +4,54 @@ from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from extras.api.views import GraphListView, TopologyMapView -from ipam.api.views import ChildServiceViewSet +from ipam.api.views import ServiceViewSet, DeviceServiceViewSet from . import views router = routers.DefaultRouter() + +# Sites router.register(r'sites', views.SiteViewSet) + +# Racks router.register(r'rack-groups', views.RackGroupViewSet) router.register(r'rack-roles', views.RackRoleViewSet) router.register(r'racks', views.RackViewSet) + +# Device types router.register(r'manufacturers', views.ManufacturerViewSet) router.register(r'device-types', views.DeviceTypeViewSet) + +# Devices router.register(r'device-roles', views.DeviceRoleViewSet) router.register(r'platforms', views.PlatformViewSet) router.register(r'devices', views.DeviceViewSet) +router.register(r'console-ports', views.ConsolePortViewSet) +router.register(r'console-server-ports', views.ConsoleServerPortViewSet) +router.register(r'power-ports', views.PowerPortViewSet) +router.register(r'power-outlets', views.PowerOutletViewSet) +router.register(r'interfaces', views.InterfaceViewSet) router.register(r'interface-connections', views.InterfaceConnectionViewSet) +router.register(r'device-bays', views.DeviceBayViewSet) +router.register(r'modules', views.ModuleViewSet) +router.register(r'services', ServiceViewSet) + +# Device components +device_router = routers.DefaultRouter() +device_router.register(r'console-ports', views.DeviceConsolePortViewSet, base_name='consoleport') +device_router.register(r'console-server-ports', views.DeviceConsoleServerPortViewSet, base_name='consoleserverport') +device_router.register(r'power-ports', views.DevicePowerPortViewSet, base_name='powerport') +device_router.register(r'power-outlets', views.DevicePowerOutletViewSet, base_name='poweroutlet') +device_router.register(r'interfaces', views.DeviceInterfaceViewSet, base_name='interface') +device_router.register(r'device-bays', views.DeviceDeviceBayViewSet, base_name='devicebay') +device_router.register(r'modules', views.DeviceModuleViewSet, base_name='module') +device_router.register(r'services', DeviceServiceViewSet, base_name='service') urlpatterns = [ url(r'', include(router.urls)), + url(r'^devices/(?P\d+)/', include(device_router.urls)), # Sites url(r'^sites/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'), @@ -36,38 +64,11 @@ # Devices url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - url(r'^devices/(?P\d+)/console-ports/$', views.ChildConsolePortViewSet.as_view({'get': 'list'}), name='consoleport-list'), - url(r'^devices/(?P\d+)/console-server-ports/$', views.ChildConsoleServerPortViewSet.as_view({'get': 'list'}), name='consoleserverport-list'), - url(r'^devices/(?P\d+)/power-ports/$', views.ChildPowerPortViewSet.as_view({'get': 'list'}), name='powerport-list'), - url(r'^devices/(?P\d+)/power-outlets/$', views.ChildPowerOutletViewSet.as_view({'get': 'list'}), name='poweroutlet-list'), - url(r'^devices/(?P\d+)/interfaces/$', views.ChildInterfaceViewSet.as_view({'get': 'list'}), name='interface-list'), - url(r'^devices/(?P\d+)/device-bays/$', views.ChildDeviceBayViewSet.as_view({'get': 'list'}), name='devicebay-list'), - url(r'^devices/(?P\d+)/modules/$', views.ChildModuleViewSet.as_view({'get': 'list'}), name='module-list'), - url(r'^devices/(?P\d+)/services/$', ChildServiceViewSet.as_view({'get': 'list'}), name='service-list'), - - # Console ports - url(r'^console-ports/(?P\d+)/$', views.ConsolePortViewSet.as_view({'get': 'retrieve'}), name='consoleport-detail'), - - # Console server ports - url(r'^console-server-ports/(?P\d+)/$', views.ConsoleServerPortViewSet.as_view({'get': 'retrieve'}), name='consoleserverport-detail'), - - # Power ports - url(r'^power-ports/(?P\d+)/$', views.PowerPortViewSet.as_view({'get': 'retrieve'}), name='powerport-detail'), - - # Power outlets - url(r'^power-outlets/(?P\d+)/$', views.PowerOutletViewSet.as_view({'get': 'retrieve'}), name='poweroutlet-detail'), # Interfaces - url(r'^interfaces/(?P\d+)/$', views.InterfaceViewSet.as_view({'get': 'retrieve'}), name='interface-detail'), url(r'^interfaces/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, name='interface_graphs'), - # Device bays - url(r'^device-bays/(?P\d+)/$', views.DeviceBayViewSet.as_view({'get': 'retrieve'}), name='devicebay-detail'), - - # Modules - url(r'^modules/(?P\d+)/$', views.ModuleViewSet.as_view({'get': 'retrieve'}), name='module-detail'), - # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), url(r'^topology-maps/(?P[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 9d3f5f68106..472fc69318f 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -150,7 +150,7 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.ConsolePortSerializer -class ChildConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DeviceConsolePortSerializer def get_queryset(self): @@ -168,7 +168,7 @@ class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyMode serializer_class = serializers.ConsoleServerPortSerializer -class ChildConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DeviceConsoleServerPortSerializer def get_queryset(self): @@ -186,7 +186,7 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.PowerPortSerializer -class ChildPowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DevicePowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DevicePowerPortSerializer def get_queryset(self): @@ -204,7 +204,7 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.PowerOutletSerializer -class ChildPowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DevicePowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DevicePowerOutletSerializer def get_queryset(self): @@ -222,7 +222,7 @@ class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.InterfaceSerializer -class ChildInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DeviceInterfaceSerializer filter_class = filters.InterfaceFilter @@ -242,7 +242,7 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.DeviceBaySerializer -class ChildDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildDeviceBaySerializer def get_queryset(self): @@ -259,7 +259,7 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wri serializer_class = serializers.ModuleSerializer -class ChildModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildModuleSerializer def get_queryset(self): diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index feedac9a737..aba741291bc 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -100,7 +100,7 @@ class ServiceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wr serializer_class = serializers.ServiceSerializer -class ChildServiceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceServiceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.ChildServiceSerializer def get_queryset(self): From d9e4017677e4447589e2698f1a51aa470a1d59b2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Jan 2017 17:00:58 -0500 Subject: [PATCH 015/206] Moved graph views into model viewsets --- netbox/circuits/api/urls.py | 4 ---- netbox/circuits/api/views.py | 11 +++++++++++ netbox/dcim/api/urls.py | 12 ++---------- netbox/dcim/api/views.py | 21 +++++++++++++++++++-- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 0dd2d2a9499..f06b14165aa 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -18,10 +18,6 @@ url(r'', include(router.urls)), - # Providers - url(r'^providers/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_PROVIDER}, - name='provider_graphs'), - # Circuits url(r'^circuits/(?P\d+)/terminations/$', views.NestedCircuitTerminationViewSet.as_view({'get': 'list'})), diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index ccfe57a4c75..1fdafa97bdc 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,13 +1,17 @@ from django.shortcuts import get_object_or_404 +from rest_framework.decorators import detail_route from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, ) +from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet, ModelViewSet from circuits.models import Provider, CircuitTermination, CircuitType, Circuit from circuits.filters import CircuitFilter +from extras.models import Graph, GRAPH_TYPE_PROVIDER +from extras.api.serializers import GraphSerializer from extras.api.views import CustomFieldModelViewSet from utilities.api import WritableSerializerMixin from . import serializers @@ -21,6 +25,13 @@ class ProviderViewSet(CustomFieldModelViewSet): queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer + @detail_route() + def graphs(self, request, pk=None): + provider = get_object_or_404(Provider, pk=pk) + queryset = Graph.objects.filter(type=GRAPH_TYPE_PROVIDER) + serializer = GraphSerializer(queryset, many=True, context={'graphed_object': provider}) + return Response(serializer.data) + # # Circuit Types diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index a7c17b36880..27f3813f2d3 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -37,6 +37,8 @@ router.register(r'modules', views.ModuleViewSet) router.register(r'services', ServiceViewSet) +# TODO: Device type components + # Device components device_router = routers.DefaultRouter() device_router.register(r'console-ports', views.DeviceConsolePortViewSet, base_name='consoleport') @@ -53,22 +55,12 @@ url(r'', include(router.urls)), url(r'^devices/(?P\d+)/', include(device_router.urls)), - # Sites - url(r'^sites/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_SITE}, name='site_graphs'), - # Racks url(r'^racks/(?P\d+)/rack-units/$', views.RackUnitListView.as_view(), name='rack_units'), - # Device types - # TODO: Nested DeviceType components - # Devices url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - # Interfaces - url(r'^interfaces/(?P\d+)/graphs/$', GraphListView.as_view(), {'type': GRAPH_TYPE_INTERFACE}, - name='interface_graphs'), - # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), url(r'^topology-maps/(?P[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 472fc69318f..696b306aae8 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,3 +1,4 @@ +from rest_framework.decorators import detail_route from rest_framework.mixins import ( CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, ) @@ -16,8 +17,10 @@ Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) from dcim import filters -from extras.api.views import CustomFieldModelViewSet from extras.api.renderers import BINDZoneRenderer, FlatJSONRenderer +from extras.api.serializers import GraphSerializer +from extras.api.views import CustomFieldModelViewSet +from extras.models import Graph, GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE from utilities.api import ServiceUnavailable, WritableSerializerMixin from .exceptions import MissingFilterException from . import serializers @@ -31,6 +34,13 @@ class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer + @detail_route() + def graphs(self, request, pk=None): + site = get_object_or_404(Site, pk=pk) + queryset = Graph.objects.filter(type=GRAPH_TYPE_SITE) + serializer = GraphSerializer(queryset, many=True, context={'graphed_object': site}) + return Response(serializer.data) + # # Rack groups @@ -221,6 +231,13 @@ class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, queryset = Interface.objects.select_related('device') serializer_class = serializers.InterfaceSerializer + @detail_route() + def graphs(self, request, pk=None): + interface = get_object_or_404(Interface, pk=pk) + queryset = Graph.objects.filter(type=GRAPH_TYPE_INTERFACE) + serializer = GraphSerializer(queryset, many=True, context={'graphed_object': interface}) + return Response(serializer.data) + class DeviceInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): serializer_class = serializers.DeviceInterfaceSerializer @@ -272,7 +289,7 @@ def get_queryset(self): # class InterfaceConnectionViewSet(ModelViewSet): - queryset = InterfaceConnection.objects.all() + queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device') serializer_class = serializers.InterfaceConnectionSerializer From 173a6eee036a9a9b7a79dadd679e24a35e16776b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 30 Jan 2017 17:24:04 -0500 Subject: [PATCH 016/206] Moved rack units and device LLDP neighbors views into model viewsets --- netbox/dcim/api/urls.py | 6 ------ netbox/dcim/api/views.py | 35 +++++++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 27f3813f2d3..b10e857e8f4 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -55,12 +55,6 @@ url(r'', include(router.urls)), url(r'^devices/(?P\d+)/', include(device_router.urls)), - # Racks - url(r'^racks/(?P\d+)/rack-units/$', views.RackUnitListView.as_view(), name='rack_units'), - - # Devices - url(r'^devices/(?P\d+)/lldp-neighbors/$', views.LLDPNeighborsView.as_view(), name='device_lldp-neighbors'), - # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), url(r'^topology-maps/(?P[\w-]+)/$', TopologyMapView.as_view(), name='topology_map'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 696b306aae8..0ddc04b99a2 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -70,14 +70,11 @@ class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet): serializer_class = serializers.RackSerializer filter_class = filters.RackFilter - -class RackUnitListView(APIView): - """ - List rack units (by rack) - """ - - def get(self, request, pk): - + @detail_route(url_path='rack-units') + def rack_units(self, request, pk=None): + """ + List rack units (by rack) + """ rack = get_object_or_404(Rack, pk=pk) face = request.GET.get('face', 0) exclude_pk = request.GET.get('exclude', None) @@ -149,6 +146,28 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] + @detail_route(url_path='lldp-neighbors') + def lldp_neighbors(self, request, pk): + """ + Retrieve live LLDP neighbors of a device + """ + device = get_object_or_404(Device, pk=pk) + if not device.primary_ip: + raise ServiceUnavailable("No IP configured for this device.") + + RPC = device.get_rpc_client() + if not RPC: + raise ServiceUnavailable("No RPC client available for this platform ({}).".format(device.platform)) + + # Connect to device and retrieve inventory info + try: + with RPC(device, username=settings.NETBOX_USERNAME, password=settings.NETBOX_PASSWORD) as rpc_client: + lldp_neighbors = rpc_client.get_lldp_neighbors() + except: + raise ServiceUnavailable("Error connecting to the remote device.") + + return Response(lldp_neighbors) + # # Console Ports From 1fcc2b002956ca42763060601870da4d154022b3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Jan 2017 10:40:53 -0500 Subject: [PATCH 017/206] Namespaced all API URLs --- netbox/circuits/api/serializers.py | 3 +++ netbox/dcim/api/serializers.py | 24 ++++++++++++++++++++++-- netbox/dcim/api/views.py | 4 ++-- netbox/ipam/api/serializers.py | 11 ++++++++++- netbox/ipam/api/views.py | 2 +- netbox/netbox/urls.py | 10 +++++----- netbox/secrets/api/serializers.py | 1 + netbox/tenancy/api/serializers.py | 2 ++ 8 files changed, 46 insertions(+), 11 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 68b26cff789..a670b2e0181 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -21,6 +21,7 @@ class Meta: class NestedProviderSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') class Meta: model = Provider @@ -39,6 +40,7 @@ class Meta: class NestedCircuitTypeSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') class Meta: model = CircuitType @@ -76,6 +78,7 @@ class Meta: class NestedCircuitSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') class Meta: model = Circuit diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index e7c9ab80dc5..0c256c1ad17 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -28,6 +28,7 @@ class Meta: class NestedSiteSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') class Meta: model = Site @@ -47,6 +48,7 @@ class Meta: class NestedRackGroupSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') class Meta: model = RackGroup @@ -65,6 +67,7 @@ class Meta: class NestedRackRoleSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') class Meta: model = RackRole @@ -91,6 +94,7 @@ class Meta: class NestedRackSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') class Meta: model = Rack @@ -132,6 +136,7 @@ class Meta: class NestedManufacturerSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') class Meta: model = Manufacturer @@ -164,6 +169,8 @@ def get_subdevice_role(self, obj): class NestedDeviceTypeSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') + manufacturer = NestedManufacturerSerializer() class Meta: model = DeviceType @@ -224,6 +231,7 @@ class Meta: class NestedDeviceRoleSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') class Meta: model = DeviceRole @@ -242,6 +250,7 @@ class Meta: class NestedPlatformSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') class Meta: model = Platform @@ -254,6 +263,7 @@ class Meta: # Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency class DeviceIPAddressSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') class Meta: model = IPAddress @@ -295,6 +305,7 @@ def get_parent_device(self, obj): class NestedDeviceSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') class Meta: model = Device @@ -314,6 +325,7 @@ class Meta: class DeviceConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') class Meta: model = ConsoleServerPort @@ -334,6 +346,7 @@ class Meta: class DeviceConsolePortSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') class Meta: model = ConsolePort @@ -353,6 +366,7 @@ class Meta: class DevicePowerOutletSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') class Meta: model = PowerOutlet @@ -373,6 +387,7 @@ class Meta: class DevicePowerPortSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') class Meta: model = PowerPort @@ -408,6 +423,7 @@ def get_connected_interface(self, obj): class PeerInterfaceSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() class Meta: @@ -416,6 +432,7 @@ class Meta: class DeviceInterfaceSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') connection = serializers.SerializerMethodField() class Meta: @@ -442,6 +459,7 @@ class Meta: class NestedInterfaceConnectionSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') class Meta: model = InterfaceConnection @@ -461,7 +479,8 @@ class Meta: fields = ['id', 'device', 'name', 'installed_device'] -class ChildDeviceBaySerializer(serializers.HyperlinkedModelSerializer): +class DeviceDeviceBaySerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') class Meta: model = DeviceBay @@ -481,7 +500,8 @@ class Meta: fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] -class ChildModuleSerializer(serializers.HyperlinkedModelSerializer): +class DeviceModuleSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') class Meta: model = Module diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 0ddc04b99a2..1dd26b73dda 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -279,7 +279,7 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, class DeviceDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildDeviceBaySerializer + serializer_class = serializers.DeviceDeviceBaySerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) @@ -296,7 +296,7 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wri class DeviceModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildModuleSerializer + serializer_class = serializers.DeviceModuleSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 37b42af6c15..d4f60340ba8 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -20,6 +20,7 @@ class Meta: class NestedVRFSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') class Meta: model = VRF @@ -38,6 +39,7 @@ class Meta: class NestedRoleSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') class Meta: model = Role @@ -56,6 +58,7 @@ class Meta: class NestedRIRSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') class Meta: model = RIR @@ -75,6 +78,7 @@ class Meta: class NestedAggregateSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') class Meta(AggregateSerializer.Meta): model = Aggregate @@ -94,6 +98,7 @@ class Meta: class NestedVLANGroupSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') class Meta: model = VLANGroup @@ -119,6 +124,7 @@ class Meta: class NestedVLANSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') class Meta: model = VLAN @@ -145,6 +151,7 @@ class Meta: class NestedPrefixSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') class Meta: model = Prefix @@ -169,6 +176,7 @@ class Meta: class NestedIPAddressSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') class Meta: model = IPAddress @@ -191,7 +199,8 @@ class Meta: fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] -class ChildServiceSerializer(serializers.HyperlinkedModelSerializer): +class DeviceServiceSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') ipaddresses = NestedIPAddressSerializer(many=True) class Meta: diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index aba741291bc..766acd97a75 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -101,7 +101,7 @@ class ServiceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wr class DeviceServiceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.ChildServiceSerializer + serializer_class = serializers.DeviceServiceSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index fe6fff333e1..9042e0a6222 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -26,11 +26,11 @@ url(r'^profile/', include('users.urls', namespace='users')), # API - url(r'^api/circuits/', include('circuits.api.urls')), - url(r'^api/dcim/', include('dcim.api.urls')), - url(r'^api/ipam/', include('ipam.api.urls')), - url(r'^api/secrets/', include('secrets.api.urls')), - url(r'^api/tenancy/', include('tenancy.api.urls')), + url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), + url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), + url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), + url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), + url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), url(r'^api-auth/', include('rest_framework.urls')), # Error testing diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 40c024e1cce..a6ed6ed8c75 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -16,6 +16,7 @@ class Meta: class NestedSecretRoleSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail') class Meta: model = SecretRole diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index afd634129d7..4091e52615d 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -16,6 +16,7 @@ class Meta: class NestedTenantGroupSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') class Meta: model = TenantGroup @@ -35,6 +36,7 @@ class Meta: class NestedTenantSerializer(serializers.HyperlinkedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') class Meta: model = Tenant From e1cd846c9a06ff9eca1f6285bf3a54871ee20f51 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Jan 2017 12:19:41 -0500 Subject: [PATCH 018/206] Enabled creation of device components --- netbox/dcim/api/serializers.py | 8 ++++++- netbox/dcim/api/views.py | 42 ++++++++++++++++++++++++++++------ netbox/utilities/api.py | 5 ++-- 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 0c256c1ad17..244846672b0 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -330,6 +330,7 @@ class DeviceConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsoleServerPort fields = ['id', 'url', 'name', 'connected_console'] + read_only_fields = ['connected_console'] # @@ -351,6 +352,7 @@ class DeviceConsolePortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ConsolePort fields = ['id', 'url', 'name', 'cs_port', 'connection_status'] + read_only_fields = ['cs_port', 'connection_status'] # @@ -371,6 +373,7 @@ class DevicePowerOutletSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerOutlet fields = ['id', 'url', 'name', 'connected_port'] + read_only_fields = ['connected_port'] # @@ -392,6 +395,7 @@ class DevicePowerPortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = PowerPort fields = ['id', 'url', 'name', 'power_outlet', 'connection_status'] + read_only_fields = ['power_outlet', 'connection_status'] # @@ -485,6 +489,7 @@ class DeviceDeviceBaySerializer(serializers.HyperlinkedModelSerializer): class Meta: model = DeviceBay fields = ['id', 'url', 'name', 'installed_device'] + read_only_fields = ['installed_device'] # @@ -502,7 +507,8 @@ class Meta: class DeviceModuleSerializer(serializers.HyperlinkedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') + manufacturer = NestedManufacturerSerializer() class Meta: model = Module - fields = ['id', 'url', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] + fields = ['id', 'url', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 1dd26b73dda..0f8f5d7422b 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -179,13 +179,17 @@ class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.ConsolePortSerializer -class DeviceConsolePortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceConsolePortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return ConsolePort.objects.filter(device=device).select_related('cs_port') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Console Server Ports @@ -197,13 +201,17 @@ class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyMode serializer_class = serializers.ConsoleServerPortSerializer -class DeviceConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceConsoleServerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return ConsoleServerPort.objects.filter(device=device).select_related('connected_console') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Power Ports @@ -215,13 +223,17 @@ class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.PowerPortSerializer -class DevicePowerPortViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DevicePowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DevicePowerPortSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return PowerPort.objects.filter(device=device).select_related('power_outlet') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Power Outlets @@ -233,13 +245,17 @@ class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin serializer_class = serializers.PowerOutletSerializer -class DevicePowerOutletViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DevicePowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DevicePowerOutletSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return PowerOutlet.objects.filter(device=device).select_related('connected_port') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Interfaces @@ -258,7 +274,7 @@ def graphs(self, request, pk=None): return Response(serializer.data) -class DeviceInterfaceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceInterfaceSerializer filter_class = filters.InterfaceFilter @@ -267,6 +283,10 @@ def get_queryset(self): return Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Device bays @@ -278,13 +298,17 @@ class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, serializer_class = serializers.DeviceBaySerializer -class DeviceDeviceBayViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceDeviceBaySerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return DeviceBay.objects.filter(device=device).select_related('installed_device') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Modules @@ -295,13 +319,17 @@ class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, Wri serializer_class = serializers.ModuleSerializer -class DeviceModuleViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): +class DeviceModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): serializer_class = serializers.DeviceModuleSerializer def get_queryset(self): device = get_object_or_404(Device, pk=self.kwargs['pk']) return Module.objects.filter(device=device).select_related('device', 'manufacturer') + def perform_create(self, serializer): + device = get_object_or_404(Device, pk=self.kwargs['pk']) + serializer.save(device=device) + # # Interface connections diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index a69e4f36984..939ca3c0500 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -20,9 +20,8 @@ def get_serializer_class(self): class WritableSerializer(ModelSerializer): - class Meta: - model = self.get_queryset().model - fields = '__all__' + class Meta(self.serializer_class.Meta): + pass if self.action in WRITE_OPERATIONS: return WritableSerializer From bb1f97abc285a12496459953db3a82435e8185e1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 31 Jan 2017 15:35:09 -0500 Subject: [PATCH 019/206] Implemented static writable ModelSerializers for all models --- netbox/circuits/api/serializers.py | 35 ++++--- netbox/circuits/api/urls.py | 3 - netbox/circuits/api/views.py | 4 +- netbox/dcim/api/serializers.py | 145 ++++++++++++++++++----------- netbox/dcim/api/urls.py | 4 +- netbox/dcim/api/views.py | 8 +- netbox/extras/api/serializers.py | 65 +++++++------ netbox/ipam/api/serializers.py | 94 ++++++++++++++----- netbox/ipam/api/views.py | 6 ++ netbox/secrets/api/serializers.py | 2 +- netbox/tenancy/api/serializers.py | 18 +++- netbox/tenancy/api/views.py | 1 + netbox/utilities/api.py | 14 +-- 13 files changed, 255 insertions(+), 144 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index a670b2e0181..0cc2a277b27 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -2,7 +2,7 @@ from circuits.models import Provider, Circuit, CircuitTermination, CircuitType from dcim.api.serializers import NestedSiteSerializer, DeviceInterfaceSerializer -from extras.api.serializers import CustomFieldSerializer +from extras.api.serializers import CustomFieldValueSerializer from tenancy.api.serializers import NestedTenantSerializer @@ -10,17 +10,18 @@ # Providers # -class ProviderSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class ProviderSerializer(serializers.ModelSerializer): + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Provider fields = [ 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - 'custom_fields', + 'custom_field_values', ] -class NestedProviderSerializer(serializers.HyperlinkedModelSerializer): +class NestedProviderSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') class Meta: @@ -28,6 +29,15 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] +class WritableProviderSerializer(serializers.ModelSerializer): + + class Meta: + model = Provider + fields = [ + 'id', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + ] + + # # Circuit types # @@ -39,7 +49,7 @@ class Meta: fields = ['id', 'name', 'slug'] -class NestedCircuitTypeSerializer(serializers.HyperlinkedModelSerializer): +class NestedCircuitTypeSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') class Meta: @@ -64,20 +74,21 @@ class Meta: # Circuits # -class CircuitSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class CircuitSerializer(serializers.ModelSerializer): provider = NestedProviderSerializer() type = NestedCircuitTypeSerializer() tenant = NestedTenantSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Circuit fields = [ 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - 'custom_fields', + 'custom_field_values', ] -class NestedCircuitSerializer(serializers.HyperlinkedModelSerializer): +class NestedCircuitSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') class Meta: @@ -85,12 +96,10 @@ class Meta: fields = ['id', 'url', 'cid'] -# TODO: Delete this -class CircuitDetailSerializer(CircuitSerializer): - terminations = CircuitTerminationSerializer(many=True) +class WritableCircuitSerializer(serializers.ModelSerializer): - class Meta(CircuitSerializer.Meta): + class Meta: + model = Circuit fields = [ 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', - 'terminations', 'custom_fields', ] diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index f06b14165aa..59739e51085 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -2,9 +2,6 @@ from rest_framework import routers -from extras.models import GRAPH_TYPE_PROVIDER -from extras.api.views import GraphListView - from . import views diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 1fdafa97bdc..97d619fd2ce 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -21,9 +21,10 @@ # Providers # -class ProviderViewSet(CustomFieldModelViewSet): +class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer + write_serializer_class = serializers.WritableProviderSerializer @detail_route() def graphs(self, request, pk=None): @@ -49,6 +50,7 @@ class CircuitTypeViewSet(ModelViewSet): class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Circuit.objects.select_related('type', 'tenant', 'provider') serializer_class = serializers.CircuitSerializer + write_serializer_class = serializers.WritableCircuitSerializer filter_class = CircuitFilter diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 244846672b0..1585109f1cb 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -4,10 +4,10 @@ from dcim.models import ( ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceBay, DeviceType, DeviceRole, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, Module, Platform, PowerOutlet, - PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, RACK_FACE_FRONT, RACK_FACE_REAR, Site, - SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, + PowerOutletTemplate, PowerPort, PowerPortTemplate, Rack, RackGroup, RackRole, Site, SUBDEVICE_ROLE_CHILD, + SUBDEVICE_ROLE_PARENT, ) -from extras.api.serializers import CustomFieldSerializer +from extras.api.serializers import CustomFieldValueSerializer from tenancy.api.serializers import NestedTenantSerializer @@ -15,19 +15,20 @@ # Sites # -class SiteSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class SiteSerializer(serializers.ModelSerializer): tenant = NestedTenantSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Site fields = [ 'id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', - 'contact_phone', 'contact_email', 'comments', 'custom_fields', 'count_prefixes', 'count_vlans', + 'contact_phone', 'contact_email', 'comments', 'custom_field_values', 'count_prefixes', 'count_vlans', 'count_racks', 'count_devices', 'count_circuits', ] -class NestedSiteSerializer(serializers.HyperlinkedModelSerializer): +class NestedSiteSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') class Meta: @@ -35,6 +36,16 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] +class WritableSiteSerializer(serializers.ModelSerializer): + + class Meta: + model = Site + fields = [ + 'id', 'name', 'slug', 'tenant', 'facility', 'asn', 'physical_address', 'shipping_address', 'contact_name', + 'contact_phone', 'contact_email', 'comments', + ] + + # # Rack groups # @@ -47,7 +58,7 @@ class Meta: fields = ['id', 'name', 'slug', 'site'] -class NestedRackGroupSerializer(serializers.HyperlinkedModelSerializer): +class NestedRackGroupSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackgroup-detail') class Meta: @@ -55,6 +66,13 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] +class WritableRackGroupSerializer(serializers.ModelSerializer): + + class Meta: + model = RackGroup + fields = ['id', 'name', 'slug', 'site'] + + # # Rack roles # @@ -66,7 +84,7 @@ class Meta: fields = ['id', 'name', 'slug', 'color'] -class NestedRackRoleSerializer(serializers.HyperlinkedModelSerializer): +class NestedRackRoleSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') class Meta: @@ -79,21 +97,22 @@ class Meta: # -class RackSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class RackSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() group = NestedRackGroupSerializer() tenant = NestedTenantSerializer() role = NestedRackRoleSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Rack fields = [ 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', - 'desc_units', 'comments', 'custom_fields', + 'desc_units', 'comments', 'custom_field_values', ] -class NestedRackSerializer(serializers.HyperlinkedModelSerializer): +class NestedRackSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') class Meta: @@ -101,28 +120,15 @@ class Meta: fields = ['id', 'url', 'name', 'display_name'] -class RackDetailSerializer(RackSerializer): - front_units = serializers.SerializerMethodField() - rear_units = serializers.SerializerMethodField() +class WritableRackSerializer(serializers.ModelSerializer): - class Meta(RackSerializer.Meta): + class Meta: + model = Rack fields = [ - 'id', 'name', 'facility_id', 'display_name', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', - 'desc_units', 'comments', 'custom_fields', 'front_units', 'rear_units', + 'id', 'name', 'facility_id', 'site', 'group', 'tenant', 'role', 'type', 'width', 'u_height', 'desc_units', + 'comments', ] - def get_front_units(self, obj): - units = obj.get_rack_units(face=RACK_FACE_FRONT) - for u in units: - u['device'] = NestedDeviceSerializer(u['device']).data if u['device'] else None - return units - - def get_rear_units(self, obj): - units = obj.get_rack_units(face=RACK_FACE_REAR) - for u in units: - u['device'] = NestedDeviceSerializer(u['device']).data if u['device'] else None - return units - # # Manufacturers @@ -135,7 +141,7 @@ class Meta: fields = ['id', 'name', 'slug'] -class NestedManufacturerSerializer(serializers.HyperlinkedModelSerializer): +class NestedManufacturerSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') class Meta: @@ -147,16 +153,17 @@ class Meta: # Device types # -class DeviceTypeSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class DeviceTypeSerializer(serializers.ModelSerializer): manufacturer = NestedManufacturerSerializer() subdevice_role = serializers.SerializerMethodField() instance_count = serializers.IntegerField(source='instances.count', read_only=True) + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = DeviceType fields = [ 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', - 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_fields', + 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', 'custom_field_values', 'instance_count', ] @@ -168,7 +175,7 @@ def get_subdevice_role(self, obj): }[obj.subdevice_role] -class NestedDeviceTypeSerializer(serializers.HyperlinkedModelSerializer): +class NestedDeviceTypeSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') manufacturer = NestedManufacturerSerializer() @@ -177,6 +184,16 @@ class Meta: fields = ['id', 'url', 'manufacturer', 'model', 'slug'] +class WritableDeviceTypeSerializer(serializers.ModelSerializer): + + class Meta: + model = DeviceType + fields = [ + 'id', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'interface_ordering', + 'is_console_server', 'is_pdu', 'is_network_device', 'subdevice_role', 'comments', + ] + + class ConsolePortTemplateSerializer(serializers.ModelSerializer): class Meta: @@ -230,7 +247,7 @@ class Meta: fields = ['id', 'name', 'slug', 'color'] -class NestedDeviceRoleSerializer(serializers.HyperlinkedModelSerializer): +class NestedDeviceRoleSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') class Meta: @@ -249,7 +266,7 @@ class Meta: fields = ['id', 'name', 'slug', 'rpc_client'] -class NestedPlatformSerializer(serializers.HyperlinkedModelSerializer): +class NestedPlatformSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') class Meta: @@ -262,7 +279,7 @@ class Meta: # # Cannot import ipam.api.NestedIPAddressSerializer due to circular dependency -class DeviceIPAddressSerializer(serializers.HyperlinkedModelSerializer): +class DeviceIPAddressSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') class Meta: @@ -270,7 +287,7 @@ class Meta: fields = ['id', 'url', 'family', 'address'] -class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class DeviceSerializer(serializers.ModelSerializer): device_type = NestedDeviceTypeSerializer() device_role = NestedDeviceRoleSerializer() tenant = NestedTenantSerializer() @@ -280,13 +297,14 @@ class DeviceSerializer(CustomFieldSerializer, serializers.ModelSerializer): primary_ip4 = DeviceIPAddressSerializer() primary_ip6 = DeviceIPAddressSerializer() parent_device = serializers.SerializerMethodField() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Device fields = [ 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', - 'comments', 'custom_fields', + 'comments', 'custom_field_values', ] def get_parent_device(self, obj): @@ -304,7 +322,7 @@ def get_parent_device(self, obj): } -class NestedDeviceSerializer(serializers.HyperlinkedModelSerializer): +class NestedDeviceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') class Meta: @@ -312,19 +330,29 @@ class Meta: fields = ['id', 'url', 'name', 'display_name'] +class WritableDeviceSerializer(serializers.ModelSerializer): + + class Meta: + model = Device + fields = [ + 'id', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'rack', + 'position', 'face', 'status', 'primary_ip4', 'primary_ip6', 'comments', + ] + + # # Console server ports # class ConsoleServerPortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) class Meta: model = ConsoleServerPort fields = ['id', 'device', 'name', 'connected_console'] -class DeviceConsoleServerPortSerializer(serializers.HyperlinkedModelSerializer): +class DeviceConsoleServerPortSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') class Meta: @@ -338,7 +366,7 @@ class Meta: # class ConsolePortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) cs_port = ConsoleServerPortSerializer() class Meta: @@ -346,7 +374,7 @@ class Meta: fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] -class DeviceConsolePortSerializer(serializers.HyperlinkedModelSerializer): +class DeviceConsolePortSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') class Meta: @@ -360,14 +388,14 @@ class Meta: # class PowerOutletSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) class Meta: model = PowerOutlet fields = ['id', 'device', 'name', 'connected_port'] -class DevicePowerOutletSerializer(serializers.HyperlinkedModelSerializer): +class DevicePowerOutletSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') class Meta: @@ -381,7 +409,7 @@ class Meta: # class PowerPortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) power_outlet = PowerOutletSerializer() class Meta: @@ -389,7 +417,7 @@ class Meta: fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] -class DevicePowerPortSerializer(serializers.HyperlinkedModelSerializer): +class DevicePowerPortSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') class Meta: @@ -404,7 +432,7 @@ class Meta: class InterfaceSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) connection = serializers.SerializerMethodField(read_only=True) connected_interface = serializers.SerializerMethodField(read_only=True) @@ -426,7 +454,7 @@ def get_connected_interface(self, obj): return None -class PeerInterfaceSerializer(serializers.HyperlinkedModelSerializer): +class PeerInterfaceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') device = NestedDeviceSerializer() @@ -435,7 +463,7 @@ class Meta: fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] -class DeviceInterfaceSerializer(serializers.HyperlinkedModelSerializer): +class DeviceInterfaceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') connection = serializers.SerializerMethodField() @@ -462,7 +490,7 @@ class Meta: fields = ['id', 'interface_a', 'interface_b', 'connection_status'] -class NestedInterfaceConnectionSerializer(serializers.HyperlinkedModelSerializer): +class NestedInterfaceConnectionSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') class Meta: @@ -470,12 +498,19 @@ class Meta: fields = ['id', 'url', 'connection_status'] +class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): + + class Meta: + model = InterfaceConnection + fields = ['id', 'interface_a', 'interface_b', 'connection_status'] + + # # Device bays # class DeviceBaySerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) installed_device = NestedDeviceSerializer() class Meta: @@ -483,7 +518,7 @@ class Meta: fields = ['id', 'device', 'name', 'installed_device'] -class DeviceDeviceBaySerializer(serializers.HyperlinkedModelSerializer): +class DeviceDeviceBaySerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') class Meta: @@ -497,7 +532,7 @@ class Meta: # class ModuleSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer() + device = NestedDeviceSerializer(read_only=True) manufacturer = NestedManufacturerSerializer() class Meta: @@ -505,7 +540,7 @@ class Meta: fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] -class DeviceModuleSerializer(serializers.HyperlinkedModelSerializer): +class DeviceModuleSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') manufacturer = NestedManufacturerSerializer() diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index b10e857e8f4..0bf692dbbea 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -2,10 +2,8 @@ from rest_framework import routers -from extras.models import GRAPH_TYPE_INTERFACE, GRAPH_TYPE_SITE -from extras.api.views import GraphListView, TopologyMapView +from extras.api.views import TopologyMapView from ipam.api.views import ServiceViewSet, DeviceServiceViewSet - from . import views diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 0f8f5d7422b..27724e28955 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -33,6 +33,7 @@ class SiteViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Site.objects.select_related('tenant') serializer_class = serializers.SiteSerializer + write_serializer_class = serializers.WritableSiteSerializer @detail_route() def graphs(self, request, pk=None): @@ -50,6 +51,7 @@ class RackGroupViewSet(WritableSerializerMixin, ModelViewSet): queryset = RackGroup.objects.select_related('site') serializer_class = serializers.RackGroupSerializer filter_class = filters.RackGroupFilter + write_serializer_class = serializers.WritableRackGroupSerializer # @@ -68,6 +70,7 @@ class RackRoleViewSet(ModelViewSet): class RackViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Rack.objects.select_related('site', 'group__site', 'tenant') serializer_class = serializers.RackSerializer + write_serializer_class = serializers.WritableRackSerializer filter_class = filters.RackFilter @detail_route(url_path='rack-units') @@ -112,6 +115,7 @@ class ManufacturerViewSet(ModelViewSet): class DeviceTypeViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = DeviceType.objects.select_related('manufacturer') serializer_class = serializers.DeviceTypeSerializer + write_serializer_class = serializers.WritableDeviceTypeSerializer # @@ -143,6 +147,7 @@ class DeviceViewSet(WritableSerializerMixin, CustomFieldModelViewSet): 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', ) serializer_class = serializers.DeviceSerializer + write_serializer_class = serializers.WritableDeviceSerializer filter_class = filters.DeviceFilter renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [BINDZoneRenderer, FlatJSONRenderer] @@ -335,9 +340,10 @@ def perform_create(self, serializer): # Interface connections # -class InterfaceConnectionViewSet(ModelViewSet): +class InterfaceConnectionViewSet(WritableSerializerMixin, ModelViewSet): queryset = InterfaceConnection.objects.select_related('interface_a__device', 'interface_b__device') serializer_class = serializers.InterfaceConnectionSerializer + write_serializer_class = serializers.WritableInterfaceConnectionSerializer # diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 01d348b0a35..2dec2a36016 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,35 +1,42 @@ from rest_framework import serializers -from extras.models import CF_TYPE_SELECT, CustomFieldChoice, Graph +from extras.models import CF_TYPE_SELECT, CustomFieldChoice, CustomFieldValue, Graph + + +# class CustomFieldSerializer(serializers.ModelSerializer): +# """ +# Extends ModelSerializer to render any CustomFields and their values associated with an object. +# """ +# custom_fields = serializers.SerializerMethodField() +# +# def get_custom_fields(self, obj): +# +# # Gather all CustomFields applicable to this object +# fields = {cf.name: None for cf in self.context['custom_fields']} +# custom_field_choices = self.context['custom_field_choices'] +# +# # Attach any defined CustomFieldValues to their respective CustomFields +# for cfv in obj.custom_field_values.all(): +# +# # Attempt to suppress database lookups for CustomFieldChoices by using the cached choice set from the view +# # context. +# if cfv.field.type == CF_TYPE_SELECT: +# cfc = { +# 'id': int(cfv.serialized_value), +# 'value': custom_field_choices[int(cfv.serialized_value)] +# } +# fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfc).data +# else: +# fields[cfv.field.name] = cfv.value +# +# return fields + + +class CustomFieldValueSerializer(serializers.ModelSerializer): - -class CustomFieldSerializer(serializers.Serializer): - """ - Extends a ModelSerializer to render any CustomFields and their values associated with an object. - """ - custom_fields = serializers.SerializerMethodField() - - def get_custom_fields(self, obj): - - # Gather all CustomFields applicable to this object - fields = {cf.name: None for cf in self.context['custom_fields']} - custom_field_choices = self.context['custom_field_choices'] - - # Attach any defined CustomFieldValues to their respective CustomFields - for cfv in obj.custom_field_values.all(): - - # Attempt to suppress database lookups for CustomFieldChoices by using the cached choice set from the view - # context. - if cfv.field.type == CF_TYPE_SELECT: - cfc = { - 'id': int(cfv.serialized_value), - 'value': custom_field_choices[int(cfv.serialized_value)] - } - fields[cfv.field.name] = CustomFieldChoiceSerializer(instance=cfc).data - else: - fields[cfv.field.name] = cfv.value - - return fields + class Meta: + model = CustomFieldValue + fields = ['field', 'serialized_value'] class CustomFieldChoiceSerializer(serializers.ModelSerializer): diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index d4f60340ba8..9104f55b9e0 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,25 +1,25 @@ from rest_framework import serializers from dcim.api.serializers import NestedDeviceSerializer, DeviceInterfaceSerializer, NestedSiteSerializer -from extras.api.serializers import CustomFieldSerializer +from extras.api.serializers import CustomFieldValueSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer -from utilities.api import WritableSerializerMixin # # VRFs # -class VRFSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class VRFSerializer(serializers.ModelSerializer): tenant = NestedTenantSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = VRF - fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_fields'] + fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'custom_field_values'] -class NestedVRFSerializer(serializers.HyperlinkedModelSerializer): +class NestedVRFSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') class Meta: @@ -27,6 +27,13 @@ class Meta: fields = ['id', 'url', 'name', 'rd'] +class WritableVRFSerializer(serializers.ModelSerializer): + + class Meta: + model = VRF + fields = ['id', 'name', 'rd', 'tenant', 'enforce_unique', 'description'] + + # # Roles # @@ -38,7 +45,7 @@ class Meta: fields = ['id', 'name', 'slug', 'weight'] -class NestedRoleSerializer(serializers.HyperlinkedModelSerializer): +class NestedRoleSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') class Meta: @@ -57,7 +64,7 @@ class Meta: fields = ['id', 'name', 'slug', 'is_private'] -class NestedRIRSerializer(serializers.HyperlinkedModelSerializer): +class NestedRIRSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') class Meta: @@ -69,15 +76,16 @@ class Meta: # Aggregates # -class AggregateSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class AggregateSerializer(serializers.ModelSerializer): rir = NestedRIRSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Aggregate - fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_fields'] + fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description', 'custom_field_values'] -class NestedAggregateSerializer(serializers.HyperlinkedModelSerializer): +class NestedAggregateSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') class Meta(AggregateSerializer.Meta): @@ -85,11 +93,18 @@ class Meta(AggregateSerializer.Meta): fields = ['id', 'url', 'family', 'prefix'] +class WritableAggregateSerializer(serializers.ModelSerializer): + + class Meta: + model = Aggregate + fields = ['id', 'family', 'prefix', 'rir', 'date_added', 'description'] + + # # VLAN groups # -class VLANGroupSerializer(WritableSerializerMixin, serializers.ModelSerializer): +class VLANGroupSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() class Meta: @@ -97,7 +112,7 @@ class Meta: fields = ['id', 'name', 'slug', 'site'] -class NestedVLANGroupSerializer(serializers.HyperlinkedModelSerializer): +class NestedVLANGroupSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') class Meta: @@ -105,25 +120,33 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] +class WritableVLANGroupSerializer(serializers.ModelSerializer): + + class Meta: + model = VLANGroup + fields = ['id', 'name', 'slug', 'site'] + + # # VLANs # -class VLANSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class VLANSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() group = NestedVLANGroupSerializer() tenant = NestedTenantSerializer() role = NestedRoleSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = VLAN fields = [ 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', 'display_name', - 'custom_fields', + 'custom_field_values', ] -class NestedVLANSerializer(serializers.HyperlinkedModelSerializer): +class NestedVLANSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') class Meta: @@ -131,26 +154,36 @@ class Meta: fields = ['id', 'url', 'vid', 'name', 'display_name'] +class WritableVLANSerializer(serializers.ModelSerializer): + + class Meta: + model = VLAN + fields = [ + 'id', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', + ] + + # # Prefixes # -class PrefixSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class PrefixSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() vrf = NestedVRFSerializer() tenant = NestedTenantSerializer() vlan = NestedVLANSerializer() role = NestedRoleSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Prefix fields = [ 'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', - 'custom_fields', + 'custom_field_values', ] -class NestedPrefixSerializer(serializers.HyperlinkedModelSerializer): +class NestedPrefixSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') class Meta: @@ -158,24 +191,34 @@ class Meta: fields = ['id', 'url', 'family', 'prefix'] +class WritablePrefixSerializer(serializers.ModelSerializer): + + class Meta: + model = Prefix + fields = [ + 'id', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', 'description', + ] + + # # IP addresses # -class IPAddressSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class IPAddressSerializer(serializers.ModelSerializer): vrf = NestedVRFSerializer() tenant = NestedTenantSerializer() interface = DeviceInterfaceSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = IPAddress fields = [ 'id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside', - 'nat_outside', 'custom_fields', + 'nat_outside', 'custom_field_values', ] -class NestedIPAddressSerializer(serializers.HyperlinkedModelSerializer): +class NestedIPAddressSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') class Meta: @@ -186,6 +229,13 @@ class Meta: IPAddressSerializer._declared_fields['nat_outside'] = NestedIPAddressSerializer() +class WritableIPAddressSerializer(serializers.ModelSerializer): + + class Meta: + model = IPAddress + fields = ['id', 'family', 'address', 'vrf', 'tenant', 'status', 'interface', 'description', 'nat_inside'] + + # # Services # @@ -199,7 +249,7 @@ class Meta: fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] -class DeviceServiceSerializer(serializers.HyperlinkedModelSerializer): +class DeviceServiceSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') ipaddresses = NestedIPAddressSerializer(many=True) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 766acd97a75..a957845785f 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -20,6 +20,7 @@ class VRFViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = VRF.objects.select_related('tenant') serializer_class = serializers.VRFSerializer + write_serializer_class = serializers.WritableVRFSerializer filter_class = filters.VRFFilter @@ -48,6 +49,7 @@ class RIRViewSet(ModelViewSet): class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Aggregate.objects.select_related('rir') serializer_class = serializers.AggregateSerializer + write_serializer_class = serializers.WritableAggregateSerializer filter_class = filters.AggregateFilter @@ -58,6 +60,7 @@ class AggregateViewSet(WritableSerializerMixin, CustomFieldModelViewSet): class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') serializer_class = serializers.PrefixSerializer + write_serializer_class = serializers.WritablePrefixSerializer filter_class = filters.PrefixFilter @@ -68,6 +71,7 @@ class PrefixViewSet(WritableSerializerMixin, CustomFieldModelViewSet): class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = IPAddress.objects.select_related('vrf__tenant', 'tenant', 'interface__device', 'nat_inside') serializer_class = serializers.IPAddressSerializer + write_serializer_class = serializers.WritableIPAddressSerializer filter_class = filters.IPAddressFilter @@ -78,6 +82,7 @@ class IPAddressViewSet(WritableSerializerMixin, CustomFieldModelViewSet): class VLANGroupViewSet(WritableSerializerMixin, ModelViewSet): queryset = VLANGroup.objects.select_related('site') serializer_class = serializers.VLANGroupSerializer + write_serializer_class = serializers.WritableVLANGroupSerializer filter_class = filters.VLANGroupFilter @@ -88,6 +93,7 @@ class VLANGroupViewSet(WritableSerializerMixin, ModelViewSet): class VLANViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') serializer_class = serializers.VLANSerializer + write_serializer_class = serializers.WritableVLANSerializer filter_class = filters.VLANFilter diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index a6ed6ed8c75..0b70e7cf8c4 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -15,7 +15,7 @@ class Meta: fields = ['id', 'name', 'slug'] -class NestedSecretRoleSerializer(serializers.HyperlinkedModelSerializer): +class NestedSecretRoleSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='secrets-api:secretrole-detail') class Meta: diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 4091e52615d..46e6edf63fd 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from extras.api.serializers import CustomFieldSerializer +from extras.api.serializers import CustomFieldValueSerializer from tenancy.models import Tenant, TenantGroup @@ -15,7 +15,7 @@ class Meta: fields = ['id', 'name', 'slug'] -class NestedTenantGroupSerializer(serializers.HyperlinkedModelSerializer): +class NestedTenantGroupSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') class Meta: @@ -27,17 +27,25 @@ class Meta: # Tenants # -class TenantSerializer(CustomFieldSerializer, serializers.ModelSerializer): +class TenantSerializer(serializers.ModelSerializer): group = NestedTenantGroupSerializer() + custom_field_values = CustomFieldValueSerializer(many=True) class Meta: model = Tenant - fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_fields'] + fields = ['id', 'name', 'slug', 'group', 'description', 'comments', 'custom_field_values'] -class NestedTenantSerializer(serializers.HyperlinkedModelSerializer): +class NestedTenantSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') class Meta: model = Tenant fields = ['id', 'url', 'name', 'slug'] + + +class WritableTenantSerializer(serializers.ModelSerializer): + + class Meta: + model = Tenant + fields = ['id', 'name', 'slug', 'group', 'description', 'comments'] diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index d288dd9f240..ae5069271c7 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -24,4 +24,5 @@ class TenantGroupViewSet(ModelViewSet): class TenantViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Tenant.objects.select_related('group') serializer_class = serializers.TenantSerializer + write_serializer_class = serializers.WritableTenantSerializer filter_class = TenantFilter diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 939ca3c0500..818eb82896a 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -12,18 +12,10 @@ class ServiceUnavailable(APIException): class WritableSerializerMixin(object): """ - Returns a flat Serializer from the given model suitable for write operations (POST, PUT, PATCH). This is necessary - to allow write operations on objects which utilize nested serializers. + Allow for the use of an alternate, writable serializer class for write operations (e.g. POST, PUT). """ def get_serializer_class(self): - - class WritableSerializer(ModelSerializer): - - class Meta(self.serializer_class.Meta): - pass - - if self.action in WRITE_OPERATIONS: - return WritableSerializer - + if self.action in WRITE_OPERATIONS and hasattr(self, 'write_serializer_class'): + return self.write_serializer_class return self.serializer_class From f52c247bd568ce18d564c9d33effcddcf0571e96 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 12:37:19 -0500 Subject: [PATCH 020/206] Re-implemented Swagger now that URL resolution has been fixed --- netbox/netbox/settings.py | 1 + netbox/netbox/urls.py | 1 + netbox/templates/_base.html | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 74bf6ae1054..56b774f9a13 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -105,6 +105,7 @@ 'debug_toolbar', 'django_tables2', 'rest_framework', + 'rest_framework_swagger', 'circuits', 'dcim', 'ipam', diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index 9042e0a6222..eb6b67fe708 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -31,6 +31,7 @@ url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), + url(r'^api/docs/', include('rest_framework_swagger.urls')), url(r'^api-auth/', include('rest_framework.urls')), # Error testing diff --git a/netbox/templates/_base.html b/netbox/templates/_base.html index d94081f65d7..4e63cf337d9 100644 --- a/netbox/templates/_base.html +++ b/netbox/templates/_base.html @@ -289,7 +289,7 @@

Maintenance Mode

Docs · - API · + API · Code

From 6e10fea119a4508052d63188bd791e19bc48501a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 14:04:45 -0500 Subject: [PATCH 021/206] Started API documentation --- docs/api/structure.md | 32 ++++++++++++++++++++++++++++++++ mkdocs.yml | 2 ++ 2 files changed, 34 insertions(+) create mode 100644 docs/api/structure.md diff --git a/docs/api/structure.md b/docs/api/structure.md new file mode 100644 index 00000000000..437ba9bfd63 --- /dev/null +++ b/docs/api/structure.md @@ -0,0 +1,32 @@ +# URL Hierarchy + +The API's URL structure is divided at the root level by application: circuits, DCIM, IPAM, secrets, and tenancy. Within each application, each model has its own path. For example: + +* /api/circuits/circuits/ +* /api/circuits/providers/ +* /api/dcim/sites/ +* /api/dcim/racks/ +* /api/dcim/devices/ + +Each model generally has two URLs associated with it: a list URL, and a detail URL. the list URL is used to request a list of multiple objects or to create a new object. The detail URL is used to retrieve, update, or delete an existing object. + +* /api/dcim/devices/ - List devices or create a new device +* /api/dcim/devices/123/ - Retrieve, update, or delete the device with ID 123 + +Lists of objects can be filtered using a set of query parameters. For example, to find all interfaces belonging to the device with ID 123: + +* /api/dcim/interfaces/?device_id=123 + +# Serializers + +The NetBox API employs three types of serializers to represent model data: + +* Base serializer +* Nested serializer +* Writable serializer + +The base serializer is used to represent the default view of a model. This includes all database table fields which comprise the model, and may include additional metadata. A base serializer includes relationships to parent objects, but **does not** include child objects. For example, the `VLANSerializer` includes a nested representation its parent VLANGroup (if any), but does not include any assigned Prefixes. + +Related objects (e.g. `ForeignKey` fields) are represented using a nested serializer. A nested serializer provides a minimal representation of an object, including only its URL and enough information to construct its name. + +When a base serializer includes one or more nested serializers, the hierarchical structure precludes it from being used for write operations. Thus, a flat representation of an object may be provided using a writable serializer. This serializer includes only raw database values and is not typically used for retrieval, except as part of the response to the creation or updating of an object. diff --git a/mkdocs.yml b/mkdocs.yml index 9a96fe0f75d..07f3194689b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,8 @@ pages: - 'Secrets': 'data-model/secrets.md' - 'Tenancy': 'data-model/tenancy.md' - 'Extras': 'data-model/extras.md' + - 'API': + - 'Structure': 'api/structure.md' - 'API Integration': 'api-integration.md' markdown_extensions: From 77e5450746a612f8840eb1ca18523600b431de1d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 14:34:19 -0500 Subject: [PATCH 022/206] Removed all device-specific API endpoints --- netbox/circuits/api/serializers.py | 4 +- netbox/dcim/api/serializers.py | 117 +++++++++------------ netbox/dcim/api/urls.py | 22 ++-- netbox/dcim/api/views.py | 159 +++++------------------------ netbox/dcim/filters.py | 40 +++++++- netbox/ipam/api/serializers.py | 10 +- netbox/ipam/api/views.py | 19 +--- 7 files changed, 130 insertions(+), 241 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 0cc2a277b27..ec6d6a18ad5 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from circuits.models import Provider, Circuit, CircuitTermination, CircuitType -from dcim.api.serializers import NestedSiteSerializer, DeviceInterfaceSerializer +from dcim.api.serializers import NestedSiteSerializer, InterfaceSerializer from extras.api.serializers import CustomFieldValueSerializer from tenancy.api.serializers import NestedTenantSerializer @@ -63,7 +63,7 @@ class Meta: class CircuitTerminationSerializer(serializers.ModelSerializer): site = NestedSiteSerializer() - interface = DeviceInterfaceSerializer() + interface = InterfaceSerializer() class Meta: model = CircuitTermination diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1585109f1cb..0fa883d40b7 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -345,20 +345,18 @@ class Meta: # class ConsoleServerPortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() class Meta: model = ConsoleServerPort fields = ['id', 'device', 'name', 'connected_console'] -class DeviceConsoleServerPortSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') +class WritableConsoleServerPortSerializer(serializers.ModelSerializer): class Meta: model = ConsoleServerPort - fields = ['id', 'url', 'name', 'connected_console'] - read_only_fields = ['connected_console'] + fields = ['id', 'device', 'name', 'connected_console'] # @@ -366,7 +364,7 @@ class Meta: # class ConsolePortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() cs_port = ConsoleServerPortSerializer() class Meta: @@ -374,13 +372,11 @@ class Meta: fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] -class DeviceConsolePortSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') +class WritableConsolePortSerializer(serializers.ModelSerializer): class Meta: model = ConsolePort - fields = ['id', 'url', 'name', 'cs_port', 'connection_status'] - read_only_fields = ['cs_port', 'connection_status'] + fields = ['id', 'device', 'name', 'cs_port', 'connection_status'] # @@ -388,20 +384,18 @@ class Meta: # class PowerOutletSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() class Meta: model = PowerOutlet fields = ['id', 'device', 'name', 'connected_port'] -class DevicePowerOutletSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') +class WritablePowerOutletSerializer(serializers.ModelSerializer): class Meta: model = PowerOutlet - fields = ['id', 'url', 'name', 'connected_port'] - read_only_fields = ['connected_port'] + fields = ['id', 'device', 'name', 'connected_port'] # @@ -409,7 +403,7 @@ class Meta: # class PowerPortSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() power_outlet = PowerOutletSerializer() class Meta: @@ -417,13 +411,11 @@ class Meta: fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] -class DevicePowerPortSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') +class WritablePowerPortSerializer(serializers.ModelSerializer): class Meta: model = PowerPort - fields = ['id', 'url', 'name', 'power_outlet', 'connection_status'] - read_only_fields = ['power_outlet', 'connection_status'] + fields = ['id', 'device', 'name', 'power_outlet', 'connection_status'] # @@ -432,7 +424,7 @@ class Meta: class InterfaceSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() connection = serializers.SerializerMethodField(read_only=True) connected_interface = serializers.SerializerMethodField(read_only=True) @@ -463,46 +455,11 @@ class Meta: fields = ['id', 'url', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] -class DeviceInterfaceSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') - connection = serializers.SerializerMethodField() +class WritableInterfaceSerializer(serializers.ModelSerializer): class Meta: model = Interface - fields = ['id', 'url', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description', 'connection'] - - def get_connection(self, obj): - if obj.connection: - return NestedInterfaceConnectionSerializer(obj.connection, context=self.context).data - return None - - -# -# Interface connections -# - -class InterfaceConnectionSerializer(serializers.ModelSerializer): - interface_a = PeerInterfaceSerializer() - interface_b = PeerInterfaceSerializer() - - class Meta: - model = InterfaceConnection - fields = ['id', 'interface_a', 'interface_b', 'connection_status'] - - -class NestedInterfaceConnectionSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') - - class Meta: - model = InterfaceConnection - fields = ['id', 'url', 'connection_status'] - - -class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): - - class Meta: - model = InterfaceConnection - fields = ['id', 'interface_a', 'interface_b', 'connection_status'] + fields = ['id', 'device', 'name', 'form_factor', 'mac_address', 'mgmt_only', 'description'] # @@ -510,7 +467,7 @@ class Meta: # class DeviceBaySerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() installed_device = NestedDeviceSerializer() class Meta: @@ -518,13 +475,11 @@ class Meta: fields = ['id', 'device', 'name', 'installed_device'] -class DeviceDeviceBaySerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') +class WritableDeviceBaySerializer(serializers.ModelSerializer): class Meta: model = DeviceBay - fields = ['id', 'url', 'name', 'installed_device'] - read_only_fields = ['installed_device'] + fields = ['id', 'device', 'name'] # @@ -532,7 +487,7 @@ class Meta: # class ModuleSerializer(serializers.ModelSerializer): - device = NestedDeviceSerializer(read_only=True) + device = NestedDeviceSerializer() manufacturer = NestedManufacturerSerializer() class Meta: @@ -540,10 +495,36 @@ class Meta: fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] -class DeviceModuleSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - manufacturer = NestedManufacturerSerializer() +class WritableModuleSerializer(serializers.ModelSerializer): class Meta: model = Module - fields = ['id', 'url', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] + fields = ['id', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'discovered'] + + +# +# Interface connections +# + +class InterfaceConnectionSerializer(serializers.ModelSerializer): + interface_a = PeerInterfaceSerializer() + interface_b = PeerInterfaceSerializer() + + class Meta: + model = InterfaceConnection + fields = ['id', 'interface_a', 'interface_b', 'connection_status'] + + +class NestedInterfaceConnectionSerializer(serializers.ModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfaceconnection-detail') + + class Meta: + model = InterfaceConnection + fields = ['id', 'url', 'connection_status'] + + +class WritableInterfaceConnectionSerializer(serializers.ModelSerializer): + + class Meta: + model = InterfaceConnection + fields = ['id', 'interface_a', 'interface_b', 'connection_status'] diff --git a/netbox/dcim/api/urls.py b/netbox/dcim/api/urls.py index 0bf692dbbea..1ce2b518fd7 100644 --- a/netbox/dcim/api/urls.py +++ b/netbox/dcim/api/urls.py @@ -3,7 +3,7 @@ from rest_framework import routers from extras.api.views import TopologyMapView -from ipam.api.views import ServiceViewSet, DeviceServiceViewSet +from ipam.api.views import ServiceViewSet from . import views @@ -21,37 +21,29 @@ router.register(r'manufacturers', views.ManufacturerViewSet) router.register(r'device-types', views.DeviceTypeViewSet) +# TODO: Device type components + # Devices router.register(r'device-roles', views.DeviceRoleViewSet) router.register(r'platforms', views.PlatformViewSet) router.register(r'devices', views.DeviceViewSet) + +# Device components router.register(r'console-ports', views.ConsolePortViewSet) router.register(r'console-server-ports', views.ConsoleServerPortViewSet) router.register(r'power-ports', views.PowerPortViewSet) router.register(r'power-outlets', views.PowerOutletViewSet) router.register(r'interfaces', views.InterfaceViewSet) -router.register(r'interface-connections', views.InterfaceConnectionViewSet) router.register(r'device-bays', views.DeviceBayViewSet) router.register(r'modules', views.ModuleViewSet) router.register(r'services', ServiceViewSet) -# TODO: Device type components - -# Device components -device_router = routers.DefaultRouter() -device_router.register(r'console-ports', views.DeviceConsolePortViewSet, base_name='consoleport') -device_router.register(r'console-server-ports', views.DeviceConsoleServerPortViewSet, base_name='consoleserverport') -device_router.register(r'power-ports', views.DevicePowerPortViewSet, base_name='powerport') -device_router.register(r'power-outlets', views.DevicePowerOutletViewSet, base_name='poweroutlet') -device_router.register(r'interfaces', views.DeviceInterfaceViewSet, base_name='interface') -device_router.register(r'device-bays', views.DeviceDeviceBayViewSet, base_name='devicebay') -device_router.register(r'modules', views.DeviceModuleViewSet, base_name='module') -device_router.register(r'services', DeviceServiceViewSet, base_name='service') +# Interface connections +router.register(r'interface-connections', views.InterfaceConnectionViewSet) urlpatterns = [ url(r'', include(router.urls)), - url(r'^devices/(?P\d+)/', include(device_router.urls)), # Miscellaneous url(r'^related-connections/$', views.RelatedConnectionsView.as_view(), name='related_connections'), diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 27724e28955..4c7899a6aa1 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -1,11 +1,8 @@ from rest_framework.decorators import detail_route -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, -) from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import APIView -from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.viewsets import ModelViewSet from django.conf import settings from django.contrib.contenttypes.models import ContentType @@ -175,101 +172,42 @@ def lldp_neighbors(self, request, pk): # -# Console Ports +# Device components # -class ConsolePortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): - queryset = ConsolePort.objects.select_related('cs_port') +class ConsolePortViewSet(WritableSerializerMixin, ModelViewSet): + queryset = ConsolePort.objects.select_related('device', 'cs_port__device') serializer_class = serializers.ConsolePortSerializer + write_serializer_class= serializers.WritableConsolePortSerializer + filter_class = filters.ConsolePortFilter -class DeviceConsolePortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceConsolePortSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return ConsolePort.objects.filter(device=device).select_related('cs_port') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Console Server Ports -# - -class ConsoleServerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): - queryset = ConsoleServerPort.objects.select_related('connected_console') +class ConsoleServerPortViewSet(WritableSerializerMixin, ModelViewSet): + queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device') serializer_class = serializers.ConsoleServerPortSerializer + write_serializer_class= serializers.WritableConsoleServerPortSerializer + filter_class = filters.ConsoleServerPortFilter -class DeviceConsoleServerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceConsoleServerPortSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return ConsoleServerPort.objects.filter(device=device).select_related('connected_console') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Power Ports -# - -class PowerPortViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): - queryset = PowerPort.objects.select_related('power_outlet') +class PowerPortViewSet(WritableSerializerMixin, ModelViewSet): + queryset = PowerPort.objects.select_related('device', 'power_outlet__device') serializer_class = serializers.PowerPortSerializer + write_serializer_class= serializers.WritablePowerPortSerializer + filter_class = filters.PowerPortFilter -class DevicePowerPortViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DevicePowerPortSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return PowerPort.objects.filter(device=device).select_related('power_outlet') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Power Outlets -# - -class PowerOutletViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): - queryset = PowerOutlet.objects.select_related('connected_port') +class PowerOutletViewSet(WritableSerializerMixin, ModelViewSet): + queryset = PowerOutlet.objects.select_related('device', 'connected_port__device') serializer_class = serializers.PowerOutletSerializer + write_serializer_class= serializers.WritablePowerOutletSerializer + filter_class = filters.PowerOutletFilter -class DevicePowerOutletViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DevicePowerOutletSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return PowerOutlet.objects.filter(device=device).select_related('connected_port') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Interfaces -# - -class InterfaceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): +class InterfaceViewSet(WritableSerializerMixin, ModelViewSet): queryset = Interface.objects.select_related('device') serializer_class = serializers.InterfaceSerializer + write_serializer_class= serializers.WritableInterfaceSerializer + filter_class = filters.InterfaceFilter @detail_route() def graphs(self, request, pk=None): @@ -279,61 +217,18 @@ def graphs(self, request, pk=None): return Response(serializer.data) -class DeviceInterfaceViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceInterfaceSerializer - filter_class = filters.InterfaceFilter - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return Interface.objects.order_naturally(device.device_type.interface_ordering).filter(device=device)\ - .select_related('connected_as_a', 'connected_as_b', 'circuit_termination') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Device bays -# - -class DeviceBayViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): +class DeviceBayViewSet(WritableSerializerMixin, ModelViewSet): queryset = DeviceBay.objects.select_related('installed_device') serializer_class = serializers.DeviceBaySerializer + write_serializer_class= serializers.WritableDeviceBaySerializer + filter_class = filters.DeviceBayFilter -class DeviceDeviceBayViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceDeviceBaySerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return DeviceBay.objects.filter(device=device).select_related('installed_device') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) - - -# -# Modules -# - -class ModuleViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, GenericViewSet): +class ModuleViewSet(WritableSerializerMixin, ModelViewSet): queryset = Module.objects.select_related('device', 'manufacturer') serializer_class = serializers.ModuleSerializer - - -class DeviceModuleViewSet(CreateModelMixin, ListModelMixin, GenericViewSet): - serializer_class = serializers.DeviceModuleSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return Module.objects.filter(device=device).select_related('device', 'manufacturer') - - def perform_create(self, serializer): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - serializer.save(device=device) + write_serializer_class= serializers.WritableModuleSerializer + filter_class = filters.ModuleFilter # diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 79024b605c5..60db5913af8 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -7,8 +7,8 @@ from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter from .models import ( - ConsolePort, ConsoleServerPort, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, Manufacturer, - Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, + ConsolePort, ConsoleServerPort, Device, DeviceBay, DeviceRole, DeviceType, Interface, InterfaceConnection, + Manufacturer, Module, Platform, PowerOutlet, PowerPort, Rack, RackGroup, RackRole, Site, ) @@ -368,6 +368,42 @@ class Meta: fields = ['name'] +class DeviceBayFilter(django_filters.FilterSet): + device_id = django_filters.ModelMultipleChoiceFilter( + name='device', + queryset=Device.objects.all(), + label='Device (ID)', + ) + device = django_filters.ModelMultipleChoiceFilter( + name='device', + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) + + class Meta: + model = DeviceBay + fields = ['name'] + + +class ModuleFilter(django_filters.FilterSet): + device_id = django_filters.ModelMultipleChoiceFilter( + name='device', + queryset=Device.objects.all(), + label='Device (ID)', + ) + device = django_filters.ModelMultipleChoiceFilter( + name='device', + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) + + class Meta: + model = Module + fields = ['name'] + + class ConsoleConnectionFilter(django_filters.FilterSet): site = django_filters.MethodFilter( action='filter_site', diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 9104f55b9e0..bf7cf6e495f 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from dcim.api.serializers import NestedDeviceSerializer, DeviceInterfaceSerializer, NestedSiteSerializer +from dcim.api.serializers import NestedDeviceSerializer, InterfaceSerializer, NestedSiteSerializer from extras.api.serializers import CustomFieldValueSerializer from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from tenancy.api.serializers import NestedTenantSerializer @@ -207,7 +207,7 @@ class Meta: class IPAddressSerializer(serializers.ModelSerializer): vrf = NestedVRFSerializer() tenant = NestedTenantSerializer() - interface = DeviceInterfaceSerializer() + interface = InterfaceSerializer() custom_field_values = CustomFieldValueSerializer(many=True) class Meta: @@ -249,10 +249,8 @@ class Meta: fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] -class DeviceServiceSerializer(serializers.ModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') - ipaddresses = NestedIPAddressSerializer(many=True) +class WritableServiceSerializer(serializers.ModelSerializer): class Meta: model = Service - fields = ['id', 'url', 'name', 'port', 'protocol', 'ipaddresses', 'description'] + fields = ['id', 'device', 'name', 'port', 'protocol', 'ipaddresses', 'description'] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index a957845785f..3bd1f71c32e 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -1,11 +1,5 @@ -from django.shortcuts import get_object_or_404 +from rest_framework.viewsets import ModelViewSet -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, -) -from rest_framework.viewsets import GenericViewSet, ModelViewSet - -from dcim.models import Device from ipam.models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF from ipam import filters from extras.api.views import CustomFieldModelViewSet @@ -101,14 +95,7 @@ class VLANViewSet(WritableSerializerMixin, CustomFieldModelViewSet): # Services # -class ServiceViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, GenericViewSet): +class ServiceViewSet(WritableSerializerMixin, ModelViewSet): queryset = Service.objects.select_related('device') serializer_class = serializers.ServiceSerializer - - -class DeviceServiceViewSet(CreateModelMixin, ListModelMixin, WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.DeviceServiceSerializer - - def get_queryset(self): - device = get_object_or_404(Device, pk=self.kwargs['pk']) - return Service.objects.filter(device=device).select_related('device') + write_serializer_class = serializers.WritableServiceSerializer From ea51f1c8962e5f21e5725eea1b9558e511d4d917 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 15:01:56 -0500 Subject: [PATCH 023/206] Removed circuit-specific endpoint for CircuitTerminations --- netbox/circuits/api/serializers.py | 38 ++++++++++++++++++++---------- netbox/circuits/api/urls.py | 3 --- netbox/circuits/api/views.py | 26 +++++++------------- netbox/circuits/filters.py | 15 ++++++++++-- 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index ec6d6a18ad5..b15e735ebda 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -57,19 +57,6 @@ class Meta: fields = ['id', 'url', 'name', 'slug'] -# -# Circuit Terminations -# - -class CircuitTerminationSerializer(serializers.ModelSerializer): - site = NestedSiteSerializer() - interface = InterfaceSerializer() - - class Meta: - model = CircuitTermination - fields = ['id', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info'] - - # # Circuits # @@ -103,3 +90,28 @@ class Meta: fields = [ 'id', 'cid', 'provider', 'type', 'tenant', 'install_date', 'commit_rate', 'description', 'comments', ] + + +# +# Circuit Terminations +# + +class CircuitTerminationSerializer(serializers.ModelSerializer): + circuit = NestedCircuitSerializer() + site = NestedSiteSerializer() + interface = InterfaceSerializer() + + class Meta: + model = CircuitTermination + fields = [ + 'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', + ] + + +class WritableCircuitTerminationSerializer(serializers.ModelSerializer): + + class Meta: + model = CircuitTermination + fields = [ + 'id', 'circuit', 'term_side', 'site', 'interface', 'port_speed', 'upstream_speed', 'xconnect_id', 'pp_info', + ] diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 59739e51085..6bb49dc38c0 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -15,7 +15,4 @@ url(r'', include(router.urls)), - # Circuits - url(r'^circuits/(?P\d+)/terminations/$', views.NestedCircuitTerminationViewSet.as_view({'get': 'list'})), - ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 97d619fd2ce..c8aa7b01002 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -1,15 +1,11 @@ from django.shortcuts import get_object_or_404 from rest_framework.decorators import detail_route -from rest_framework.mixins import ( - CreateModelMixin, DestroyModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin, -) from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet, ModelViewSet +from rest_framework.viewsets import ModelViewSet +from circuits import filters from circuits.models import Provider, CircuitTermination, CircuitType, Circuit -from circuits.filters import CircuitFilter - from extras.models import Graph, GRAPH_TYPE_PROVIDER from extras.api.serializers import GraphSerializer from extras.api.views import CustomFieldModelViewSet @@ -25,6 +21,7 @@ class ProviderViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Provider.objects.all() serializer_class = serializers.ProviderSerializer write_serializer_class = serializers.WritableProviderSerializer + filter_class = filters.ProviderFilter @detail_route() def graphs(self, request, pk=None): @@ -51,22 +48,15 @@ class CircuitViewSet(WritableSerializerMixin, CustomFieldModelViewSet): queryset = Circuit.objects.select_related('type', 'tenant', 'provider') serializer_class = serializers.CircuitSerializer write_serializer_class = serializers.WritableCircuitSerializer - filter_class = CircuitFilter + filter_class = filters.CircuitFilter # # Circuit Terminations # -class CircuitTerminationViewSet(RetrieveModelMixin, UpdateModelMixin, DestroyModelMixin, WritableSerializerMixin, - GenericViewSet): - queryset = CircuitTermination.objects.select_related('site', 'interface__device') +class CircuitTerminationViewSet(WritableSerializerMixin, ModelViewSet): + queryset = CircuitTermination.objects.select_related('circuit', 'site', 'interface__device') serializer_class = serializers.CircuitTerminationSerializer - - -class NestedCircuitTerminationViewSet(CreateModelMixin, ListModelMixin ,WritableSerializerMixin, GenericViewSet): - serializer_class = serializers.CircuitTerminationSerializer - - def get_queryset(self): - circuit = get_object_or_404(Circuit, pk=self.kwargs['pk']) - return CircuitTermination.objects.filter(circuit=circuit).select_related('site', 'interface__device') + write_serializer_class = serializers.WritableCircuitTerminationSerializer + filter_class = filters.CircuitTerminationFilter diff --git a/netbox/circuits/filters.py b/netbox/circuits/filters.py index fa57a74dccc..a920ae55c9e 100644 --- a/netbox/circuits/filters.py +++ b/netbox/circuits/filters.py @@ -6,8 +6,7 @@ from extras.filters import CustomFieldFilterSet from tenancy.models import Tenant from utilities.filters import NullableModelMultipleChoiceFilter - -from .models import Provider, Circuit, CircuitType +from .models import Provider, Circuit, CircuitTermination, CircuitType class ProviderFilter(CustomFieldFilterSet, django_filters.FilterSet): @@ -101,3 +100,15 @@ def search(self, queryset, value): Q(description__icontains=value) | Q(comments__icontains=value) ).distinct() + + +class CircuitTerminationFilter(django_filters.FilterSet): + circuit_id = django_filters.ModelMultipleChoiceFilter( + name='circuit', + queryset=Circuit.objects.all(), + label='Circuit', + ) + + class Meta: + model = CircuitTermination + fields = ['term_side', 'site'] From 06e5966cb41f17a78096569b6883038e676214e1 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 15:09:23 -0500 Subject: [PATCH 024/206] Include API routers directly where possible --- netbox/circuits/api/urls.py | 8 -------- netbox/ipam/api/urls.py | 8 -------- netbox/netbox/urls.py | 9 ++++++--- netbox/tenancy/api/urls.py | 8 -------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/netbox/circuits/api/urls.py b/netbox/circuits/api/urls.py index 6bb49dc38c0..b104be5947a 100644 --- a/netbox/circuits/api/urls.py +++ b/netbox/circuits/api/urls.py @@ -1,5 +1,3 @@ -from django.conf.urls import include, url - from rest_framework import routers from . import views @@ -10,9 +8,3 @@ router.register(r'circuit-types', views.CircuitTypeViewSet) router.register(r'circuits', views.CircuitViewSet) router.register(r'circuit-terminations', views.CircuitTerminationViewSet) - -urlpatterns = [ - - url(r'', include(router.urls)), - -] diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 24c97b341f8..e084c92876f 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -1,5 +1,3 @@ -from django.conf.urls import include, url - from rest_framework import routers from . import views @@ -15,9 +13,3 @@ router.register(r'vlan-groups', views.VLANGroupViewSet) router.register(r'vlans', views.VLANViewSet) router.register(r'services', views.ServiceViewSet) - -urlpatterns = [ - - url(r'', include(router.urls)), - -] diff --git a/netbox/netbox/urls.py b/netbox/netbox/urls.py index eb6b67fe708..3b1d7b56974 100644 --- a/netbox/netbox/urls.py +++ b/netbox/netbox/urls.py @@ -2,8 +2,11 @@ from django.conf.urls import include, url from django.contrib import admin +from circuits.api.urls import router as circuits_router +from ipam.api.urls import router as ipam_router from netbox.views import home, handle_500, trigger_500 from users.views import login, logout +from tenancy.api.urls import router as tenancy_router handler500 = handle_500 @@ -26,11 +29,11 @@ url(r'^profile/', include('users.urls', namespace='users')), # API - url(r'^api/circuits/', include('circuits.api.urls', namespace='circuits-api')), + url(r'^api/circuits/', include(circuits_router.urls, namespace='circuits-api')), url(r'^api/dcim/', include('dcim.api.urls', namespace='dcim-api')), - url(r'^api/ipam/', include('ipam.api.urls', namespace='ipam-api')), + url(r'^api/ipam/', include(ipam_router.urls, namespace='ipam-api')), url(r'^api/secrets/', include('secrets.api.urls', namespace='secrets-api')), - url(r'^api/tenancy/', include('tenancy.api.urls', namespace='tenancy-api')), + url(r'^api/tenancy/', include(tenancy_router.urls, namespace='tenancy-api')), url(r'^api/docs/', include('rest_framework_swagger.urls')), url(r'^api-auth/', include('rest_framework.urls')), diff --git a/netbox/tenancy/api/urls.py b/netbox/tenancy/api/urls.py index 35e11cd6b20..483f54d235d 100644 --- a/netbox/tenancy/api/urls.py +++ b/netbox/tenancy/api/urls.py @@ -1,5 +1,3 @@ -from django.conf.urls import include, url - from rest_framework import routers from . import views @@ -8,9 +6,3 @@ router = routers.DefaultRouter() router.register(r'tenant-groups', views.TenantGroupViewSet) router.register(r'tenants', views.TenantViewSet) - -urlpatterns = [ - - url(r'', include(router.urls)), - -] From 4f8a5eb1a05fb514111a8f6185368d12f6510186 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 16:21:33 -0500 Subject: [PATCH 025/206] Moved secret views into a ViewSet (no write ability yet) --- netbox/secrets/api/serializers.py | 8 ++++++++ netbox/secrets/api/urls.py | 5 +---- netbox/secrets/api/views.py | 22 ++++++++++++++++++++++ netbox/secrets/filters.py | 7 ++++++- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/netbox/secrets/api/serializers.py b/netbox/secrets/api/serializers.py index 0b70e7cf8c4..d948818112d 100644 --- a/netbox/secrets/api/serializers.py +++ b/netbox/secrets/api/serializers.py @@ -34,3 +34,11 @@ class SecretSerializer(serializers.ModelSerializer): class Meta: model = Secret fields = ['id', 'device', 'role', 'name', 'plaintext', 'hash', 'created', 'last_updated'] + + +class WritableSecretSerializer(serializers.ModelSerializer): + plaintext = serializers.CharField() + + class Meta: + model = Secret + fields = ['id', 'device', 'role', 'name', 'plaintext'] diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index a030b481eb2..fd38c12ecbb 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -7,15 +7,12 @@ router = routers.DefaultRouter() router.register(r'secret-roles', views.SecretRoleViewSet) +router.register(r'secrets', views.SecretViewSet) urlpatterns = [ url(r'', include(router.urls)), - # Secrets - url(r'^secrets/$', views.SecretListView.as_view(), name='secret_list'), - url(r'^secrets/(?P\d+)/$', views.SecretDetailView.as_view(), name='secret_detail'), - # Miscellaneous url(r'^generate-keys/$', views.RSAKeyGeneratorView.as_view(), name='generate_keys'), diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index e2ccbdb075b..e343022865c 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -4,6 +4,7 @@ from rest_framework import generics from rest_framework import status +from rest_framework.authentication import BasicAuthentication, SessionAuthentication from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer @@ -14,6 +15,7 @@ from extras.api.renderers import FormlessBrowsableAPIRenderer, FreeRADIUSClientsRenderer from secrets.filters import SecretFilter from secrets.models import Secret, SecretRole, UserKey +from utilities.api import WritableSerializerMixin from . import serializers @@ -37,6 +39,25 @@ class SecretRoleViewSet(ModelViewSet): # Secrets # +# TODO: Need to implement custom create() and update() methods to handle secret encryption, and custom list() and +# retrieve() methods to handle decryption. +class SecretViewSet(WritableSerializerMixin, ModelViewSet): + queryset = Secret.objects.select_related( + 'device__primary_ip4', 'device__primary_ip6', 'role', + ).prefetch_related( + 'role__users', 'role__groups', + ) + serializer_class = serializers.SecretSerializer + write_serializer_class = serializers.WritableSecretSerializer + filter_class = SecretFilter + # DRF's BrowsableAPIRenderer can't support passing the secret key as a header, so we disable it. + renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] + # Enabled BasicAuthentication for testing (until we have TokenAuthentication implemented) + authentication_classes = [BasicAuthentication, SessionAuthentication] + permission_classes = [IsAuthenticated] + + +# TODO: Delete class SecretListView(generics.GenericAPIView): """ List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. @@ -83,6 +104,7 @@ def post(self, request): return self.get(request, private_key=request.POST.get('private_key')) +# TODO: Delete class SecretDetailView(generics.GenericAPIView): """ Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret. diff --git a/netbox/secrets/filters.py b/netbox/secrets/filters.py index af6f62fbd47..9c4e70b9767 100644 --- a/netbox/secrets/filters.py +++ b/netbox/secrets/filters.py @@ -22,11 +22,16 @@ class SecretFilter(django_filters.FilterSet): to_field_name='slug', label='Role (slug)', ) + device_id = django_filters.ModelMultipleChoiceFilter( + name='device', + queryset=Device.objects.all(), + label='Device (ID)', + ) device = django_filters.ModelMultipleChoiceFilter( name='device', queryset=Device.objects.all(), to_field_name='name', - label='Device (Name)', + label='Device (name)', ) class Meta: From 2408d78f47938f49ca608cb708dc9bc7d9bd9489 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 1 Feb 2017 17:40:50 -0500 Subject: [PATCH 026/206] Introduced ability to decrypt secrets by sending the user's private key in an HTTP header --- netbox/secrets/api/views.py | 136 +++++++++++++++--------------------- 1 file changed, 55 insertions(+), 81 deletions(-) diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index e343022865c..25663c5036b 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,11 +1,9 @@ +import base64 from Crypto.PublicKey import RSA -from django.shortcuts import get_object_or_404 +from django.http import HttpResponseBadRequest -from rest_framework import generics -from rest_framework import status from rest_framework.authentication import BasicAuthentication, SessionAuthentication -from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer from rest_framework.response import Response @@ -39,8 +37,8 @@ class SecretRoleViewSet(ModelViewSet): # Secrets # -# TODO: Need to implement custom create() and update() methods to handle secret encryption, and custom list() and -# retrieve() methods to handle decryption. +# TODO: Need to implement custom create() and update() methods to handle secret encryption. +# TODO: Figure out a better way of transmitting the private key class SecretViewSet(WritableSerializerMixin, ModelViewSet): queryset = Secret.objects.select_related( 'device__primary_ip4', 'device__primary_ip6', 'role', @@ -56,97 +54,73 @@ class SecretViewSet(WritableSerializerMixin, ModelViewSet): authentication_classes = [BasicAuthentication, SessionAuthentication] permission_classes = [IsAuthenticated] + # The user's private key must be sent as a header (X-Private-Key). To overcome limitations around sending line + # breaks in a header field, the key must be base64-encoded and stripped of all whitespace. This is a temporary + # kludge until a more elegant approach can be devised. + def _get_private_key(self, request): + private_key_b64 = request.META.get('HTTP_X_PRIVATE_KEY', None) + if private_key_b64 is None: + return None + # TODO: Handle invalid encoding + return base64.b64decode(private_key_b64) -# TODO: Delete -class SecretListView(generics.GenericAPIView): - """ - List secrets (filterable). If a private key is POSTed, attempt to decrypt each Secret. - """ - queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\ - .prefetch_related('role__users', 'role__groups') - serializer_class = serializers.SecretSerializer - filter_class = SecretFilter - renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] - permission_classes = [IsAuthenticated] + def retrieve(self, request, *args, **kwargs): + private_key = self._get_private_key(request) + secret = self.get_object() - def get(self, request, private_key=None): - queryset = self.filter_queryset(self.get_queryset()) + # Attempt to unlock the Secret if a private key was provided + if private_key is not None: - # Attempt to decrypt each Secret if a private key was provided. - if private_key: try: - uk = UserKey.objects.get(user=request.user) + user_key = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - return Response( - {'error': ERR_USERKEY_MISSING}, - status=status.HTTP_400_BAD_REQUEST - ) - if not uk.is_active(): - return Response( - {'error': ERR_USERKEY_INACTIVE}, - status=status.HTTP_400_BAD_REQUEST - ) - master_key = uk.get_master_key(private_key) - if master_key is not None: - for s in queryset: - if s.decryptable_by(request.user): - s.decrypt(master_key) - else: - return Response( - {'error': ERR_PRIVKEY_INVALID}, - status=status.HTTP_400_BAD_REQUEST - ) + return HttpResponseBadRequest(ERR_USERKEY_MISSING) + if not user_key.is_active(): + return HttpResponseBadRequest(ERR_USERKEY_INACTIVE) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) + master_key = user_key.get_master_key(private_key) + if master_key is None: + return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) - def post(self, request): - return self.get(request, private_key=request.POST.get('private_key')) + secret.decrypt(master_key) + serializer = self.get_serializer(secret) + return Response(serializer.data) -# TODO: Delete -class SecretDetailView(generics.GenericAPIView): - """ - Retrieve a single Secret. If a private key is POSTed, attempt to decrypt the Secret. - """ - queryset = Secret.objects.select_related('device__primary_ip4', 'device__primary_ip6', 'role')\ - .prefetch_related('role__users', 'role__groups') - serializer_class = serializers.SecretSerializer - renderer_classes = [FormlessBrowsableAPIRenderer, JSONRenderer, FreeRADIUSClientsRenderer] - permission_classes = [IsAuthenticated] + def list(self, request, *args, **kwargs): + private_key = self._get_private_key(request) + queryset = self.filter_queryset(self.get_queryset()) - def get(self, request, pk, private_key=None): - secret = get_object_or_404(Secret, pk=pk) + # Attempt to unlock the Secrets if a private key was provided + master_key = None + if private_key is not None: - # Attempt to decrypt the Secret if a private key was provided. - if private_key: try: - uk = UserKey.objects.get(user=request.user) + user_key = UserKey.objects.get(user=request.user) except UserKey.DoesNotExist: - return Response( - {'error': ERR_USERKEY_MISSING}, - status=status.HTTP_400_BAD_REQUEST - ) - if not uk.is_active(): - return Response( - {'error': ERR_USERKEY_INACTIVE}, - status=status.HTTP_400_BAD_REQUEST - ) - if not secret.decryptable_by(request.user): - raise PermissionDenied(detail="You do not have permission to decrypt this secret.") - master_key = uk.get_master_key(private_key) + return HttpResponseBadRequest(ERR_USERKEY_MISSING) + if not user_key.is_active(): + return HttpResponseBadRequest(ERR_USERKEY_INACTIVE) + + master_key = user_key.get_master_key(private_key) if master_key is None: - return Response( - {'error': ERR_PRIVKEY_INVALID}, - status=status.HTTP_400_BAD_REQUEST - ) - secret.decrypt(master_key) + return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) - serializer = self.get_serializer(secret) - return Response(serializer.data) + # Pagination + page = self.paginate_queryset(queryset) + if page is not None: + secrets = [] + if master_key is not None: + for secret in page: + secret.decrypt(master_key) + secrets.append(secret) + serializer = self.get_serializer(secrets, many=True) + else: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) - def post(self, request, pk): - return self.get(request, pk, private_key=request.POST.get('private_key')) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) class RSAKeyGeneratorView(APIView): From cf66f67fb68a708c336cee1835ab6e400ee425de Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 2 Feb 2017 21:26:51 -0500 Subject: [PATCH 027/206] Initial work on using session-based master key ciphers --- netbox/secrets/api/urls.py | 1 + netbox/secrets/api/views.py | 98 ++++++++++++++++++++++--------------- 2 files changed, 59 insertions(+), 40 deletions(-) diff --git a/netbox/secrets/api/urls.py b/netbox/secrets/api/urls.py index fd38c12ecbb..d67ea92e43d 100644 --- a/netbox/secrets/api/urls.py +++ b/netbox/secrets/api/urls.py @@ -14,6 +14,7 @@ url(r'', include(router.urls)), # Miscellaneous + url(r'^get-session-key/$', views.GetSessionKey.as_view(), name='get_session_key'), url(r'^generate-keys/$', views.RSAKeyGeneratorView.as_view(), name='generate_keys'), ] diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 25663c5036b..b4e75d82df9 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,5 +1,7 @@ import base64 +from Crypto.Cipher import XOR from Crypto.PublicKey import RSA +import os from django.http import HttpResponseBadRequest @@ -20,6 +22,7 @@ ERR_USERKEY_MISSING = "No UserKey found for the current user." ERR_USERKEY_INACTIVE = "UserKey has not been activated for decryption." +ERR_PRIVKEY_MISSING = "Private key was not provided." ERR_PRIVKEY_INVALID = "Invalid private key." @@ -38,7 +41,6 @@ class SecretRoleViewSet(ModelViewSet): # # TODO: Need to implement custom create() and update() methods to handle secret encryption. -# TODO: Figure out a better way of transmitting the private key class SecretViewSet(WritableSerializerMixin, ModelViewSet): queryset = Secret.objects.select_related( 'device__primary_ip4', 'device__primary_ip6', 'role', @@ -54,58 +56,33 @@ class SecretViewSet(WritableSerializerMixin, ModelViewSet): authentication_classes = [BasicAuthentication, SessionAuthentication] permission_classes = [IsAuthenticated] - # The user's private key must be sent as a header (X-Private-Key). To overcome limitations around sending line - # breaks in a header field, the key must be base64-encoded and stripped of all whitespace. This is a temporary - # kludge until a more elegant approach can be devised. - def _get_private_key(self, request): - private_key_b64 = request.META.get('HTTP_X_PRIVATE_KEY', None) - if private_key_b64 is None: + def _get_master_key(self, request): + cached_key = request.session.get('cached_key', None) + session_key = request.COOKIES.get('session_key', None) + if cached_key is None or session_key is None: return None - # TODO: Handle invalid encoding - return base64.b64decode(private_key_b64) - def retrieve(self, request, *args, **kwargs): - private_key = self._get_private_key(request) - secret = self.get_object() - - # Attempt to unlock the Secret if a private key was provided - if private_key is not None: + cached_key = base64.b64decode(cached_key) + session_key = base64.b64decode(session_key) - try: - user_key = UserKey.objects.get(user=request.user) - except UserKey.DoesNotExist: - return HttpResponseBadRequest(ERR_USERKEY_MISSING) - if not user_key.is_active(): - return HttpResponseBadRequest(ERR_USERKEY_INACTIVE) + xor = XOR.new(session_key) + master_key = xor.encrypt(cached_key) + return master_key - master_key = user_key.get_master_key(private_key) - if master_key is None: - return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) + def retrieve(self, request, *args, **kwargs): + master_key = self._get_master_key(request) + secret = self.get_object() + if master_key is not None: secret.decrypt(master_key) serializer = self.get_serializer(secret) return Response(serializer.data) def list(self, request, *args, **kwargs): - private_key = self._get_private_key(request) + master_key = self._get_master_key(request) queryset = self.filter_queryset(self.get_queryset()) - # Attempt to unlock the Secrets if a private key was provided - master_key = None - if private_key is not None: - - try: - user_key = UserKey.objects.get(user=request.user) - except UserKey.DoesNotExist: - return HttpResponseBadRequest(ERR_USERKEY_MISSING) - if not user_key.is_active(): - return HttpResponseBadRequest(ERR_USERKEY_INACTIVE) - - master_key = user_key.get_master_key(private_key) - if master_key is None: - return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) - # Pagination page = self.paginate_queryset(queryset) if page is not None: @@ -145,3 +122,44 @@ def get(self, request): 'private_key': private_key, 'public_key': public_key, }) + + +class GetSessionKey(APIView): + """ + Cache an encrypted copy of the master key derived from the submitted private key. + """ + permission_classes = [IsAuthenticated] + + def post(self, request): + + # Read private key + private_key = request.POST.get('private_key', None) + if private_key is None: + return HttpResponseBadRequest(ERR_PRIVKEY_MISSING) + + # Validate user key + try: + user_key = UserKey.objects.get(user=request.user) + except UserKey.DoesNotExist: + return HttpResponseBadRequest(ERR_USERKEY_MISSING) + if not user_key.is_active(): + return HttpResponseBadRequest(ERR_USERKEY_INACTIVE) + + # Validate private key + master_key = user_key.get_master_key(private_key) + if master_key is None: + return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) + + # Generate a random 256-bit encryption key + session_key = os.urandom(32) + xor = XOR.new(session_key) + cached_key = xor.encrypt(master_key) + + # Save XORed copy of the master key + request.session['cached_key'] = base64.b64encode(cached_key) + + response = Response({ + 'session_key': base64.b64encode(session_key), + }) + response.set_cookie('session_key', base64.b64encode(session_key)) + return response From a42eeb12d2b6cbecbda158a8a0e1dd64b60655ec Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Feb 2017 12:49:32 -0500 Subject: [PATCH 028/206] Implemented SessionKeys for secrets --- netbox/secrets/api/views.py | 93 ++++++++++--------- .../migrations/0002_add_sessionkeys.py | 37 ++++++++ netbox/secrets/models.py | 76 +++++++++++++-- netbox/secrets/tests/test_models.py | 12 +-- 4 files changed, 164 insertions(+), 54 deletions(-) create mode 100644 netbox/secrets/migrations/0002_add_sessionkeys.py diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index b4e75d82df9..0ac087c6368 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -1,7 +1,5 @@ import base64 -from Crypto.Cipher import XOR from Crypto.PublicKey import RSA -import os from django.http import HttpResponseBadRequest @@ -14,7 +12,7 @@ from extras.api.renderers import FormlessBrowsableAPIRenderer, FreeRADIUSClientsRenderer from secrets.filters import SecretFilter -from secrets.models import Secret, SecretRole, UserKey +from secrets.models import Secret, SecretRole, SessionKey, UserKey from utilities.api import WritableSerializerMixin from . import serializers @@ -57,16 +55,25 @@ class SecretViewSet(WritableSerializerMixin, ModelViewSet): permission_classes = [IsAuthenticated] def _get_master_key(self, request): - cached_key = request.session.get('cached_key', None) - session_key = request.COOKIES.get('session_key', None) - if cached_key is None or session_key is None: + + # Check for a session key provided as a cookie or header + if 'session_key' in request.COOKIES: + session_key = base64.b64decode(request.COOKIES['session_key']) + elif 'HTTP_X_SESSION_KEY' in request.META: + session_key = base64.b64decode(request.META['HTTP_X_SESSION_KEY']) + else: + return None + + # Retrieve session key cipher (if any) for the current user + try: + sk = SessionKey.objects.get(user=request.user) + except SessionKey.DoesNotExist: return None - cached_key = base64.b64decode(cached_key) - session_key = base64.b64decode(session_key) + # Recover master key + # TODO: Exception handling + master_key = sk.get_master_key(session_key) - xor = XOR.new(session_key) - master_key = xor.encrypt(cached_key) return master_key def retrieve(self, request, *args, **kwargs): @@ -100,30 +107,6 @@ def list(self, request, *args, **kwargs): return Response(serializer.data) -class RSAKeyGeneratorView(APIView): - """ - Generate a new RSA key pair for a user. Authenticated because it's a ripe avenue for DoS. - """ - permission_classes = [IsAuthenticated] - - def get(self, request): - - # Determine what size key to generate - key_size = request.GET.get('key_size', 2048) - if key_size not in range(2048, 4097, 256): - key_size = 2048 - - # Export RSA private and public keys in PEM format - key = RSA.generate(key_size) - private_key = key.exportKey('PEM') - public_key = key.publickey().exportKey('PEM') - - return Response({ - 'private_key': private_key, - 'public_key': public_key, - }) - - class GetSessionKey(APIView): """ Cache an encrypted copy of the master key derived from the submitted private key. @@ -150,16 +133,42 @@ def post(self, request): if master_key is None: return HttpResponseBadRequest(ERR_PRIVKEY_INVALID) - # Generate a random 256-bit encryption key - session_key = os.urandom(32) - xor = XOR.new(session_key) - cached_key = xor.encrypt(master_key) + # Delete the existing SessionKey for this user if one exists + SessionKey.objects.filter(user=request.user).delete() - # Save XORed copy of the master key - request.session['cached_key'] = base64.b64encode(cached_key) + # Create a new SessionKey + sk = SessionKey(user=request.user) + sk.save(master_key=master_key) + # Return the session key both as JSON and as a cookie response = Response({ - 'session_key': base64.b64encode(session_key), + 'session_key': base64.b64encode(sk.key), + 'expiration_time': sk.expiration_time, }) - response.set_cookie('session_key', base64.b64encode(session_key)) + # TODO: Limit cookie path to secrets API URLs + response.set_cookie('session_key', base64.b64encode(sk.key), expires=sk.expiration_time) return response + + +class RSAKeyGeneratorView(APIView): + """ + Generate a new RSA key pair for a user. Authenticated because it's a ripe avenue for DoS. + """ + permission_classes = [IsAuthenticated] + + def get(self, request): + + # Determine what size key to generate + key_size = request.GET.get('key_size', 2048) + if key_size not in range(2048, 4097, 256): + key_size = 2048 + + # Export RSA private and public keys in PEM format + key = RSA.generate(key_size) + private_key = key.exportKey('PEM') + public_key = key.publickey().exportKey('PEM') + + return Response({ + 'private_key': private_key, + 'public_key': public_key, + }) diff --git a/netbox/secrets/migrations/0002_add_sessionkeys.py b/netbox/secrets/migrations/0002_add_sessionkeys.py new file mode 100644 index 00000000000..c4b848b3570 --- /dev/null +++ b/netbox/secrets/migrations/0002_add_sessionkeys.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-02-03 17:10 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('secrets', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SessionKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cipher', models.BinaryField(max_length=512)), + ('hash', models.CharField(editable=False, max_length=128)), + ('created', models.DateTimeField(auto_now_add=True)), + ('expiration_time', models.DateTimeField(blank=True, editable=False, null=True)), + ('user', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='session_key', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['user__username'], + }, + ), + migrations.AlterField( + model_name='userkey', + name='user', + field=models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='user_key', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/netbox/secrets/models.py b/netbox/secrets/models.py index a0c3e6f8b31..8542c9bf3d9 100644 --- a/netbox/secrets/models.py +++ b/netbox/secrets/models.py @@ -1,5 +1,6 @@ +import datetime import os -from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.Cipher import AES, PKCS1_OAEP, XOR from Crypto.PublicKey import RSA from django.conf import settings @@ -8,6 +9,7 @@ from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import models +from django.utils import timezone from django.utils.encoding import force_bytes, python_2_unicode_compatible from dcim.models import Device @@ -16,11 +18,13 @@ from .hashers import SecretValidationHasher -def generate_master_key(): +def generate_random_key(bits=256): """ - Generate a new 256-bit (32 bytes) AES key to be used for symmetric encryption of secrets. + Generate a random encryption key. Sizes is given in bits and must be in increments of 32. """ - return os.urandom(32) + if bits % 32: + raise Exception("Invalid key size ({}). Key sizes must be in increments of 32 bits.".format(bits)) + return os.urandom(bits / 8) def encrypt_master_key(master_key, public_key): @@ -41,6 +45,14 @@ def decrypt_master_key(master_key_cipher, private_key): return cipher.decrypt(master_key_cipher) +def xor_keys(key_a, key_b): + """ + Return the binary XOR of two given keys. + """ + xor = XOR.new(key_a) + return xor.encrypt(key_b) + + class UserKeyQuerySet(models.QuerySet): def active(self): @@ -58,7 +70,7 @@ class UserKey(CreatedUpdatedModel): copy of the master encryption key. The encrypted instance of the master key can be decrypted only with the user's matching (private) decryption key. """ - user = models.OneToOneField(User, related_name='user_key', verbose_name='User') + user = models.OneToOneField(User, related_name='user_key', editable=False) public_key = models.TextField(verbose_name='RSA public key') master_key_cipher = models.BinaryField(max_length=512, blank=True, null=True, editable=False) @@ -121,7 +133,7 @@ def save(self, *args, **kwargs): # If no other active UserKeys exist, generate a new master key and use it to activate this UserKey. if self.is_filled() and not self.is_active() and not UserKey.objects.active().count(): - master_key = generate_master_key() + master_key = generate_random_key() self.master_key_cipher = encrypt_master_key(master_key, self.public_key) super(UserKey, self).save(*args, **kwargs) @@ -171,6 +183,58 @@ def activate(self, master_key): self.save() +@python_2_unicode_compatible +class SessionKey(models.Model): + """ + A SessionKey stores a User's temporary key to be used for the encryption and decryption of secrets. + """ + user = models.OneToOneField(User, related_name='session_key', editable=False) + cipher = models.BinaryField(max_length=512, editable=False) + hash = models.CharField(max_length=128, editable=False) + created = models.DateTimeField(auto_now_add=True) + expiration_time = models.DateTimeField(blank=True, null=True, editable=False) + + key = None + + class Meta: + ordering = ['user__username'] + + def __str__(self): + return self.user.username + + def save(self, master_key=None, *args, **kwargs): + + if master_key is None: + raise Exception("The master key must be provided to save a session key.") + + # Generate a random 256-bit session key if one is not already defined + if self.key is None: + self.key = generate_random_key() + + # Generate SHA256 hash using Django's built-in password hashing mechanism + self.hash = make_password(self.key) + + # Encrypt master key using the session key + self.cipher = xor_keys(self.key, master_key) + + # Calculate expiration time + # TODO: Define a SESSION_KEY_MAX_AGE configuration setting + self.expiration_time = timezone.now() + datetime.timedelta(hours=12) + + super(SessionKey, self).save(*args, **kwargs) + + def get_master_key(self, session_key): + + # Validate the provided session key + if not check_password(session_key, self.hash): + raise Exception("Invalid session key") + + # Decrypt master key using provided session key + master_key = xor_keys(session_key, self.cipher) + + return master_key + + @python_2_unicode_compatible class SecretRole(models.Model): """ diff --git a/netbox/secrets/tests/test_models.py b/netbox/secrets/tests/test_models.py index 13248776593..e668f1185f9 100644 --- a/netbox/secrets/tests/test_models.py +++ b/netbox/secrets/tests/test_models.py @@ -5,7 +5,7 @@ from django.core.exceptions import ValidationError from django.test import TestCase -from secrets.models import UserKey, Secret, generate_master_key, encrypt_master_key, decrypt_master_key +from secrets.models import UserKey, Secret, encrypt_master_key, decrypt_master_key, generate_random_key from secrets.hashers import SecretValidationHasher @@ -33,7 +33,7 @@ def test_02_activate(self): """ Validate the activation of a UserKey. """ - master_key = generate_master_key() + master_key = generate_random_key() alice_uk = UserKey(user=User.objects.get(username='alice'), public_key=self.TEST_KEYS['alice_public']) self.assertFalse(alice_uk.is_active(), "Inactive UserKey is_active() did not return False") alice_uk.activate(master_key) @@ -62,7 +62,7 @@ def test_04_master_key_retrieval(self): """ Test the decryption of a master key using the user's private key. """ - master_key = generate_master_key() + master_key = generate_random_key() alice_uk = UserKey(user=User.objects.get(username='alice'), public_key=self.TEST_KEYS['alice_public']) alice_uk.activate(master_key) retrieved_master_key = alice_uk.get_master_key(self.TEST_KEYS['alice_private']) @@ -72,7 +72,7 @@ def test_05_invalid_private_key(self): """ Ensure that an exception is raised when attempting to retrieve a secret key using an invalid private key. """ - secret_key = generate_master_key() + secret_key = generate_random_key() secret_key_cipher = encrypt_master_key(secret_key, self.TEST_KEYS['alice_public']) try: decrypted_secret_key = decrypt_master_key(secret_key_cipher, self.TEST_KEYS['bob_private']) @@ -88,7 +88,7 @@ def test_01_encrypt_decrypt(self): Test basic encryption and decryption functionality using a random master key. """ plaintext = "FooBar123" - secret_key = generate_master_key() + secret_key = generate_random_key() s = Secret(plaintext=plaintext) s.encrypt(secret_key) @@ -118,7 +118,7 @@ def test_02_ciphertext_uniqueness(self): Generate 50 Secrets using the same plaintext and check for duplicate IVs or payloads. """ plaintext = "1234567890abcdef" - secret_key = generate_master_key() + secret_key = generate_random_key() ivs = [] ciphertexts = [] for i in range(1, 51): From 616ca4fe1fc9080b91acb974ea482906ba0039b8 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 3 Feb 2017 16:14:42 -0500 Subject: [PATCH 029/206] Adapted the web UI to work with the new secrets API --- netbox/project-static/js/secrets.js | 112 ++++++++++-------- netbox/secrets/forms.py | 9 +- netbox/secrets/views.py | 55 ++++++--- .../secrets/inc/private_key_modal.html | 9 +- 4 files changed, 109 insertions(+), 76 deletions(-) diff --git a/netbox/project-static/js/secrets.js b/netbox/project-static/js/secrets.js index 22710236b9e..40f4b5f0c8a 100644 --- a/netbox/project-static/js/secrets.js +++ b/netbox/project-static/js/secrets.js @@ -4,13 +4,16 @@ $(document).ready(function() { $('button.unlock-secret').click(function (event) { var secret_id = $(this).attr('secret-id'); - // Retrieve from storage or prompt for private key - var private_key = sessionStorage.getItem('private_key'); - if (!private_key) { - $('#privkey_modal').modal('show'); + // If we have an active cookie containing a session key, send the API request. + if (document.cookie.indexOf('session_key') > 0) { + console.log("Retrieving secret..."); + unlock_secret(secret_id); + // Otherwise, prompt the user for a private key so we can request a session key. } else { - unlock_secret(secret_id, private_key); + console.log("No session key found. Prompt user for private key."); + $('#privkey_modal').modal('show'); } + }); // Locking a secret @@ -18,64 +21,50 @@ $(document).ready(function() { var secret_id = $(this).attr('secret-id'); var secret_div = $('#secret_' + secret_id); - // Delete the plaintext + // Delete the plaintext from the DOM element. secret_div.html('********'); $(this).hide(); $(this).siblings('button.unlock-secret').show(); }); - // Adding/editing a secret - private_key_field = $('#id_private_key'); - private_key_field.parents('form').submit(function(event) { - console.log("form submitted"); - var private_key = sessionStorage.getItem('private_key'); - if (private_key) { - private_key_field.val(private_key); - } else if ($('form .requires-private-key:first').val()) { - console.log("we need a key!"); - $('#privkey_modal').modal('show'); - return false; - } - }); - - // Saving a private RSA key locally - $('#submit_privkey').click(function() { + // Retrieve a session key + $('#request_session_key').click(function() { var private_key = $('#user_privkey').val(); - sessionStorage.setItem('private_key', private_key); + + // POST the user's private key to request a temporary session key. + console.log("Requesting a session key..."); + get_session_key(private_key); }); - // Generate a new public/private key pair via the API - $('#generate_keypair').click(function() { - $('#new_keypair_modal').modal('show'); + // Retrieve a secret via the API + function unlock_secret(secret_id) { $.ajax({ - url: netbox_api_path + 'secrets/generate-keys/', + url: netbox_api_path + 'secrets/secrets/' + secret_id + '/', type: 'GET', dataType: 'json', success: function (response, status) { - var public_key = response.public_key; - var private_key = response.private_key; - $('#new_pubkey').val(public_key); - $('#new_privkey').val(private_key); + console.log("Secret retrieved successfully"); + $('#secret_' + secret_id).html(response.plaintext); + $('button.unlock-secret[secret-id=' + secret_id + ']').hide(); + $('button.lock-secret[secret-id=' + secret_id + ']').show(); }, error: function (xhr, ajaxOptions, thrownError) { - alert("There was an error generating a new key pair."); + console.log("Error: " + xhr.responseText); + if (xhr.status == 403) { + alert("Permission denied"); + } else { + var json = jQuery.parseJSON(xhr.responseText); + alert("Secret retrieval failed: " + json['error']); + } } }); - }); - - // Enter a newly generated public key - $('#use_new_pubkey').click(function() { - var new_pubkey = $('#new_pubkey'); - if (new_pubkey.val()) { - $('#id_public_key').val(new_pubkey.val()); - } - }); + } - // Retrieve a secret via the API - function unlock_secret(secret_id, private_key) { + // Request a session key via the API + function get_session_key(private_key) { var csrf_token = $('input[name=csrfmiddlewaretoken]').val(); $.ajax({ - url: netbox_api_path + 'secrets/secrets/' + secret_id + '/', + url: netbox_api_path + 'secrets/get-session-key/', type: 'POST', data: { private_key: private_key @@ -85,19 +74,46 @@ $(document).ready(function() { xhr.setRequestHeader("X-CSRFToken", csrf_token); }, success: function (response, status) { - $('#secret_' + secret_id).html(response.plaintext); - $('button.unlock-secret[secret-id=' + secret_id + ']').hide(); - $('button.lock-secret[secret-id=' + secret_id + ']').show(); + console.log("Received a new session key; valid until " + response.expiration_time); + alert('Session key received! You may now unlock secrets.'); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 403) { alert("Permission denied"); } else { var json = jQuery.parseJSON(xhr.responseText); - alert("Decryption failed: " + json['error']); + alert("Failed to retrieve a session key: " + json['error']); } } }); } + // Generate a new public/private key pair via the API + $('#generate_keypair').click(function() { + $('#new_keypair_modal').modal('show'); + $.ajax({ + url: netbox_api_path + 'secrets/generate-keys/', + type: 'GET', + dataType: 'json', + success: function (response, status) { + var public_key = response.public_key; + var private_key = response.private_key; + $('#new_pubkey').val(public_key); + $('#new_privkey').val(private_key); + }, + error: function (xhr, ajaxOptions, thrownError) { + alert("There was an error generating a new key pair."); + } + }); + }); + + // Accept a new RSA key pair generated via the API + $('#use_new_pubkey').click(function() { + var new_pubkey = $('#new_pubkey'); + + if (new_pubkey.val()) { + $('#id_public_key').val(new_pubkey.val()); + } + }); + }); diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index b4c64b485af..15bfad9ebdc 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -47,9 +47,8 @@ class Meta: # class SecretForm(BootstrapMixin, forms.ModelForm): - private_key = forms.CharField(required=False, widget=forms.HiddenInput()) plaintext = forms.CharField(max_length=65535, required=False, label='Plaintext', - widget=forms.PasswordInput(attrs={'class': 'requires-private-key'})) + widget=forms.PasswordInput()) plaintext2 = forms.CharField(max_length=65535, required=False, label='Plaintext (verify)', widget=forms.PasswordInput()) @@ -59,9 +58,6 @@ class Meta: def clean(self): - if self.cleaned_data['plaintext']: - validate_rsa_key(self.cleaned_data['private_key']) - if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']: raise forms.ValidationError({ 'plaintext2': "The two given plaintext values do not match. Please check your input." @@ -86,8 +82,7 @@ def save(self, *args, **kwargs): class SecretImportForm(BootstrapMixin, BulkImportForm): - private_key = forms.CharField(widget=forms.HiddenInput()) - csv = CSVDataField(csv_form=SecretFromCSVForm, widget=forms.Textarea(attrs={'class': 'requires-private-key'})) + csv = CSVDataField(csv_form=SecretFromCSVForm) class SecretBulkEditForm(BootstrapMixin, BulkEditForm): diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py index d67cd18a00e..5a90eca6ebc 100644 --- a/netbox/secrets/views.py +++ b/netbox/secrets/views.py @@ -1,3 +1,5 @@ +import base64 + from django.contrib import messages from django.contrib.auth.decorators import permission_required, login_required from django.contrib.auth.mixins import PermissionRequiredMixin @@ -12,7 +14,7 @@ from . import filters, forms, tables from .decorators import userkey_required -from .models import SecretRole, Secret, UserKey +from .models import SecretRole, Secret, SessionKey, UserKey # @@ -110,32 +112,44 @@ def secret_add(request, pk): def secret_edit(request, pk): secret = get_object_or_404(Secret, pk=pk) - uk = UserKey.objects.get(user=request.user) if request.method == 'POST': form = forms.SecretForm(request.POST, instance=secret) if form.is_valid(): - # Re-encrypt the Secret if a plaintext has been specified. - if form.cleaned_data['plaintext']: + # Re-encrypt the Secret if a plaintext and session key have been provided. + session_key = request.COOKIES.get('session_key', None) + if form.cleaned_data['plaintext'] and session_key is not None: - # Retrieve the master key from the current user's UserKey - master_key = uk.get_master_key(form.cleaned_data['private_key']) - if master_key is None: - form.add_error(None, "Invalid private key! Unable to encrypt secret data.") + # Retrieve the master key using the provided session key + session_key = base64.b64decode(session_key) + master_key = None + try: + sk = SessionKey.objects.get(user=request.user) + master_key = sk.get_master_key(session_key) + except SessionKey.DoesNotExist: + form.add_error(None, "No session key found for this user.") # Create and encrypt the new Secret - else: + if master_key is not None: secret = form.save(commit=False) secret.plaintext = str(form.cleaned_data['plaintext']) secret.encrypt(master_key) secret.save() + messages.success(request, u"Modified secret {}.".format(secret)) + return redirect('secrets:secret', pk=secret.pk) + else: + form.add_error(None, "Invalid session key. Unable to encrypt secret data.") + # We can't save the plaintext without a session key. + elif form.cleaned_data['plaintext']: + form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.") + + # If no new plaintext was specified, a session key is not needed. else: secret = form.save() - - messages.success(request, u"Modified secret {}.".format(secret)) - return redirect('secrets:secret', pk=secret.pk) + messages.success(request, u"Modified secret {}.".format(secret)) + return redirect('secrets:secret', pk=secret.pk) else: form = forms.SecretForm(instance=secret) @@ -157,19 +171,28 @@ class SecretDeleteView(PermissionRequiredMixin, ObjectDeleteView): @userkey_required() def secret_import(request): - uk = UserKey.objects.get(user=request.user) + session_key = request.COOKIES.get('session_key', None) if request.method == 'POST': form = forms.SecretImportForm(request.POST) + + if session_key is None: + form.add_error(None, "No session key was provided with the request. Unable to encrypt secret data.") + if form.is_valid(): new_secrets = [] - # Retrieve the master key from the current user's UserKey - master_key = uk.get_master_key(form.cleaned_data['private_key']) + session_key = base64.b64decode(session_key) + master_key = None + try: + sk = SessionKey.objects.get(user=request.user) + master_key = sk.get_master_key(session_key) + except SessionKey.DoesNotExist: + form.add_error(None, "No session key found for this user.") + if master_key is None: form.add_error(None, "Invalid private key! Unable to encrypt secret data.") - else: try: with transaction.atomic(): diff --git a/netbox/templates/secrets/inc/private_key_modal.html b/netbox/templates/secrets/inc/private_key_modal.html index d55e6425e56..00d2455c1b6 100644 --- a/netbox/templates/secrets/inc/private_key_modal.html +++ b/netbox/templates/secrets/inc/private_key_modal.html @@ -10,16 +10,15 @@