1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Merge pull request #8833 from netbox-community/8823-api-serializers

Closes #8823: Add plugin support for REST API components
This commit is contained in:
Jeremy Stretch
2022-03-09 12:31:01 -05:00
committed by GitHub
23 changed files with 695 additions and 658 deletions

View File

@ -6,25 +6,37 @@ Generally speaking, there aren't many NetBox-specific components to implementing
## Serializers
First, create a serializer for the plugin model, in `api/serializers.py`. Specify its model class and the fields to include within the serializer's `Meta` class.
Serializers are responsible for converting Python objects to JSON data suitable for conveying to consumers, and vice versa. NetBox provides the `NetBoxModelSerializer` class for use by plugins to handle the assignment of tags and custom field data. (These features can also be included ad hoc via the `CustomFieldModelSerializer` and `TaggableModelSerializer` classes.)
### Example
To create a serializer for a plugin model, subclass `NetBoxModelSerializer` in `api/serializers.py`. Specify the model class and the fields to include within the serializer's `Meta` class.
```python
from rest_framework.serializers import ModelSerializer
# api/serializers.py
from netbox.api.serializers import NetBoxModelSerializer
from my_plugin.models import MyModel
class MyModelSerializer(ModelSerializer):
class MyModelSerializer(NetBoxModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'foo', 'bar')
```
## Views
## Viewsets
Next, create a generic API view set that allows basic CRUD (create, read, update, and delete) operations for objects. This is defined in `api/views.py`. Specify the `queryset` and `serializer_class` attributes under the view set.
Just as in the user interface, a REST API view handles the business logic of displaying and interacting with NetBox objects. NetBox provides the `NetBoxModelViewSet` class, which extends DRF's built-in `ModelViewSet` to handle bulk operations and object validation.
Unlike the user interface, typically only a single view set is required per model: This view set handles all request types (`GET`, `POST`, `DELETE`, etc.).
### Example
To create a viewset for a plugin model, subclass `NetBoxModelViewSet` in `api/views.py`, and define the `queryset` and `serializer_class` attributes.
```python
from rest_framework.viewsets import ModelViewSet
# api/views.py
from netbox.api.viewsets import ModelViewSet
from my_plugin.models import MyModel
from .serializers import MyModelSerializer
@ -33,11 +45,16 @@ class MyModelViewSet(ModelViewSet):
serializer_class = MyModelSerializer
```
## URLs
## Routers
Finally, we'll register a URL for our endpoint in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
Routers map URLs to REST API views (endpoints). NetBox does not provide any custom components for this; the [`DefaultRouter`](https://www.django-rest-framework.org/api-guide/routers/#defaultrouter) class provided by DRF should suffice for most use cases.
Routers should be exposed in `api/urls.py`. This file **must** define a variable named `urlpatterns`.
### Example
```python
# api/urls.py
from rest_framework import routers
from .views import MyModelViewSet
@ -46,7 +63,7 @@ router.register('my-model', MyModelViewSet)
urlpatterns = router.urls
```
With these three components in place, we can request `/api/plugins/my-plugin/my-model/` to retrieve a list of all MyModel instances.
This will make the plugin's view accessible at `/api/plugins/my-plugin/my-model/`.
!!! warning
This example is provided as a minimal reference implementation only. It does not address authentication, performance, or myriad other concerns that plugin authors may need to address.
The examples provided here are intended to serve as a minimal reference implementation only. This documentation does not address authentication, performance, or myriad other concerns that plugin authors may need to address.

View File

@ -5,7 +5,7 @@ from circuits.models import *
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
from dcim.api.serializers import LinkTerminationSerializer
from netbox.api import ChoiceField
from netbox.api.serializers import PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from .nested_serializers import *
@ -14,7 +14,7 @@ from .nested_serializers import *
# Providers
#
class ProviderSerializer(PrimaryModelSerializer):
class ProviderSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
circuit_count = serializers.IntegerField(read_only=True)
@ -30,7 +30,7 @@ class ProviderSerializer(PrimaryModelSerializer):
# Provider networks
#
class ProviderNetworkSerializer(PrimaryModelSerializer):
class ProviderNetworkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
provider = NestedProviderSerializer()
@ -46,7 +46,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
# Circuits
#
class CircuitTypeSerializer(PrimaryModelSerializer):
class CircuitTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
circuit_count = serializers.IntegerField(read_only=True)
@ -70,7 +70,7 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
]
class CircuitSerializer(PrimaryModelSerializer):
class CircuitSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
provider = NestedProviderSerializer()
status = ChoiceField(choices=CircuitStatusChoices, required=False)

View File

@ -3,8 +3,7 @@ from rest_framework.routers import APIRootView
from circuits import filtersets
from circuits.models import *
from dcim.api.views import PassThroughPortMixin
from extras.api.views import CustomFieldModelViewSet
from netbox.api.views import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from . import serializers
@ -21,7 +20,7 @@ class CircuitsRootView(APIRootView):
# Providers
#
class ProviderViewSet(CustomFieldModelViewSet):
class ProviderViewSet(NetBoxModelViewSet):
queryset = Provider.objects.prefetch_related('tags').annotate(
circuit_count=count_related(Circuit, 'provider')
)
@ -33,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
# Circuit Types
#
class CircuitTypeViewSet(CustomFieldModelViewSet):
class CircuitTypeViewSet(NetBoxModelViewSet):
queryset = CircuitType.objects.prefetch_related('tags').annotate(
circuit_count=count_related(Circuit, 'type')
)
@ -45,7 +44,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
# Circuits
#
class CircuitViewSet(CustomFieldModelViewSet):
class CircuitViewSet(NetBoxModelViewSet):
queryset = Circuit.objects.prefetch_related(
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
).prefetch_related('tags')
@ -57,7 +56,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
# Circuit Terminations
#
class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = CircuitTermination.objects.prefetch_related(
'circuit', 'site', 'provider_network', 'cable'
)
@ -70,7 +69,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
# Provider networks
#
class ProviderNetworkViewSet(CustomFieldModelViewSet):
class ProviderNetworkViewSet(NetBoxModelViewSet):
queryset = ProviderNetwork.objects.prefetch_related('tags')
serializer_class = serializers.ProviderNetworkSerializer
filterset_class = filtersets.ProviderNetworkFilterSet

View File

@ -12,7 +12,7 @@ from ipam.api.nested_serializers import (
from ipam.models import ASN, VLAN
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import (
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
)
from netbox.config import ConfigItem
from tenancy.api.nested_serializers import NestedTenantSerializer
@ -109,7 +109,7 @@ class SiteGroupSerializer(NestedGroupModelSerializer):
]
class SiteSerializer(PrimaryModelSerializer):
class SiteSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
status = ChoiceField(choices=SiteStatusChoices, required=False)
region = NestedRegionSerializer(required=False, allow_null=True)
@ -161,7 +161,7 @@ class LocationSerializer(NestedGroupModelSerializer):
]
class RackRoleSerializer(PrimaryModelSerializer):
class RackRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
rack_count = serializers.IntegerField(read_only=True)
@ -173,7 +173,7 @@ class RackRoleSerializer(PrimaryModelSerializer):
]
class RackSerializer(PrimaryModelSerializer):
class RackSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
site = NestedSiteSerializer()
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
@ -212,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer):
return obj['name']
class RackReservationSerializer(PrimaryModelSerializer):
class RackReservationSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer()
user = NestedUserSerializer()
@ -266,7 +266,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
# Device/module types
#
class ManufacturerSerializer(PrimaryModelSerializer):
class ManufacturerSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
devicetype_count = serializers.IntegerField(read_only=True)
inventoryitem_count = serializers.IntegerField(read_only=True)
@ -280,7 +280,7 @@ class ManufacturerSerializer(PrimaryModelSerializer):
]
class DeviceTypeSerializer(PrimaryModelSerializer):
class DeviceTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
manufacturer = NestedManufacturerSerializer()
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
@ -296,7 +296,7 @@ class DeviceTypeSerializer(PrimaryModelSerializer):
]
class ModuleTypeSerializer(PrimaryModelSerializer):
class ModuleTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
manufacturer = NestedManufacturerSerializer()
# module_count = serializers.IntegerField(read_only=True)
@ -487,7 +487,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
# Devices
#
class DeviceRoleSerializer(PrimaryModelSerializer):
class DeviceRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
device_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
@ -500,7 +500,7 @@ class DeviceRoleSerializer(PrimaryModelSerializer):
]
class PlatformSerializer(PrimaryModelSerializer):
class PlatformSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
@ -514,7 +514,7 @@ class PlatformSerializer(PrimaryModelSerializer):
]
class DeviceSerializer(PrimaryModelSerializer):
class DeviceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
device_type = NestedDeviceTypeSerializer()
device_role = NestedDeviceRoleSerializer()
@ -556,7 +556,7 @@ class DeviceSerializer(PrimaryModelSerializer):
return data
class ModuleSerializer(PrimaryModelSerializer):
class ModuleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
device = NestedDeviceSerializer()
module_bay = NestedModuleBaySerializer()
@ -594,7 +594,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
# Device components
#
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -622,7 +622,7 @@ class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSeriali
]
class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class ConsolePortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -650,7 +650,7 @@ class ConsolePortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
]
class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class PowerOutletSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -685,7 +685,7 @@ class PowerOutletSerializer(PrimaryModelSerializer, LinkTerminationSerializer, C
]
class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class PowerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -709,7 +709,7 @@ class PowerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
]
class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class InterfaceSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -768,7 +768,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
return super().validate(data)
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -798,7 +798,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name', 'label']
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
device = NestedDeviceSerializer()
module = ComponentNestedModuleSerializer(
@ -818,7 +818,7 @@ class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
]
class ModuleBaySerializer(PrimaryModelSerializer):
class ModuleBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
device = NestedDeviceSerializer()
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
@ -831,7 +831,7 @@ class ModuleBaySerializer(PrimaryModelSerializer):
]
class DeviceBaySerializer(PrimaryModelSerializer):
class DeviceBaySerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
device = NestedDeviceSerializer()
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
@ -844,7 +844,7 @@ class DeviceBaySerializer(PrimaryModelSerializer):
]
class InventoryItemSerializer(PrimaryModelSerializer):
class InventoryItemSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
device = NestedDeviceSerializer()
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
@ -879,7 +879,7 @@ class InventoryItemSerializer(PrimaryModelSerializer):
# Device component roles
#
class InventoryItemRoleSerializer(PrimaryModelSerializer):
class InventoryItemRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
inventoryitem_count = serializers.IntegerField(read_only=True)
@ -895,7 +895,7 @@ class InventoryItemRoleSerializer(PrimaryModelSerializer):
# Cables
#
class CableSerializer(PrimaryModelSerializer):
class CableSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@ -1001,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer):
# Virtual chassis
#
class VirtualChassisSerializer(PrimaryModelSerializer):
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True)
@ -1018,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
# Power panels
#
class PowerPanelSerializer(PrimaryModelSerializer):
class PowerPanelSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer()
location = NestedLocationSerializer(
@ -1036,7 +1036,7 @@ class PowerPanelSerializer(PrimaryModelSerializer):
]
class PowerFeedSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
class PowerFeedSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
power_panel = NestedPowerPanelSerializer()
rack = NestedRackSerializer(

View File

@ -14,12 +14,12 @@ from rest_framework.viewsets import ViewSet
from circuits.models import Circuit
from dcim import filtersets
from dcim.models import *
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from extras.api.views import ConfigContextQuerySetMixin
from ipam.models import Prefix, VLAN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.views import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.config import get_config
from utilities.api import get_serializer_for_model
from utilities.utils import count_related
@ -103,7 +103,7 @@ class PassThroughPortMixin(object):
# Regions
#
class RegionViewSet(CustomFieldModelViewSet):
class RegionViewSet(NetBoxModelViewSet):
queryset = Region.objects.add_related_count(
Region.objects.all(),
Site,
@ -119,7 +119,7 @@ class RegionViewSet(CustomFieldModelViewSet):
# Site groups
#
class SiteGroupViewSet(CustomFieldModelViewSet):
class SiteGroupViewSet(NetBoxModelViewSet):
queryset = SiteGroup.objects.add_related_count(
SiteGroup.objects.all(),
Site,
@ -135,7 +135,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
# Sites
#
class SiteViewSet(CustomFieldModelViewSet):
class SiteViewSet(NetBoxModelViewSet):
queryset = Site.objects.prefetch_related(
'region', 'tenant', 'asns', 'tags'
).annotate(
@ -154,7 +154,7 @@ class SiteViewSet(CustomFieldModelViewSet):
# Locations
#
class LocationViewSet(CustomFieldModelViewSet):
class LocationViewSet(NetBoxModelViewSet):
queryset = Location.objects.add_related_count(
Location.objects.add_related_count(
Location.objects.all(),
@ -176,7 +176,7 @@ class LocationViewSet(CustomFieldModelViewSet):
# Rack roles
#
class RackRoleViewSet(CustomFieldModelViewSet):
class RackRoleViewSet(NetBoxModelViewSet):
queryset = RackRole.objects.prefetch_related('tags').annotate(
rack_count=count_related(Rack, 'role')
)
@ -188,7 +188,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
# Racks
#
class RackViewSet(CustomFieldModelViewSet):
class RackViewSet(NetBoxModelViewSet):
queryset = Rack.objects.prefetch_related(
'site', 'location', 'role', 'tenant', 'tags'
).annotate(
@ -250,7 +250,7 @@ class RackViewSet(CustomFieldModelViewSet):
# Rack reservations
#
class RackReservationViewSet(ModelViewSet):
class RackReservationViewSet(NetBoxModelViewSet):
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
serializer_class = serializers.RackReservationSerializer
filterset_class = filtersets.RackReservationFilterSet
@ -260,7 +260,7 @@ class RackReservationViewSet(ModelViewSet):
# Manufacturers
#
class ManufacturerViewSet(CustomFieldModelViewSet):
class ManufacturerViewSet(NetBoxModelViewSet):
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
devicetype_count=count_related(DeviceType, 'manufacturer'),
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
@ -274,7 +274,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
# Device/module types
#
class DeviceTypeViewSet(CustomFieldModelViewSet):
class DeviceTypeViewSet(NetBoxModelViewSet):
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
device_count=count_related(Device, 'device_type')
)
@ -283,7 +283,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
brief_prefetch_fields = ['manufacturer']
class ModuleTypeViewSet(CustomFieldModelViewSet):
class ModuleTypeViewSet(NetBoxModelViewSet):
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
# module_count=count_related(Module, 'module_type')
)
@ -296,61 +296,61 @@ class ModuleTypeViewSet(CustomFieldModelViewSet):
# Device type components
#
class ConsolePortTemplateViewSet(ModelViewSet):
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsolePortTemplateSerializer
filterset_class = filtersets.ConsolePortTemplateFilterSet
class ConsoleServerPortTemplateViewSet(ModelViewSet):
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ConsoleServerPortTemplateSerializer
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
class PowerPortTemplateViewSet(ModelViewSet):
class PowerPortTemplateViewSet(NetBoxModelViewSet):
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerPortTemplateSerializer
filterset_class = filtersets.PowerPortTemplateFilterSet
class PowerOutletTemplateViewSet(ModelViewSet):
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.PowerOutletTemplateSerializer
filterset_class = filtersets.PowerOutletTemplateFilterSet
class InterfaceTemplateViewSet(ModelViewSet):
class InterfaceTemplateViewSet(NetBoxModelViewSet):
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.InterfaceTemplateSerializer
filterset_class = filtersets.InterfaceTemplateFilterSet
class FrontPortTemplateViewSet(ModelViewSet):
class FrontPortTemplateViewSet(NetBoxModelViewSet):
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.FrontPortTemplateSerializer
filterset_class = filtersets.FrontPortTemplateFilterSet
class RearPortTemplateViewSet(ModelViewSet):
class RearPortTemplateViewSet(NetBoxModelViewSet):
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.RearPortTemplateSerializer
filterset_class = filtersets.RearPortTemplateFilterSet
class ModuleBayTemplateViewSet(ModelViewSet):
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.ModuleBayTemplateSerializer
filterset_class = filtersets.ModuleBayTemplateFilterSet
class DeviceBayTemplateViewSet(ModelViewSet):
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
serializer_class = serializers.DeviceBayTemplateSerializer
filterset_class = filtersets.DeviceBayTemplateFilterSet
class InventoryItemTemplateViewSet(ModelViewSet):
class InventoryItemTemplateViewSet(NetBoxModelViewSet):
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
serializer_class = serializers.InventoryItemTemplateSerializer
filterset_class = filtersets.InventoryItemTemplateFilterSet
@ -360,7 +360,7 @@ class InventoryItemTemplateViewSet(ModelViewSet):
# Device roles
#
class DeviceRoleViewSet(CustomFieldModelViewSet):
class DeviceRoleViewSet(NetBoxModelViewSet):
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'device_role'),
virtualmachine_count=count_related(VirtualMachine, 'role')
@ -373,7 +373,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
# Platforms
#
class PlatformViewSet(CustomFieldModelViewSet):
class PlatformViewSet(NetBoxModelViewSet):
queryset = Platform.objects.prefetch_related('tags').annotate(
device_count=count_related(Device, 'platform'),
virtualmachine_count=count_related(VirtualMachine, 'platform')
@ -386,7 +386,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
# Devices/modules
#
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
queryset = Device.objects.prefetch_related(
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
@ -532,7 +532,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
return Response(response)
class ModuleViewSet(CustomFieldModelViewSet):
class ModuleViewSet(NetBoxModelViewSet):
queryset = Module.objects.prefetch_related(
'device', 'module_bay', 'module_type__manufacturer', 'tags',
)
@ -544,7 +544,7 @@ class ModuleViewSet(CustomFieldModelViewSet):
# Device components
#
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsolePort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
@ -553,7 +553,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = ConsoleServerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
@ -562,7 +562,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerPort.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
@ -571,7 +571,7 @@ class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerOutlet.objects.prefetch_related(
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
)
@ -580,7 +580,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = Interface.objects.prefetch_related(
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses', 'fhrp_group_assignments', 'tags'
@ -590,7 +590,7 @@ class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = FrontPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
)
@ -599,7 +599,7 @@ class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
queryset = RearPort.objects.prefetch_related(
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
)
@ -608,21 +608,21 @@ class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
brief_prefetch_fields = ['device']
class ModuleBayViewSet(ModelViewSet):
class ModuleBayViewSet(NetBoxModelViewSet):
queryset = ModuleBay.objects.prefetch_related('tags')
serializer_class = serializers.ModuleBaySerializer
filterset_class = filtersets.ModuleBayFilterSet
brief_prefetch_fields = ['device']
class DeviceBayViewSet(ModelViewSet):
class DeviceBayViewSet(NetBoxModelViewSet):
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
serializer_class = serializers.DeviceBaySerializer
filterset_class = filtersets.DeviceBayFilterSet
brief_prefetch_fields = ['device']
class InventoryItemViewSet(ModelViewSet):
class InventoryItemViewSet(NetBoxModelViewSet):
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
serializer_class = serializers.InventoryItemSerializer
filterset_class = filtersets.InventoryItemFilterSet
@ -633,7 +633,7 @@ class InventoryItemViewSet(ModelViewSet):
# Device component roles
#
class InventoryItemRoleViewSet(CustomFieldModelViewSet):
class InventoryItemRoleViewSet(NetBoxModelViewSet):
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
inventoryitem_count=count_related(InventoryItem, 'role')
)
@ -645,7 +645,7 @@ class InventoryItemRoleViewSet(CustomFieldModelViewSet):
# Cables
#
class CableViewSet(ModelViewSet):
class CableViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Cable.objects.prefetch_related(
'termination_a', 'termination_b'
@ -658,7 +658,7 @@ class CableViewSet(ModelViewSet):
# Virtual chassis
#
class VirtualChassisViewSet(ModelViewSet):
class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
member_count=count_related(Device, 'virtual_chassis')
)
@ -671,7 +671,7 @@ class VirtualChassisViewSet(ModelViewSet):
# Power panels
#
class PowerPanelViewSet(ModelViewSet):
class PowerPanelViewSet(NetBoxModelViewSet):
queryset = PowerPanel.objects.prefetch_related(
'site', 'location'
).annotate(
@ -685,7 +685,7 @@ class PowerPanelViewSet(ModelViewSet):
# Power feeds
#
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
queryset = PowerFeed.objects.prefetch_related(
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
)

View File

@ -18,7 +18,7 @@ from extras.reports import get_report, get_reports, run_report
from extras.scripts import get_script, get_scripts, run_script
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.metadata import ContentTypeMetadata
from netbox.api.views import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.exceptions import RQWorkerNotRunningException
from utilities.utils import copy_safe_request, count_related
from . import serializers
@ -58,7 +58,7 @@ class ConfigContextQuerySetMixin:
# Webhooks
#
class WebhookViewSet(ModelViewSet):
class WebhookViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = Webhook.objects.all()
serializer_class = serializers.WebhookSerializer
@ -69,36 +69,18 @@ class WebhookViewSet(ModelViewSet):
# Custom fields
#
class CustomFieldViewSet(ModelViewSet):
class CustomFieldViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = CustomField.objects.all()
serializer_class = serializers.CustomFieldSerializer
filterset_class = filtersets.CustomFieldFilterSet
class CustomFieldModelViewSet(ModelViewSet):
"""
Include the applicable set of CustomFields in the ModelViewSet context.
"""
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.all()
context = super().get_serializer_context()
context.update({
'custom_fields': custom_fields,
})
return context
#
# Custom links
#
class CustomLinkViewSet(ModelViewSet):
class CustomLinkViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = CustomLink.objects.all()
serializer_class = serializers.CustomLinkSerializer
@ -109,7 +91,7 @@ class CustomLinkViewSet(ModelViewSet):
# Export templates
#
class ExportTemplateViewSet(ModelViewSet):
class ExportTemplateViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = ExportTemplate.objects.all()
serializer_class = serializers.ExportTemplateSerializer
@ -120,7 +102,7 @@ class ExportTemplateViewSet(ModelViewSet):
# Tags
#
class TagViewSet(ModelViewSet):
class TagViewSet(NetBoxModelViewSet):
queryset = Tag.objects.annotate(
tagged_items=count_related(TaggedItem, 'tag')
)
@ -132,7 +114,7 @@ class TagViewSet(ModelViewSet):
# Image attachments
#
class ImageAttachmentViewSet(ModelViewSet):
class ImageAttachmentViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = ImageAttachment.objects.all()
serializer_class = serializers.ImageAttachmentSerializer
@ -143,7 +125,7 @@ class ImageAttachmentViewSet(ModelViewSet):
# Journal entries
#
class JournalEntryViewSet(ModelViewSet):
class JournalEntryViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = JournalEntry.objects.all()
serializer_class = serializers.JournalEntrySerializer
@ -154,7 +136,7 @@ class JournalEntryViewSet(ModelViewSet):
# Config contexts
#
class ConfigContextViewSet(ModelViewSet):
class ConfigContextViewSet(NetBoxModelViewSet):
queryset = ConfigContext.objects.prefetch_related(
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
)

View File

@ -9,7 +9,7 @@ from ipam.choices import *
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
from ipam.models import *
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import PrimaryModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from utilities.api import get_serializer_for_model
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
@ -20,7 +20,7 @@ from .nested_serializers import *
# ASNs
#
class ASNSerializer(PrimaryModelSerializer):
class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=True)
@ -37,7 +37,7 @@ class ASNSerializer(PrimaryModelSerializer):
# VRFs
#
class VRFSerializer(PrimaryModelSerializer):
class VRFSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
import_targets = SerializedPKRelatedField(
@ -67,7 +67,7 @@ class VRFSerializer(PrimaryModelSerializer):
# Route targets
#
class RouteTargetSerializer(PrimaryModelSerializer):
class RouteTargetSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
@ -82,7 +82,7 @@ class RouteTargetSerializer(PrimaryModelSerializer):
# RIRs/aggregates
#
class RIRSerializer(PrimaryModelSerializer):
class RIRSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
aggregate_count = serializers.IntegerField(read_only=True)
@ -94,7 +94,7 @@ class RIRSerializer(PrimaryModelSerializer):
]
class AggregateSerializer(PrimaryModelSerializer):
class AggregateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
rir = NestedRIRSerializer()
@ -113,7 +113,7 @@ class AggregateSerializer(PrimaryModelSerializer):
# FHRP Groups
#
class FHRPGroupSerializer(PrimaryModelSerializer):
class FHRPGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
@ -125,7 +125,7 @@ class FHRPGroupSerializer(PrimaryModelSerializer):
]
class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
group = NestedFHRPGroupSerializer()
interface_type = ContentTypeField(
@ -153,7 +153,7 @@ class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
# VLANs
#
class RoleSerializer(PrimaryModelSerializer):
class RoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
prefix_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
@ -166,7 +166,7 @@ class RoleSerializer(PrimaryModelSerializer):
]
class VLANGroupSerializer(PrimaryModelSerializer):
class VLANGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
scope_type = ContentTypeField(
queryset=ContentType.objects.filter(
@ -196,7 +196,7 @@ class VLANGroupSerializer(PrimaryModelSerializer):
return serializer(obj.scope, context=context).data
class VLANSerializer(PrimaryModelSerializer):
class VLANSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
site = NestedSiteSerializer(required=False, allow_null=True)
group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
@ -230,7 +230,7 @@ class AvailableVLANSerializer(serializers.Serializer):
])
class CreateAvailableVLANSerializer(PrimaryModelSerializer):
class CreateAvailableVLANSerializer(NetBoxModelSerializer):
site = NestedSiteSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
status = ChoiceField(choices=VLANStatusChoices, required=False)
@ -251,7 +251,7 @@ class CreateAvailableVLANSerializer(PrimaryModelSerializer):
# Prefixes
#
class PrefixSerializer(PrimaryModelSerializer):
class PrefixSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
site = NestedSiteSerializer(required=False, allow_null=True)
@ -323,7 +323,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
# IP ranges
#
class IPRangeSerializer(PrimaryModelSerializer):
class IPRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
@ -345,7 +345,7 @@ class IPRangeSerializer(PrimaryModelSerializer):
# IP addresses
#
class IPAddressSerializer(PrimaryModelSerializer):
class IPAddressSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
vrf = NestedVRFSerializer(required=False, allow_null=True)
@ -403,7 +403,7 @@ class AvailableIPSerializer(serializers.Serializer):
# Services
#
class ServiceTemplateSerializer(PrimaryModelSerializer):
class ServiceTemplateSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
@ -415,7 +415,7 @@ class ServiceTemplateSerializer(PrimaryModelSerializer):
]
class ServiceSerializer(PrimaryModelSerializer):
class ServiceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
device = NestedDeviceSerializer(required=False, allow_null=True)
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)

View File

@ -1,19 +1,18 @@
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django_pglocks import advisory_lock
from django.shortcuts import get_object_or_404
from django_pglocks import advisory_lock
from drf_yasg.utils import swagger_auto_schema
from rest_framework import status
from rest_framework.response import Response
from rest_framework.routers import APIRootView
from rest_framework.views import APIView
from dcim.models import Site
from extras.api.views import CustomFieldModelViewSet
from ipam import filtersets
from ipam.models import *
from netbox.api.views import ModelViewSet, ObjectValidationMixin
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.viewsets.mixins import ObjectValidationMixin
from netbox.config import get_config
from utilities.constants import ADVISORY_LOCK_KEYS
from utilities.utils import count_related
@ -32,13 +31,13 @@ class IPAMRootView(APIRootView):
# Viewsets
#
class ASNViewSet(CustomFieldModelViewSet):
class ASNViewSet(NetBoxModelViewSet):
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
serializer_class = serializers.ASNSerializer
filterset_class = filtersets.ASNFilterSet
class VRFViewSet(CustomFieldModelViewSet):
class VRFViewSet(NetBoxModelViewSet):
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
'import_targets', 'export_targets', 'tags'
).annotate(
@ -49,13 +48,13 @@ class VRFViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VRFFilterSet
class RouteTargetViewSet(CustomFieldModelViewSet):
class RouteTargetViewSet(NetBoxModelViewSet):
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
serializer_class = serializers.RouteTargetSerializer
filterset_class = filtersets.RouteTargetFilterSet
class RIRViewSet(CustomFieldModelViewSet):
class RIRViewSet(NetBoxModelViewSet):
queryset = RIR.objects.annotate(
aggregate_count=count_related(Aggregate, 'rir')
).prefetch_related('tags')
@ -63,13 +62,13 @@ class RIRViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.RIRFilterSet
class AggregateViewSet(CustomFieldModelViewSet):
class AggregateViewSet(NetBoxModelViewSet):
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
serializer_class = serializers.AggregateSerializer
filterset_class = filtersets.AggregateFilterSet
class RoleViewSet(CustomFieldModelViewSet):
class RoleViewSet(NetBoxModelViewSet):
queryset = Role.objects.annotate(
prefix_count=count_related(Prefix, 'role'),
vlan_count=count_related(VLAN, 'role')
@ -78,7 +77,7 @@ class RoleViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.RoleFilterSet
class PrefixViewSet(CustomFieldModelViewSet):
class PrefixViewSet(NetBoxModelViewSet):
queryset = Prefix.objects.prefetch_related(
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
)
@ -93,7 +92,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
return super().get_serializer_class()
class IPRangeViewSet(CustomFieldModelViewSet):
class IPRangeViewSet(NetBoxModelViewSet):
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
serializer_class = serializers.IPRangeSerializer
filterset_class = filtersets.IPRangeFilterSet
@ -101,7 +100,7 @@ class IPRangeViewSet(CustomFieldModelViewSet):
parent_model = IPRange # AvailableIPsMixin
class IPAddressViewSet(CustomFieldModelViewSet):
class IPAddressViewSet(NetBoxModelViewSet):
queryset = IPAddress.objects.prefetch_related(
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
)
@ -109,20 +108,20 @@ class IPAddressViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.IPAddressFilterSet
class FHRPGroupViewSet(CustomFieldModelViewSet):
class FHRPGroupViewSet(NetBoxModelViewSet):
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
serializer_class = serializers.FHRPGroupSerializer
filterset_class = filtersets.FHRPGroupFilterSet
brief_prefetch_fields = ('ip_addresses',)
class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
serializer_class = serializers.FHRPGroupAssignmentSerializer
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
class VLANGroupViewSet(CustomFieldModelViewSet):
class VLANGroupViewSet(NetBoxModelViewSet):
queryset = VLANGroup.objects.annotate(
vlan_count=count_related(VLAN, 'group')
).prefetch_related('tags')
@ -130,7 +129,7 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VLANGroupFilterSet
class VLANViewSet(CustomFieldModelViewSet):
class VLANViewSet(NetBoxModelViewSet):
queryset = VLAN.objects.prefetch_related(
'site', 'group', 'tenant', 'role', 'tags'
).annotate(
@ -140,13 +139,13 @@ class VLANViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.VLANFilterSet
class ServiceTemplateViewSet(CustomFieldModelViewSet):
class ServiceTemplateViewSet(NetBoxModelViewSet):
queryset = ServiceTemplate.objects.prefetch_related('tags')
serializer_class = serializers.ServiceTemplateSerializer
filterset_class = filtersets.ServiceTemplateFilterSet
class ServiceViewSet(CustomFieldModelViewSet):
class ServiceViewSet(NetBoxModelViewSet):
queryset = Service.objects.prefetch_related(
'device', 'virtual_machine', 'tags', 'ipaddresses'
)

View File

@ -1,193 +0,0 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import ManyToManyField
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CreateOnlyDefault
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
from extras.models import CustomField, Tag
from utilities.utils import dict_to_filter_params
class BaseModelSerializer(serializers.ModelSerializer):
display = serializers.SerializerMethodField(read_only=True)
def get_display(self, obj):
return str(obj)
class ValidatedModelSerializer(BaseModelSerializer):
"""
Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
"""
def validate(self, data):
# Remove custom fields data and tags (if any) prior to model validation
attrs = data.copy()
attrs.pop('custom_fields', None)
attrs.pop('tags', None)
# Skip ManyToManyFields
for field in self.Meta.model._meta.get_fields():
if isinstance(field, ManyToManyField):
attrs.pop(field.name, None)
# Run clean() on an instance of the model
if self.instance is None:
instance = self.Meta.model(**attrs)
else:
instance = self.instance
for k, v in attrs.items():
setattr(instance, k, v)
instance.full_clean()
return data
class CustomFieldModelSerializer(ValidatedModelSerializer):
"""
Extends ModelSerializer to render any CustomFields and their values associated with an object.
"""
custom_fields = CustomFieldsDataField(
source='custom_field_data',
default=CreateOnlyDefault(CustomFieldDefaultValues())
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
# Retrieve the set of CustomFields which apply to this type of object
content_type = ContentType.objects.get_for_model(self.Meta.model)
fields = CustomField.objects.filter(content_types=content_type)
# Populate CustomFieldValues for each instance from database
if type(self.instance) in (list, tuple):
for obj in self.instance:
self._populate_custom_fields(obj, fields)
else:
self._populate_custom_fields(self.instance, fields)
def _populate_custom_fields(self, instance, custom_fields):
instance.custom_fields = {}
for field in custom_fields:
instance.custom_fields[field.name] = instance.cf.get(field.name)
#
# Nested serializers
#
class WritableNestedSerializer(BaseModelSerializer):
"""
Returns a nested representation of an object on read, but accepts only a primary key on write.
"""
def to_internal_value(self, data):
if data is None:
return None
# Dictionary of related object attributes
if isinstance(data, dict):
params = dict_to_filter_params(data)
queryset = self.Meta.model.objects
try:
return queryset.get(**params)
except ObjectDoesNotExist:
raise ValidationError(
"Related object not found using the provided attributes: {}".format(params)
)
except MultipleObjectsReturned:
raise ValidationError(
"Multiple objects match the provided attributes: {}".format(params)
)
except FieldError as e:
raise ValidationError(e)
# Integer PK of related object
if isinstance(data, int):
pk = data
else:
try:
# PK might have been mistakenly passed as a string
pk = int(data)
except (TypeError, ValueError):
raise ValidationError(
"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
"unrecognized value: {}".format(data)
)
# Look up object by PK
queryset = self.Meta.model.objects
try:
return queryset.get(pk=int(data))
except ObjectDoesNotExist:
raise ValidationError(
"Related object not found using the provided numeric ID: {}".format(pk)
)
#
# Nested tags serialization
#
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
class NestedTagSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
class Meta:
model = Tag
fields = ['id', 'url', 'display', 'name', 'slug', 'color']
#
# Base model serializers
#
class PrimaryModelSerializer(CustomFieldModelSerializer):
"""
Adds support for custom fields and tags.
"""
tags = NestedTagSerializer(many=True, required=False)
def create(self, validated_data):
tags = validated_data.pop('tags', None)
instance = super().create(validated_data)
if tags is not None:
return self._save_tags(instance, tags)
return instance
def update(self, instance, validated_data):
tags = validated_data.pop('tags', None)
# Cache tags on instance for change logging
instance._tags = tags or []
instance = super().update(instance, validated_data)
if tags is not None:
return self._save_tags(instance, tags)
return instance
def _save_tags(self, instance, tags):
if tags:
instance.tags.set([t.name for t in tags])
else:
instance.tags.clear()
return instance
class NestedGroupModelSerializer(PrimaryModelSerializer):
"""
Extends PrimaryModelSerializer to include MPTT support.
"""
_depth = serializers.IntegerField(source='level', read_only=True)
class BulkOperationSerializer(serializers.Serializer):
id = serializers.IntegerField()

View File

@ -0,0 +1,27 @@
from rest_framework import serializers
from .base import *
from .features import *
from .nested import *
#
# Base model serializers
#
class NetBoxModelSerializer(TaggableModelSerializer, CustomFieldModelSerializer, ValidatedModelSerializer):
"""
Adds support for custom fields and tags.
"""
pass
class NestedGroupModelSerializer(NetBoxModelSerializer):
"""
Extends PrimaryModelSerializer to include MPTT support.
"""
_depth = serializers.IntegerField(source='level', read_only=True)
class BulkOperationSerializer(serializers.Serializer):
id = serializers.IntegerField()

View File

@ -0,0 +1,43 @@
from django.db.models import ManyToManyField
from rest_framework import serializers
__all__ = (
'BaseModelSerializer',
'ValidatedModelSerializer',
)
class BaseModelSerializer(serializers.ModelSerializer):
display = serializers.SerializerMethodField(read_only=True)
def get_display(self, obj):
return str(obj)
class ValidatedModelSerializer(BaseModelSerializer):
"""
Extends the built-in ModelSerializer to enforce calling full_clean() on a copy of the associated instance during
validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144)
"""
def validate(self, data):
# Remove custom fields data and tags (if any) prior to model validation
attrs = data.copy()
attrs.pop('custom_fields', None)
attrs.pop('tags', None)
# Skip ManyToManyFields
for field in self.Meta.model._meta.get_fields():
if isinstance(field, ManyToManyField):
attrs.pop(field.name, None)
# Run clean() on an instance of the model
if self.instance is None:
instance = self.Meta.model(**attrs)
else:
instance = self.instance
for k, v in attrs.items():
setattr(instance, k, v)
instance.full_clean()
return data

View File

@ -0,0 +1,80 @@
from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
from rest_framework.fields import CreateOnlyDefault
from extras.api.customfields import CustomFieldsDataField, CustomFieldDefaultValues
from extras.models import CustomField
from .nested import NestedTagSerializer
__all__ = (
'CustomFieldModelSerializer',
'TaggableModelSerializer',
)
class CustomFieldModelSerializer(serializers.Serializer):
"""
Introduces support for custom field assignment. Adds `custom_fields` serialization and ensures
that custom field data is populated upon initialization.
"""
custom_fields = CustomFieldsDataField(
source='custom_field_data',
default=CreateOnlyDefault(CustomFieldDefaultValues())
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None:
# Retrieve the set of CustomFields which apply to this type of object
content_type = ContentType.objects.get_for_model(self.Meta.model)
fields = CustomField.objects.filter(content_types=content_type)
# Populate custom field values for each instance from database
if type(self.instance) in (list, tuple):
for obj in self.instance:
self._populate_custom_fields(obj, fields)
else:
self._populate_custom_fields(self.instance, fields)
def _populate_custom_fields(self, instance, custom_fields):
instance.custom_fields = {}
for field in custom_fields:
instance.custom_fields[field.name] = instance.cf.get(field.name)
class TaggableModelSerializer(serializers.Serializer):
"""
Introduces support for Tag assignment. Adds `tags` serialization, and handles tag assignment
on create() and update().
"""
tags = NestedTagSerializer(many=True, required=False)
def create(self, validated_data):
tags = validated_data.pop('tags', None)
instance = super().create(validated_data)
if tags is not None:
return self._save_tags(instance, tags)
return instance
def update(self, instance, validated_data):
tags = validated_data.pop('tags', None)
# Cache tags on instance for change logging
instance._tags = tags or []
instance = super().update(instance, validated_data)
if tags is not None:
return self._save_tags(instance, tags)
return instance
def _save_tags(self, instance, tags):
if tags:
instance.tags.set([t.name for t in tags])
else:
instance.tags.clear()
return instance

View File

@ -0,0 +1,62 @@
from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from extras.models import Tag
from utilities.utils import dict_to_filter_params
from .base import BaseModelSerializer
__all__ = (
'NestedTagSerializer',
'WritableNestedSerializer',
)
class WritableNestedSerializer(BaseModelSerializer):
"""
Represents an object related through a ForeignKey field. On write, it accepts a primary key (PK) value or a
dictionary of attributes which can be used to uniquely identify the related object. This class should be
subclassed to return a full representation of the related object on read.
"""
def to_internal_value(self, data):
if data is None:
return None
# Dictionary of related object attributes
if isinstance(data, dict):
params = dict_to_filter_params(data)
queryset = self.Meta.model.objects
try:
return queryset.get(**params)
except ObjectDoesNotExist:
raise ValidationError(f"Related object not found using the provided attributes: {params}")
except MultipleObjectsReturned:
raise ValidationError(f"Multiple objects match the provided attributes: {params}")
except FieldError as e:
raise ValidationError(e)
# Integer PK of related object
try:
# Cast as integer in case a PK was mistakenly sent as a string
pk = int(data)
except (TypeError, ValueError):
raise ValidationError(
f"Related objects must be referenced by numeric ID or by dictionary of attributes. Received an "
f"unrecognized value: {data}"
)
# Look up object by PK
try:
return self.Meta.model.objects.get(pk=pk)
except ObjectDoesNotExist:
raise ValidationError(f"Related object not found using the provided numeric ID: {pk}")
# Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers
class NestedTagSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
class Meta:
model = Tag
fields = ['id', 'url', 'display', 'name', 'slug', 'color']

View File

@ -1,292 +1,17 @@
import logging
import platform
from collections import OrderedDict
from django import __version__ as DJANGO_VERSION
from django.apps import apps
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import ProtectedError
from django.shortcuts import get_object_or_404
from django_rq.queues import get_connection
from rest_framework import status
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet as ModelViewSet_
from rq.worker import Worker
from extras.models import ExportTemplate
from netbox.api import BulkOperationSerializer
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import SerializerNotFound
from utilities.api import get_serializer_for_model
HTTP_ACTIONS = {
'GET': 'view',
'OPTIONS': None,
'HEAD': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
#
# Mixins
#
class BulkUpdateModelMixin:
"""
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
For example:
PATCH /api/dcim/sites/
[
{
"id": 123,
"name": "New name"
},
{
"id": 456,
"status": "planned"
}
]
"""
def bulk_update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
qs = self.get_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
# Map update data by object ID
update_data = {
obj.pop('id'): obj for obj in request.data
}
data = self.perform_bulk_update(qs, update_data, partial=partial)
return Response(data, status=status.HTTP_200_OK)
def perform_bulk_update(self, objects, update_data, partial):
with transaction.atomic():
data_list = []
for obj in objects:
data = update_data.get(obj.id)
if hasattr(obj, 'snapshot'):
obj.snapshot()
serializer = self.get_serializer(obj, data=data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
data_list.append(serializer.data)
return data_list
def bulk_partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.bulk_update(request, *args, **kwargs)
class BulkDestroyModelMixin:
"""
Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
DELETE /api/dcim/sites/
[
{"id": 123},
{"id": 456}
]
"""
def bulk_destroy(self, request, *args, **kwargs):
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
qs = self.get_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
self.perform_bulk_destroy(qs)
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_bulk_destroy(self, objects):
with transaction.atomic():
for obj in objects:
if hasattr(obj, 'snapshot'):
obj.snapshot()
self.perform_destroy(obj)
class ObjectValidationMixin:
def _validate_objects(self, instance):
"""
Check that the provided instance or list of instances are matched by the current queryset. This confirms that
any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
"""
if type(instance) is list:
# Check that all instances are still included in the view's queryset
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
if conforming_count != len(instance):
raise ObjectDoesNotExist
else:
# Check that the instance is matched by the view's queryset
self.queryset.get(pk=instance.pk)
#
# Viewsets
#
class ModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet_):
"""
Extend DRF's ModelViewSet to support bulk update and delete functions.
"""
brief = False
brief_prefetch_fields = []
def get_object_with_snapshot(self):
"""
Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
record the "before" data in the changelog.
"""
obj = super().get_object()
if hasattr(obj, 'snapshot'):
obj.snapshot()
return obj
def get_serializer(self, *args, **kwargs):
# If a list of objects has been provided, initialize the serializer with many=True
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
def get_serializer_class(self):
logger = logging.getLogger('netbox.api.views.ModelViewSet')
# If using 'brief' mode, find and return the nested serializer for this model, if one exists
if self.brief:
logger.debug("Request is for 'brief' format; initializing nested serializer")
try:
serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
logger.debug(f"Using serializer {serializer}")
return serializer
except SerializerNotFound:
logger.debug(f"Nested serializer for {self.queryset.model} not found!")
# Fall back to the hard-coded serializer class
logger.debug(f"Using serializer {self.serializer_class}")
return self.serializer_class
def get_queryset(self):
# If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
if self.brief:
return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
return super().get_queryset()
def initialize_request(self, request, *args, **kwargs):
# Check if brief=True has been passed
if request.method == 'GET' and request.GET.get('brief'):
self.brief = True
return super().initialize_request(request, *args, **kwargs)
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if not request.user.is_authenticated:
return
# Restrict the view's QuerySet to allow only the permitted objects
action = HTTP_ACTIONS[request.method]
if action:
self.queryset = self.queryset.restrict(request.user, action)
def dispatch(self, request, *args, **kwargs):
logger = logging.getLogger('netbox.api.views.ModelViewSet')
try:
return super().dispatch(request, *args, **kwargs)
except ProtectedError as e:
protected_objects = list(e.protected_objects)
msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
logger.warning(msg)
return self.finalize_response(
request,
Response({'detail': msg}, status=409),
*args,
**kwargs
)
def list(self, request, *args, **kwargs):
"""
Overrides ListModelMixin to allow processing ExportTemplates.
"""
if 'export' in request.GET:
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
queryset = self.filter_queryset(self.get_queryset())
return et.render_to_response(queryset)
return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Creating new {model._meta.verbose_name}")
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def update(self, request, *args, **kwargs):
# Hotwire get_object() to ensure we save a pre-change snapshot
self.get_object = self.get_object_with_snapshot
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def destroy(self, request, *args, **kwargs):
# Hotwire get_object() to ensure we save a pre-change snapshot
self.get_object = self.get_object_with_snapshot
return super().destroy(request, *args, **kwargs)
def perform_destroy(self, instance):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
return super().perform_destroy(instance)
#
# Views
#
class APIRootView(APIView):
"""

View File

@ -0,0 +1,182 @@
import logging
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
from django.db import transaction
from django.db.models import ProtectedError
from django.shortcuts import get_object_or_404
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from extras.models import ExportTemplate
from netbox.api.exceptions import SerializerNotFound
from utilities.api import get_serializer_for_model
from .mixins import *
__all__ = (
'NetBoxModelViewSet',
)
HTTP_ACTIONS = {
'GET': 'view',
'OPTIONS': None,
'HEAD': 'view',
'POST': 'add',
'PUT': 'change',
'PATCH': 'change',
'DELETE': 'delete',
}
class NetBoxModelViewSet(BulkUpdateModelMixin, BulkDestroyModelMixin, ObjectValidationMixin, ModelViewSet):
"""
Extend DRF's ModelViewSet to support bulk update and delete functions.
"""
brief = False
brief_prefetch_fields = []
def get_object_with_snapshot(self):
"""
Save a pre-change snapshot of the object immediately after retrieving it. This snapshot will be used to
record the "before" data in the changelog.
"""
obj = super().get_object()
if hasattr(obj, 'snapshot'):
obj.snapshot()
return obj
def get_serializer(self, *args, **kwargs):
# If a list of objects has been provided, initialize the serializer with many=True
if isinstance(kwargs.get('data', {}), list):
kwargs['many'] = True
return super().get_serializer(*args, **kwargs)
def get_serializer_class(self):
logger = logging.getLogger('netbox.api.views.ModelViewSet')
# If using 'brief' mode, find and return the nested serializer for this model, if one exists
if self.brief:
logger.debug("Request is for 'brief' format; initializing nested serializer")
try:
serializer = get_serializer_for_model(self.queryset.model, prefix='Nested')
logger.debug(f"Using serializer {serializer}")
return serializer
except SerializerNotFound:
logger.debug(f"Nested serializer for {self.queryset.model} not found!")
# Fall back to the hard-coded serializer class
logger.debug(f"Using serializer {self.serializer_class}")
return self.serializer_class
def get_serializer_context(self):
"""
For models which support custom fields, populate the `custom_fields` context.
"""
context = super().get_serializer_context()
if hasattr(self.queryset.model, 'custom_fields'):
content_type = ContentType.objects.get_for_model(self.queryset.model)
context.update({
'custom_fields': content_type.custom_fields.all(),
})
return context
def get_queryset(self):
# If using brief mode, clear all prefetches from the queryset and append only brief_prefetch_fields (if any)
if self.brief:
return super().get_queryset().prefetch_related(None).prefetch_related(*self.brief_prefetch_fields)
return super().get_queryset()
def initialize_request(self, request, *args, **kwargs):
# Check if brief=True has been passed
if request.method == 'GET' and request.GET.get('brief'):
self.brief = True
return super().initialize_request(request, *args, **kwargs)
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if not request.user.is_authenticated:
return
# Restrict the view's QuerySet to allow only the permitted objects
action = HTTP_ACTIONS[request.method]
if action:
self.queryset = self.queryset.restrict(request.user, action)
def dispatch(self, request, *args, **kwargs):
logger = logging.getLogger('netbox.api.views.ModelViewSet')
try:
return super().dispatch(request, *args, **kwargs)
except ProtectedError as e:
protected_objects = list(e.protected_objects)
msg = f'Unable to delete object. {len(protected_objects)} dependent objects were found: '
msg += ', '.join([f'{obj} ({obj.pk})' for obj in protected_objects])
logger.warning(msg)
return self.finalize_response(
request,
Response({'detail': msg}, status=409),
*args,
**kwargs
)
def list(self, request, *args, **kwargs):
"""
Overrides ListModelMixin to allow processing ExportTemplates.
"""
if 'export' in request.GET:
content_type = ContentType.objects.get_for_model(self.get_serializer_class().Meta.model)
et = get_object_or_404(ExportTemplate, content_type=content_type, name=request.GET['export'])
queryset = self.filter_queryset(self.get_queryset())
return et.render_to_response(queryset)
return super().list(request, *args, **kwargs)
def perform_create(self, serializer):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Creating new {model._meta.verbose_name}")
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def update(self, request, *args, **kwargs):
# Hotwire get_object() to ensure we save a pre-change snapshot
self.get_object = self.get_object_with_snapshot
return super().update(request, *args, **kwargs)
def perform_update(self, serializer):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Updating {model._meta.verbose_name} {serializer.instance} (PK: {serializer.instance.pk})")
# Enforce object-level permissions on save()
try:
with transaction.atomic():
instance = serializer.save()
self._validate_objects(instance)
except ObjectDoesNotExist:
raise PermissionDenied()
def destroy(self, request, *args, **kwargs):
# Hotwire get_object() to ensure we save a pre-change snapshot
self.get_object = self.get_object_with_snapshot
return super().destroy(request, *args, **kwargs)
def perform_destroy(self, instance):
model = self.queryset.model
logger = logging.getLogger('netbox.api.views.ModelViewSet')
logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})")
return super().perform_destroy(instance)

View File

@ -0,0 +1,113 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from rest_framework import status
from rest_framework.response import Response
from netbox.api.serializers import BulkOperationSerializer
__all__ = (
'BulkUpdateModelMixin',
'BulkDestroyModelMixin',
'ObjectValidationMixin',
)
class BulkUpdateModelMixin:
"""
Support bulk modification of objects using the list endpoint for a model. Accepts a PATCH action with a list of one
or more JSON objects, each specifying the numeric ID of an object to be updated as well as the attributes to be set.
For example:
PATCH /api/dcim/sites/
[
{
"id": 123,
"name": "New name"
},
{
"id": 456,
"status": "planned"
}
]
"""
def bulk_update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
qs = self.get_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
# Map update data by object ID
update_data = {
obj.pop('id'): obj for obj in request.data
}
data = self.perform_bulk_update(qs, update_data, partial=partial)
return Response(data, status=status.HTTP_200_OK)
def perform_bulk_update(self, objects, update_data, partial):
with transaction.atomic():
data_list = []
for obj in objects:
data = update_data.get(obj.id)
if hasattr(obj, 'snapshot'):
obj.snapshot()
serializer = self.get_serializer(obj, data=data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
data_list.append(serializer.data)
return data_list
def bulk_partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.bulk_update(request, *args, **kwargs)
class BulkDestroyModelMixin:
"""
Support bulk deletion of objects using the list endpoint for a model. Accepts a DELETE action with a list of one
or more JSON objects, each specifying the numeric ID of an object to be deleted. For example:
DELETE /api/dcim/sites/
[
{"id": 123},
{"id": 456}
]
"""
def bulk_destroy(self, request, *args, **kwargs):
serializer = BulkOperationSerializer(data=request.data, many=True)
serializer.is_valid(raise_exception=True)
qs = self.get_queryset().filter(
pk__in=[o['id'] for o in serializer.data]
)
self.perform_bulk_destroy(qs)
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_bulk_destroy(self, objects):
with transaction.atomic():
for obj in objects:
if hasattr(obj, 'snapshot'):
obj.snapshot()
self.perform_destroy(obj)
class ObjectValidationMixin:
def _validate_objects(self, instance):
"""
Check that the provided instance or list of instances are matched by the current queryset. This confirms that
any newly created or modified objects abide by the attributes granted by any applicable ObjectPermissions.
"""
if type(instance) is list:
# Check that all instances are still included in the view's queryset
conforming_count = self.queryset.filter(pk__in=[obj.pk for obj in instance]).count()
if conforming_count != len(instance):
raise ObjectDoesNotExist
else:
# Check that the instance is matched by the view's queryset
self.queryset.get(pk=instance.pk)

View File

@ -3,7 +3,7 @@ from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers
from netbox.api import ChoiceField, ContentTypeField
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from tenancy.choices import ContactPriorityChoices
from tenancy.models import *
from utilities.api import get_serializer_for_model
@ -27,7 +27,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer):
]
class TenantSerializer(PrimaryModelSerializer):
class TenantSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
group = NestedTenantGroupSerializer(required=False, allow_null=True)
circuit_count = serializers.IntegerField(read_only=True)
@ -67,7 +67,7 @@ class ContactGroupSerializer(NestedGroupModelSerializer):
]
class ContactRoleSerializer(PrimaryModelSerializer):
class ContactRoleSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
class Meta:
@ -77,7 +77,7 @@ class ContactRoleSerializer(PrimaryModelSerializer):
]
class ContactSerializer(PrimaryModelSerializer):
class ContactSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
group = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
@ -89,7 +89,7 @@ class ContactSerializer(PrimaryModelSerializer):
]
class ContactAssignmentSerializer(PrimaryModelSerializer):
class ContactAssignmentSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
content_type = ContentTypeField(
queryset=ContentType.objects.all()

View File

@ -1,9 +1,9 @@
from rest_framework.routers import APIRootView
from circuits.models import Circuit
from dcim.models import Device, Rack, Site, Cable
from extras.api.views import CustomFieldModelViewSet
from dcim.models import Device, Rack, Site
from ipam.models import IPAddress, Prefix, VLAN, VRF
from netbox.api.viewsets import NetBoxModelViewSet
from tenancy import filtersets
from tenancy.models import *
from utilities.utils import count_related
@ -23,7 +23,7 @@ class TenancyRootView(APIRootView):
# Tenants
#
class TenantGroupViewSet(CustomFieldModelViewSet):
class TenantGroupViewSet(NetBoxModelViewSet):
queryset = TenantGroup.objects.add_related_count(
TenantGroup.objects.all(),
Tenant,
@ -35,7 +35,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.TenantGroupFilterSet
class TenantViewSet(CustomFieldModelViewSet):
class TenantViewSet(NetBoxModelViewSet):
queryset = Tenant.objects.prefetch_related(
'group', 'tags'
).annotate(
@ -58,7 +58,7 @@ class TenantViewSet(CustomFieldModelViewSet):
# Contacts
#
class ContactGroupViewSet(CustomFieldModelViewSet):
class ContactGroupViewSet(NetBoxModelViewSet):
queryset = ContactGroup.objects.add_related_count(
ContactGroup.objects.all(),
Contact,
@ -70,19 +70,19 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.ContactGroupFilterSet
class ContactRoleViewSet(CustomFieldModelViewSet):
class ContactRoleViewSet(NetBoxModelViewSet):
queryset = ContactRole.objects.prefetch_related('tags')
serializer_class = serializers.ContactRoleSerializer
filterset_class = filtersets.ContactRoleFilterSet
class ContactViewSet(CustomFieldModelViewSet):
class ContactViewSet(NetBoxModelViewSet):
queryset = Contact.objects.prefetch_related('group', 'tags')
serializer_class = serializers.ContactSerializer
filterset_class = filtersets.ContactFilterSet
class ContactAssignmentViewSet(CustomFieldModelViewSet):
class ContactAssignmentViewSet(NetBoxModelViewSet):
queryset = ContactAssignment.objects.prefetch_related('object', 'contact', 'role')
serializer_class = serializers.ContactAssignmentSerializer
filterset_class = filtersets.ContactAssignmentFilterSet

View File

@ -9,7 +9,7 @@ from rest_framework.status import HTTP_201_CREATED
from rest_framework.views import APIView
from rest_framework.viewsets import ViewSet
from netbox.api.views import ModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from users import filtersets
from users.models import ObjectPermission, Token, UserConfig
from utilities.querysets import RestrictedQuerySet
@ -29,13 +29,13 @@ class UsersRootView(APIRootView):
# Users and groups
#
class UserViewSet(ModelViewSet):
class UserViewSet(NetBoxModelViewSet):
queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
serializer_class = serializers.UserSerializer
filterset_class = filtersets.UserFilterSet
class GroupViewSet(ModelViewSet):
class GroupViewSet(NetBoxModelViewSet):
queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
serializer_class = serializers.GroupSerializer
filterset_class = filtersets.GroupFilterSet
@ -45,7 +45,7 @@ class GroupViewSet(ModelViewSet):
# REST API tokens
#
class TokenViewSet(ModelViewSet):
class TokenViewSet(NetBoxModelViewSet):
queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
serializer_class = serializers.TokenSerializer
filterset_class = filtersets.TokenFilterSet
@ -94,7 +94,7 @@ class TokenProvisionView(APIView):
# ObjectPermissions
#
class ObjectPermissionViewSet(ModelViewSet):
class ObjectPermissionViewSet(NetBoxModelViewSet):
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
serializer_class = serializers.ObjectPermissionSerializer
filterset_class = filtersets.ObjectPermissionFilterSet

View File

@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
from ipam.models import VLAN
from netbox.api import ChoiceField, SerializedPKRelatedField
from netbox.api.serializers import PrimaryModelSerializer
from netbox.api.serializers import NetBoxModelSerializer
from tenancy.api.nested_serializers import NestedTenantSerializer
from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -17,7 +17,7 @@ from .nested_serializers import *
# Clusters
#
class ClusterTypeSerializer(PrimaryModelSerializer):
class ClusterTypeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
cluster_count = serializers.IntegerField(read_only=True)
@ -29,7 +29,7 @@ class ClusterTypeSerializer(PrimaryModelSerializer):
]
class ClusterGroupSerializer(PrimaryModelSerializer):
class ClusterGroupSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
cluster_count = serializers.IntegerField(read_only=True)
@ -41,7 +41,7 @@ class ClusterGroupSerializer(PrimaryModelSerializer):
]
class ClusterSerializer(PrimaryModelSerializer):
class ClusterSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
type = NestedClusterTypeSerializer()
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
@ -62,7 +62,7 @@ class ClusterSerializer(PrimaryModelSerializer):
# Virtual machines
#
class VirtualMachineSerializer(PrimaryModelSerializer):
class VirtualMachineSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
site = NestedSiteSerializer(read_only=True)
@ -103,7 +103,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
# VM interfaces
#
class VMInterfaceSerializer(PrimaryModelSerializer):
class VMInterfaceSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
virtual_machine = NestedVirtualMachineSerializer()
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)

View File

@ -1,7 +1,8 @@
from rest_framework.routers import APIRootView
from dcim.models import Device
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet, ModelViewSet
from extras.api.views import ConfigContextQuerySetMixin
from netbox.api.viewsets import NetBoxModelViewSet
from utilities.utils import count_related
from virtualization import filtersets
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -20,7 +21,7 @@ class VirtualizationRootView(APIRootView):
# Clusters
#
class ClusterTypeViewSet(CustomFieldModelViewSet):
class ClusterTypeViewSet(NetBoxModelViewSet):
queryset = ClusterType.objects.annotate(
cluster_count=count_related(Cluster, 'type')
).prefetch_related('tags')
@ -28,7 +29,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.ClusterTypeFilterSet
class ClusterGroupViewSet(CustomFieldModelViewSet):
class ClusterGroupViewSet(NetBoxModelViewSet):
queryset = ClusterGroup.objects.annotate(
cluster_count=count_related(Cluster, 'group')
).prefetch_related('tags')
@ -36,7 +37,7 @@ class ClusterGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.ClusterGroupFilterSet
class ClusterViewSet(CustomFieldModelViewSet):
class ClusterViewSet(NetBoxModelViewSet):
queryset = Cluster.objects.prefetch_related(
'type', 'group', 'tenant', 'site', 'tags'
).annotate(
@ -51,7 +52,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
# Virtual machines
#
class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
queryset = VirtualMachine.objects.prefetch_related(
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
)
@ -78,7 +79,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
return serializers.VirtualMachineWithConfigContextSerializer
class VMInterfaceViewSet(ModelViewSet):
class VMInterfaceViewSet(NetBoxModelViewSet):
queryset = VMInterface.objects.prefetch_related(
'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
'fhrp_group_assignments',

View File

@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices
from dcim.api.serializers import NestedInterfaceSerializer
from ipam.api.serializers import NestedVLANSerializer
from netbox.api import ChoiceField
from netbox.api.serializers import NestedGroupModelSerializer, PrimaryModelSerializer
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
from wireless.choices import *
from wireless.models import *
from .nested_serializers import *
@ -29,7 +29,7 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer):
]
class WirelessLANSerializer(PrimaryModelSerializer):
class WirelessLANSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
vlan = NestedVLANSerializer(required=False, allow_null=True)
@ -44,7 +44,7 @@ class WirelessLANSerializer(PrimaryModelSerializer):
]
class WirelessLinkSerializer(PrimaryModelSerializer):
class WirelessLinkSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
status = ChoiceField(choices=LinkStatusChoices, required=False)
interface_a = NestedInterfaceSerializer()

View File

@ -1,6 +1,6 @@
from rest_framework.routers import APIRootView
from extras.api.views import CustomFieldModelViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from wireless import filtersets
from wireless.models import *
from . import serializers
@ -14,7 +14,7 @@ class WirelessRootView(APIRootView):
return 'Wireless'
class WirelessLANGroupViewSet(CustomFieldModelViewSet):
class WirelessLANGroupViewSet(NetBoxModelViewSet):
queryset = WirelessLANGroup.objects.add_related_count(
WirelessLANGroup.objects.all(),
WirelessLAN,
@ -26,13 +26,13 @@ class WirelessLANGroupViewSet(CustomFieldModelViewSet):
filterset_class = filtersets.WirelessLANGroupFilterSet
class WirelessLANViewSet(CustomFieldModelViewSet):
class WirelessLANViewSet(NetBoxModelViewSet):
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
serializer_class = serializers.WirelessLANSerializer
filterset_class = filtersets.WirelessLANFilterSet
class WirelessLinkViewSet(CustomFieldModelViewSet):
class WirelessLinkViewSet(NetBoxModelViewSet):
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
serializer_class = serializers.WirelessLinkSerializer
filterset_class = filtersets.WirelessLinkFilterSet