mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 04:02:52 -06: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:
commit
f559ceeb7f
@ -6,25 +6,37 @@ Generally speaking, there aren't many NetBox-specific components to implementing
|
|||||||
|
|
||||||
## Serializers
|
## 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
|
```python
|
||||||
from rest_framework.serializers import ModelSerializer
|
# api/serializers.py
|
||||||
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
from my_plugin.models import MyModel
|
from my_plugin.models import MyModel
|
||||||
|
|
||||||
class MyModelSerializer(ModelSerializer):
|
class MyModelSerializer(NetBoxModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MyModel
|
model = MyModel
|
||||||
fields = ('id', 'foo', 'bar')
|
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
|
```python
|
||||||
from rest_framework.viewsets import ModelViewSet
|
# api/views.py
|
||||||
|
from netbox.api.viewsets import ModelViewSet
|
||||||
from my_plugin.models import MyModel
|
from my_plugin.models import MyModel
|
||||||
from .serializers import MyModelSerializer
|
from .serializers import MyModelSerializer
|
||||||
|
|
||||||
@ -33,11 +45,16 @@ class MyModelViewSet(ModelViewSet):
|
|||||||
serializer_class = MyModelSerializer
|
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
|
```python
|
||||||
|
# api/urls.py
|
||||||
from rest_framework import routers
|
from rest_framework import routers
|
||||||
from .views import MyModelViewSet
|
from .views import MyModelViewSet
|
||||||
|
|
||||||
@ -46,7 +63,7 @@ router.register('my-model', MyModelViewSet)
|
|||||||
urlpatterns = router.urls
|
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
|
!!! 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.
|
||||||
|
@ -5,7 +5,7 @@ from circuits.models import *
|
|||||||
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
|
||||||
from dcim.api.serializers import LinkTerminationSerializer
|
from dcim.api.serializers import LinkTerminationSerializer
|
||||||
from netbox.api import ChoiceField
|
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from .nested_serializers import *
|
from .nested_serializers import *
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ from .nested_serializers import *
|
|||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderSerializer(PrimaryModelSerializer):
|
class ProviderSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ class ProviderSerializer(PrimaryModelSerializer):
|
|||||||
# Provider networks
|
# Provider networks
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderNetworkSerializer(PrimaryModelSerializer):
|
class ProviderNetworkSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ class ProviderNetworkSerializer(PrimaryModelSerializer):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeSerializer(PrimaryModelSerializer):
|
class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail')
|
||||||
provider = NestedProviderSerializer()
|
provider = NestedProviderSerializer()
|
||||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||||
|
@ -3,8 +3,7 @@ from rest_framework.routers import APIRootView
|
|||||||
from circuits import filtersets
|
from circuits import filtersets
|
||||||
from circuits.models import *
|
from circuits.models import *
|
||||||
from dcim.api.views import PassThroughPortMixin
|
from dcim.api.views import PassThroughPortMixin
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from netbox.api.views import ModelViewSet
|
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
from . import serializers
|
from . import serializers
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ class CircuitsRootView(APIRootView):
|
|||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderViewSet(CustomFieldModelViewSet):
|
class ProviderViewSet(NetBoxModelViewSet):
|
||||||
queryset = Provider.objects.prefetch_related('tags').annotate(
|
queryset = Provider.objects.prefetch_related('tags').annotate(
|
||||||
circuit_count=count_related(Circuit, 'provider')
|
circuit_count=count_related(Circuit, 'provider')
|
||||||
)
|
)
|
||||||
@ -33,7 +32,7 @@ class ProviderViewSet(CustomFieldModelViewSet):
|
|||||||
# Circuit Types
|
# Circuit Types
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTypeViewSet(CustomFieldModelViewSet):
|
class CircuitTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
queryset = CircuitType.objects.prefetch_related('tags').annotate(
|
||||||
circuit_count=count_related(Circuit, 'type')
|
circuit_count=count_related(Circuit, 'type')
|
||||||
)
|
)
|
||||||
@ -45,7 +44,7 @@ class CircuitTypeViewSet(CustomFieldModelViewSet):
|
|||||||
# Circuits
|
# Circuits
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitViewSet(CustomFieldModelViewSet):
|
class CircuitViewSet(NetBoxModelViewSet):
|
||||||
queryset = Circuit.objects.prefetch_related(
|
queryset = Circuit.objects.prefetch_related(
|
||||||
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
'type', 'tenant', 'provider', 'termination_a', 'termination_z'
|
||||||
).prefetch_related('tags')
|
).prefetch_related('tags')
|
||||||
@ -57,7 +56,7 @@ class CircuitViewSet(CustomFieldModelViewSet):
|
|||||||
# Circuit Terminations
|
# Circuit Terminations
|
||||||
#
|
#
|
||||||
|
|
||||||
class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
|
class CircuitTerminationViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = CircuitTermination.objects.prefetch_related(
|
queryset = CircuitTermination.objects.prefetch_related(
|
||||||
'circuit', 'site', 'provider_network', 'cable'
|
'circuit', 'site', 'provider_network', 'cable'
|
||||||
)
|
)
|
||||||
@ -70,7 +69,7 @@ class CircuitTerminationViewSet(PassThroughPortMixin, ModelViewSet):
|
|||||||
# Provider networks
|
# Provider networks
|
||||||
#
|
#
|
||||||
|
|
||||||
class ProviderNetworkViewSet(CustomFieldModelViewSet):
|
class ProviderNetworkViewSet(NetBoxModelViewSet):
|
||||||
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
queryset = ProviderNetwork.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ProviderNetworkSerializer
|
serializer_class = serializers.ProviderNetworkSerializer
|
||||||
filterset_class = filtersets.ProviderNetworkFilterSet
|
filterset_class = filtersets.ProviderNetworkFilterSet
|
||||||
|
@ -12,7 +12,7 @@ from ipam.api.nested_serializers import (
|
|||||||
from ipam.models import ASN, VLAN
|
from ipam.models import ASN, VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NestedGroupModelSerializer, PrimaryModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
|
||||||
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
status = ChoiceField(choices=SiteStatusChoices, required=False)
|
||||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||||
rack_count = serializers.IntegerField(read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||||
@ -212,7 +212,7 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
return obj['name']
|
return obj['name']
|
||||||
|
|
||||||
|
|
||||||
class RackReservationSerializer(PrimaryModelSerializer):
|
class RackReservationSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
|
||||||
rack = NestedRackSerializer()
|
rack = NestedRackSerializer()
|
||||||
user = NestedUserSerializer()
|
user = NestedUserSerializer()
|
||||||
@ -266,7 +266,7 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
|
|||||||
# Device/module types
|
# Device/module types
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerSerializer(PrimaryModelSerializer):
|
class ManufacturerSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||||
devicetype_count = serializers.IntegerField(read_only=True)
|
devicetype_count = serializers.IntegerField(read_only=True)
|
||||||
inventoryitem_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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||||
manufacturer = NestedManufacturerSerializer()
|
manufacturer = NestedManufacturerSerializer()
|
||||||
# module_count = serializers.IntegerField(read_only=True)
|
# module_count = serializers.IntegerField(read_only=True)
|
||||||
@ -487,7 +487,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
|||||||
# Devices
|
# Devices
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleSerializer(PrimaryModelSerializer):
|
class DeviceRoleSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
virtualmachine_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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||||
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
manufacturer = NestedManufacturerSerializer(required=False, allow_null=True)
|
||||||
device_count = serializers.IntegerField(read_only=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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||||
device_type = NestedDeviceTypeSerializer()
|
device_type = NestedDeviceTypeSerializer()
|
||||||
device_role = NestedDeviceRoleSerializer()
|
device_role = NestedDeviceRoleSerializer()
|
||||||
@ -556,7 +556,7 @@ class DeviceSerializer(PrimaryModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
class ModuleSerializer(PrimaryModelSerializer):
|
class ModuleSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module_bay = NestedModuleBaySerializer()
|
module_bay = NestedModuleBaySerializer()
|
||||||
@ -594,7 +594,7 @@ class DeviceNAPALMSerializer(serializers.Serializer):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsoleServerPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
class ConsoleServerPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer, ConnectedEndpointSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -768,7 +768,7 @@ class InterfaceSerializer(PrimaryModelSerializer, LinkTerminationSerializer, Con
|
|||||||
return super().validate(data)
|
return super().validate(data)
|
||||||
|
|
||||||
|
|
||||||
class RearPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
class RearPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
module = ComponentNestedModuleSerializer(
|
||||||
@ -798,7 +798,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer):
|
|||||||
fields = ['id', 'url', 'display', 'name', 'label']
|
fields = ['id', 'url', 'display', 'name', 'label']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortSerializer(PrimaryModelSerializer, LinkTerminationSerializer):
|
class FrontPortSerializer(NetBoxModelSerializer, LinkTerminationSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
module = ComponentNestedModuleSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
# installed_module = NestedModuleSerializer(required=False, allow_null=True)
|
# 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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
installed_device = NestedDeviceSerializer(required=False, allow_null=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
|
||||||
device = NestedDeviceSerializer()
|
device = NestedDeviceSerializer()
|
||||||
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
|
||||||
@ -879,7 +879,7 @@ class InventoryItemSerializer(PrimaryModelSerializer):
|
|||||||
# Device component roles
|
# Device component roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemRoleSerializer(PrimaryModelSerializer):
|
class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
@ -895,7 +895,7 @@ class InventoryItemRoleSerializer(PrimaryModelSerializer):
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableSerializer(PrimaryModelSerializer):
|
class CableSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
termination_a_type = ContentTypeField(
|
termination_a_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
|
||||||
@ -1001,7 +1001,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisSerializer(PrimaryModelSerializer):
|
class VirtualChassisSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||||
master = NestedDeviceSerializer(required=False)
|
master = NestedDeviceSerializer(required=False)
|
||||||
member_count = serializers.IntegerField(read_only=True)
|
member_count = serializers.IntegerField(read_only=True)
|
||||||
@ -1018,7 +1018,7 @@ class VirtualChassisSerializer(PrimaryModelSerializer):
|
|||||||
# Power panels
|
# Power panels
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelSerializer(PrimaryModelSerializer):
|
class PowerPanelSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||||
site = NestedSiteSerializer()
|
site = NestedSiteSerializer()
|
||||||
location = NestedLocationSerializer(
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail')
|
||||||
power_panel = NestedPowerPanelSerializer()
|
power_panel = NestedPowerPanelSerializer()
|
||||||
rack = NestedRackSerializer(
|
rack = NestedRackSerializer(
|
||||||
|
@ -14,12 +14,12 @@ from rest_framework.viewsets import ViewSet
|
|||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim import filtersets
|
from dcim import filtersets
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
from extras.api.views import ConfigContextQuerySetMixin
|
||||||
from ipam.models import Prefix, VLAN
|
from ipam.models import Prefix, VLAN
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.exceptions import ServiceUnavailable
|
from netbox.api.exceptions import ServiceUnavailable
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
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 netbox.config import get_config
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -103,7 +103,7 @@ class PassThroughPortMixin(object):
|
|||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
|
|
||||||
class RegionViewSet(CustomFieldModelViewSet):
|
class RegionViewSet(NetBoxModelViewSet):
|
||||||
queryset = Region.objects.add_related_count(
|
queryset = Region.objects.add_related_count(
|
||||||
Region.objects.all(),
|
Region.objects.all(),
|
||||||
Site,
|
Site,
|
||||||
@ -119,7 +119,7 @@ class RegionViewSet(CustomFieldModelViewSet):
|
|||||||
# Site groups
|
# Site groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteGroupViewSet(CustomFieldModelViewSet):
|
class SiteGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = SiteGroup.objects.add_related_count(
|
queryset = SiteGroup.objects.add_related_count(
|
||||||
SiteGroup.objects.all(),
|
SiteGroup.objects.all(),
|
||||||
Site,
|
Site,
|
||||||
@ -135,7 +135,7 @@ class SiteGroupViewSet(CustomFieldModelViewSet):
|
|||||||
# Sites
|
# Sites
|
||||||
#
|
#
|
||||||
|
|
||||||
class SiteViewSet(CustomFieldModelViewSet):
|
class SiteViewSet(NetBoxModelViewSet):
|
||||||
queryset = Site.objects.prefetch_related(
|
queryset = Site.objects.prefetch_related(
|
||||||
'region', 'tenant', 'asns', 'tags'
|
'region', 'tenant', 'asns', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -154,7 +154,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
|||||||
# Locations
|
# Locations
|
||||||
#
|
#
|
||||||
|
|
||||||
class LocationViewSet(CustomFieldModelViewSet):
|
class LocationViewSet(NetBoxModelViewSet):
|
||||||
queryset = Location.objects.add_related_count(
|
queryset = Location.objects.add_related_count(
|
||||||
Location.objects.add_related_count(
|
Location.objects.add_related_count(
|
||||||
Location.objects.all(),
|
Location.objects.all(),
|
||||||
@ -176,7 +176,7 @@ class LocationViewSet(CustomFieldModelViewSet):
|
|||||||
# Rack roles
|
# Rack roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackRoleViewSet(CustomFieldModelViewSet):
|
class RackRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
queryset = RackRole.objects.prefetch_related('tags').annotate(
|
||||||
rack_count=count_related(Rack, 'role')
|
rack_count=count_related(Rack, 'role')
|
||||||
)
|
)
|
||||||
@ -188,7 +188,7 @@ class RackRoleViewSet(CustomFieldModelViewSet):
|
|||||||
# Racks
|
# Racks
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackViewSet(CustomFieldModelViewSet):
|
class RackViewSet(NetBoxModelViewSet):
|
||||||
queryset = Rack.objects.prefetch_related(
|
queryset = Rack.objects.prefetch_related(
|
||||||
'site', 'location', 'role', 'tenant', 'tags'
|
'site', 'location', 'role', 'tenant', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -250,7 +250,7 @@ class RackViewSet(CustomFieldModelViewSet):
|
|||||||
# Rack reservations
|
# Rack reservations
|
||||||
#
|
#
|
||||||
|
|
||||||
class RackReservationViewSet(ModelViewSet):
|
class RackReservationViewSet(NetBoxModelViewSet):
|
||||||
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
queryset = RackReservation.objects.prefetch_related('rack', 'user', 'tenant')
|
||||||
serializer_class = serializers.RackReservationSerializer
|
serializer_class = serializers.RackReservationSerializer
|
||||||
filterset_class = filtersets.RackReservationFilterSet
|
filterset_class = filtersets.RackReservationFilterSet
|
||||||
@ -260,7 +260,7 @@ class RackReservationViewSet(ModelViewSet):
|
|||||||
# Manufacturers
|
# Manufacturers
|
||||||
#
|
#
|
||||||
|
|
||||||
class ManufacturerViewSet(CustomFieldModelViewSet):
|
class ManufacturerViewSet(NetBoxModelViewSet):
|
||||||
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
queryset = Manufacturer.objects.prefetch_related('tags').annotate(
|
||||||
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
devicetype_count=count_related(DeviceType, 'manufacturer'),
|
||||||
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
inventoryitem_count=count_related(InventoryItem, 'manufacturer'),
|
||||||
@ -274,7 +274,7 @@ class ManufacturerViewSet(CustomFieldModelViewSet):
|
|||||||
# Device/module types
|
# Device/module types
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceTypeViewSet(CustomFieldModelViewSet):
|
class DeviceTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
queryset = DeviceType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||||
device_count=count_related(Device, 'device_type')
|
device_count=count_related(Device, 'device_type')
|
||||||
)
|
)
|
||||||
@ -283,7 +283,7 @@ class DeviceTypeViewSet(CustomFieldModelViewSet):
|
|||||||
brief_prefetch_fields = ['manufacturer']
|
brief_prefetch_fields = ['manufacturer']
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeViewSet(CustomFieldModelViewSet):
|
class ModuleTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
queryset = ModuleType.objects.prefetch_related('manufacturer', 'tags').annotate(
|
||||||
# module_count=count_related(Module, 'module_type')
|
# module_count=count_related(Module, 'module_type')
|
||||||
)
|
)
|
||||||
@ -296,61 +296,61 @@ class ModuleTypeViewSet(CustomFieldModelViewSet):
|
|||||||
# Device type components
|
# Device type components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortTemplateViewSet(ModelViewSet):
|
class ConsolePortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ConsolePortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsolePortTemplateSerializer
|
serializer_class = serializers.ConsolePortTemplateSerializer
|
||||||
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
filterset_class = filtersets.ConsolePortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateViewSet(ModelViewSet):
|
class ConsoleServerPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ConsoleServerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
serializer_class = serializers.ConsoleServerPortTemplateSerializer
|
||||||
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
filterset_class = filtersets.ConsoleServerPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateViewSet(ModelViewSet):
|
class PowerPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = PowerPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerPortTemplateSerializer
|
serializer_class = serializers.PowerPortTemplateSerializer
|
||||||
filterset_class = filtersets.PowerPortTemplateFilterSet
|
filterset_class = filtersets.PowerPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateViewSet(ModelViewSet):
|
class PowerOutletTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = PowerOutletTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.PowerOutletTemplateSerializer
|
serializer_class = serializers.PowerOutletTemplateSerializer
|
||||||
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
filterset_class = filtersets.PowerOutletTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateViewSet(ModelViewSet):
|
class InterfaceTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = InterfaceTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.InterfaceTemplateSerializer
|
serializer_class = serializers.InterfaceTemplateSerializer
|
||||||
filterset_class = filtersets.InterfaceTemplateFilterSet
|
filterset_class = filtersets.InterfaceTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateViewSet(ModelViewSet):
|
class FrontPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = FrontPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.FrontPortTemplateSerializer
|
serializer_class = serializers.FrontPortTemplateSerializer
|
||||||
filterset_class = filtersets.FrontPortTemplateFilterSet
|
filterset_class = filtersets.FrontPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RearPortTemplateViewSet(ModelViewSet):
|
class RearPortTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = RearPortTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.RearPortTemplateSerializer
|
serializer_class = serializers.RearPortTemplateSerializer
|
||||||
filterset_class = filtersets.RearPortTemplateFilterSet
|
filterset_class = filtersets.RearPortTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayTemplateViewSet(ModelViewSet):
|
class ModuleBayTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = ModuleBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.ModuleBayTemplateSerializer
|
serializer_class = serializers.ModuleBayTemplateSerializer
|
||||||
filterset_class = filtersets.ModuleBayTemplateFilterSet
|
filterset_class = filtersets.ModuleBayTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateViewSet(ModelViewSet):
|
class DeviceBayTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
queryset = DeviceBayTemplate.objects.prefetch_related('device_type__manufacturer')
|
||||||
serializer_class = serializers.DeviceBayTemplateSerializer
|
serializer_class = serializers.DeviceBayTemplateSerializer
|
||||||
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
filterset_class = filtersets.DeviceBayTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemTemplateViewSet(ModelViewSet):
|
class InventoryItemTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
|
queryset = InventoryItemTemplate.objects.prefetch_related('device_type__manufacturer', 'role')
|
||||||
serializer_class = serializers.InventoryItemTemplateSerializer
|
serializer_class = serializers.InventoryItemTemplateSerializer
|
||||||
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
filterset_class = filtersets.InventoryItemTemplateFilterSet
|
||||||
@ -360,7 +360,7 @@ class InventoryItemTemplateViewSet(ModelViewSet):
|
|||||||
# Device roles
|
# Device roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceRoleViewSet(CustomFieldModelViewSet):
|
class DeviceRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'device_role'),
|
device_count=count_related(Device, 'device_role'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'role')
|
virtualmachine_count=count_related(VirtualMachine, 'role')
|
||||||
@ -373,7 +373,7 @@ class DeviceRoleViewSet(CustomFieldModelViewSet):
|
|||||||
# Platforms
|
# Platforms
|
||||||
#
|
#
|
||||||
|
|
||||||
class PlatformViewSet(CustomFieldModelViewSet):
|
class PlatformViewSet(NetBoxModelViewSet):
|
||||||
queryset = Platform.objects.prefetch_related('tags').annotate(
|
queryset = Platform.objects.prefetch_related('tags').annotate(
|
||||||
device_count=count_related(Device, 'platform'),
|
device_count=count_related(Device, 'platform'),
|
||||||
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
virtualmachine_count=count_related(VirtualMachine, 'platform')
|
||||||
@ -386,7 +386,7 @@ class PlatformViewSet(CustomFieldModelViewSet):
|
|||||||
# Devices/modules
|
# Devices/modules
|
||||||
#
|
#
|
||||||
|
|
||||||
class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
class DeviceViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||||
queryset = Device.objects.prefetch_related(
|
queryset = Device.objects.prefetch_related(
|
||||||
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'location', 'rack', 'parent_bay',
|
||||||
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
'virtual_chassis__master', 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags',
|
||||||
@ -532,7 +532,7 @@ class DeviceViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
|||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
class ModuleViewSet(CustomFieldModelViewSet):
|
class ModuleViewSet(NetBoxModelViewSet):
|
||||||
queryset = Module.objects.prefetch_related(
|
queryset = Module.objects.prefetch_related(
|
||||||
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
'device', 'module_bay', 'module_type__manufacturer', 'tags',
|
||||||
)
|
)
|
||||||
@ -544,7 +544,7 @@ class ModuleViewSet(CustomFieldModelViewSet):
|
|||||||
# Device components
|
# Device components
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
class ConsolePortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsolePort.objects.prefetch_related(
|
queryset = ConsolePort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
@ -553,7 +553,7 @@ class ConsolePortViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
class ConsoleServerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = ConsoleServerPort.objects.prefetch_related(
|
queryset = ConsoleServerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
@ -562,7 +562,7 @@ class ConsoleServerPortViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
class PowerPortViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerPort.objects.prefetch_related(
|
queryset = PowerPort.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
@ -571,7 +571,7 @@ class PowerPortViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
class PowerOutletViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerOutlet.objects.prefetch_related(
|
queryset = PowerOutlet.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
'device', 'module__module_bay', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
@ -580,7 +580,7 @@ class PowerOutletViewSet(PathEndpointMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class InterfaceViewSet(PathEndpointMixin, ModelViewSet):
|
class InterfaceViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = Interface.objects.prefetch_related(
|
queryset = Interface.objects.prefetch_related(
|
||||||
'device', 'module__module_bay', 'parent', 'bridge', 'lag', '_path__destination', 'cable', '_link_peer',
|
'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'
|
'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']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
|
class FrontPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = FrontPort.objects.prefetch_related(
|
queryset = FrontPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
|
'device__device_type__manufacturer', 'module__module_bay', 'rear_port', 'cable', 'tags'
|
||||||
)
|
)
|
||||||
@ -599,7 +599,7 @@ class FrontPortViewSet(PassThroughPortMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
|
class RearPortViewSet(PassThroughPortMixin, NetBoxModelViewSet):
|
||||||
queryset = RearPort.objects.prefetch_related(
|
queryset = RearPort.objects.prefetch_related(
|
||||||
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
|
'device__device_type__manufacturer', 'module__module_bay', 'cable', 'tags'
|
||||||
)
|
)
|
||||||
@ -608,21 +608,21 @@ class RearPortViewSet(PassThroughPortMixin, ModelViewSet):
|
|||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class ModuleBayViewSet(ModelViewSet):
|
class ModuleBayViewSet(NetBoxModelViewSet):
|
||||||
queryset = ModuleBay.objects.prefetch_related('tags')
|
queryset = ModuleBay.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ModuleBaySerializer
|
serializer_class = serializers.ModuleBaySerializer
|
||||||
filterset_class = filtersets.ModuleBayFilterSet
|
filterset_class = filtersets.ModuleBayFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class DeviceBayViewSet(ModelViewSet):
|
class DeviceBayViewSet(NetBoxModelViewSet):
|
||||||
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
|
queryset = DeviceBay.objects.prefetch_related('installed_device', 'tags')
|
||||||
serializer_class = serializers.DeviceBaySerializer
|
serializer_class = serializers.DeviceBaySerializer
|
||||||
filterset_class = filtersets.DeviceBayFilterSet
|
filterset_class = filtersets.DeviceBayFilterSet
|
||||||
brief_prefetch_fields = ['device']
|
brief_prefetch_fields = ['device']
|
||||||
|
|
||||||
|
|
||||||
class InventoryItemViewSet(ModelViewSet):
|
class InventoryItemViewSet(NetBoxModelViewSet):
|
||||||
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
|
queryset = InventoryItem.objects.prefetch_related('device', 'manufacturer', 'tags')
|
||||||
serializer_class = serializers.InventoryItemSerializer
|
serializer_class = serializers.InventoryItemSerializer
|
||||||
filterset_class = filtersets.InventoryItemFilterSet
|
filterset_class = filtersets.InventoryItemFilterSet
|
||||||
@ -633,7 +633,7 @@ class InventoryItemViewSet(ModelViewSet):
|
|||||||
# Device component roles
|
# Device component roles
|
||||||
#
|
#
|
||||||
|
|
||||||
class InventoryItemRoleViewSet(CustomFieldModelViewSet):
|
class InventoryItemRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
|
queryset = InventoryItemRole.objects.prefetch_related('tags').annotate(
|
||||||
inventoryitem_count=count_related(InventoryItem, 'role')
|
inventoryitem_count=count_related(InventoryItem, 'role')
|
||||||
)
|
)
|
||||||
@ -645,7 +645,7 @@ class InventoryItemRoleViewSet(CustomFieldModelViewSet):
|
|||||||
# Cables
|
# Cables
|
||||||
#
|
#
|
||||||
|
|
||||||
class CableViewSet(ModelViewSet):
|
class CableViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = Cable.objects.prefetch_related(
|
queryset = Cable.objects.prefetch_related(
|
||||||
'termination_a', 'termination_b'
|
'termination_a', 'termination_b'
|
||||||
@ -658,7 +658,7 @@ class CableViewSet(ModelViewSet):
|
|||||||
# Virtual chassis
|
# Virtual chassis
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualChassisViewSet(ModelViewSet):
|
class VirtualChassisViewSet(NetBoxModelViewSet):
|
||||||
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
|
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
|
||||||
member_count=count_related(Device, 'virtual_chassis')
|
member_count=count_related(Device, 'virtual_chassis')
|
||||||
)
|
)
|
||||||
@ -671,7 +671,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
|||||||
# Power panels
|
# Power panels
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerPanelViewSet(ModelViewSet):
|
class PowerPanelViewSet(NetBoxModelViewSet):
|
||||||
queryset = PowerPanel.objects.prefetch_related(
|
queryset = PowerPanel.objects.prefetch_related(
|
||||||
'site', 'location'
|
'site', 'location'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -685,7 +685,7 @@ class PowerPanelViewSet(ModelViewSet):
|
|||||||
# Power feeds
|
# Power feeds
|
||||||
#
|
#
|
||||||
|
|
||||||
class PowerFeedViewSet(PathEndpointMixin, CustomFieldModelViewSet):
|
class PowerFeedViewSet(PathEndpointMixin, NetBoxModelViewSet):
|
||||||
queryset = PowerFeed.objects.prefetch_related(
|
queryset = PowerFeed.objects.prefetch_related(
|
||||||
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
'power_panel', 'rack', '_path__destination', 'cable', '_link_peer', 'tags'
|
||||||
)
|
)
|
||||||
|
@ -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 extras.scripts import get_script, get_scripts, run_script
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
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.exceptions import RQWorkerNotRunningException
|
||||||
from utilities.utils import copy_safe_request, count_related
|
from utilities.utils import copy_safe_request, count_related
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -58,7 +58,7 @@ class ConfigContextQuerySetMixin:
|
|||||||
# Webhooks
|
# Webhooks
|
||||||
#
|
#
|
||||||
|
|
||||||
class WebhookViewSet(ModelViewSet):
|
class WebhookViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = Webhook.objects.all()
|
queryset = Webhook.objects.all()
|
||||||
serializer_class = serializers.WebhookSerializer
|
serializer_class = serializers.WebhookSerializer
|
||||||
@ -69,36 +69,18 @@ class WebhookViewSet(ModelViewSet):
|
|||||||
# Custom fields
|
# Custom fields
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomFieldViewSet(ModelViewSet):
|
class CustomFieldViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = CustomField.objects.all()
|
queryset = CustomField.objects.all()
|
||||||
serializer_class = serializers.CustomFieldSerializer
|
serializer_class = serializers.CustomFieldSerializer
|
||||||
filterset_class = filtersets.CustomFieldFilterSet
|
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
|
# Custom links
|
||||||
#
|
#
|
||||||
|
|
||||||
class CustomLinkViewSet(ModelViewSet):
|
class CustomLinkViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = CustomLink.objects.all()
|
queryset = CustomLink.objects.all()
|
||||||
serializer_class = serializers.CustomLinkSerializer
|
serializer_class = serializers.CustomLinkSerializer
|
||||||
@ -109,7 +91,7 @@ class CustomLinkViewSet(ModelViewSet):
|
|||||||
# Export templates
|
# Export templates
|
||||||
#
|
#
|
||||||
|
|
||||||
class ExportTemplateViewSet(ModelViewSet):
|
class ExportTemplateViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ExportTemplate.objects.all()
|
queryset = ExportTemplate.objects.all()
|
||||||
serializer_class = serializers.ExportTemplateSerializer
|
serializer_class = serializers.ExportTemplateSerializer
|
||||||
@ -120,7 +102,7 @@ class ExportTemplateViewSet(ModelViewSet):
|
|||||||
# Tags
|
# Tags
|
||||||
#
|
#
|
||||||
|
|
||||||
class TagViewSet(ModelViewSet):
|
class TagViewSet(NetBoxModelViewSet):
|
||||||
queryset = Tag.objects.annotate(
|
queryset = Tag.objects.annotate(
|
||||||
tagged_items=count_related(TaggedItem, 'tag')
|
tagged_items=count_related(TaggedItem, 'tag')
|
||||||
)
|
)
|
||||||
@ -132,7 +114,7 @@ class TagViewSet(ModelViewSet):
|
|||||||
# Image attachments
|
# Image attachments
|
||||||
#
|
#
|
||||||
|
|
||||||
class ImageAttachmentViewSet(ModelViewSet):
|
class ImageAttachmentViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = ImageAttachment.objects.all()
|
queryset = ImageAttachment.objects.all()
|
||||||
serializer_class = serializers.ImageAttachmentSerializer
|
serializer_class = serializers.ImageAttachmentSerializer
|
||||||
@ -143,7 +125,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
|||||||
# Journal entries
|
# Journal entries
|
||||||
#
|
#
|
||||||
|
|
||||||
class JournalEntryViewSet(ModelViewSet):
|
class JournalEntryViewSet(NetBoxModelViewSet):
|
||||||
metadata_class = ContentTypeMetadata
|
metadata_class = ContentTypeMetadata
|
||||||
queryset = JournalEntry.objects.all()
|
queryset = JournalEntry.objects.all()
|
||||||
serializer_class = serializers.JournalEntrySerializer
|
serializer_class = serializers.JournalEntrySerializer
|
||||||
@ -154,7 +136,7 @@ class JournalEntryViewSet(ModelViewSet):
|
|||||||
# Config contexts
|
# Config contexts
|
||||||
#
|
#
|
||||||
|
|
||||||
class ConfigContextViewSet(ModelViewSet):
|
class ConfigContextViewSet(NetBoxModelViewSet):
|
||||||
queryset = ConfigContext.objects.prefetch_related(
|
queryset = ConfigContext.objects.prefetch_related(
|
||||||
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
|
||||||
)
|
)
|
||||||
|
@ -9,7 +9,7 @@ from ipam.choices import *
|
|||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from utilities.api import get_serializer_for_model
|
from utilities.api import get_serializer_for_model
|
||||||
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
||||||
@ -20,7 +20,7 @@ from .nested_serializers import *
|
|||||||
# ASNs
|
# ASNs
|
||||||
#
|
#
|
||||||
|
|
||||||
class ASNSerializer(PrimaryModelSerializer):
|
class ASNSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
site_count = serializers.IntegerField(read_only=True)
|
site_count = serializers.IntegerField(read_only=True)
|
||||||
@ -37,7 +37,7 @@ class ASNSerializer(PrimaryModelSerializer):
|
|||||||
# VRFs
|
# VRFs
|
||||||
#
|
#
|
||||||
|
|
||||||
class VRFSerializer(PrimaryModelSerializer):
|
class VRFSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
import_targets = SerializedPKRelatedField(
|
import_targets = SerializedPKRelatedField(
|
||||||
@ -67,7 +67,7 @@ class VRFSerializer(PrimaryModelSerializer):
|
|||||||
# Route targets
|
# Route targets
|
||||||
#
|
#
|
||||||
|
|
||||||
class RouteTargetSerializer(PrimaryModelSerializer):
|
class RouteTargetSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail')
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
|
|
||||||
@ -82,7 +82,7 @@ class RouteTargetSerializer(PrimaryModelSerializer):
|
|||||||
# RIRs/aggregates
|
# RIRs/aggregates
|
||||||
#
|
#
|
||||||
|
|
||||||
class RIRSerializer(PrimaryModelSerializer):
|
class RIRSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||||
aggregate_count = serializers.IntegerField(read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
rir = NestedRIRSerializer()
|
rir = NestedRIRSerializer()
|
||||||
@ -113,7 +113,7 @@ class AggregateSerializer(PrimaryModelSerializer):
|
|||||||
# FHRP Groups
|
# FHRP Groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class FHRPGroupSerializer(PrimaryModelSerializer):
|
class FHRPGroupSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail')
|
||||||
ip_addresses = NestedIPAddressSerializer(many=True, read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail')
|
||||||
group = NestedFHRPGroupSerializer()
|
group = NestedFHRPGroupSerializer()
|
||||||
interface_type = ContentTypeField(
|
interface_type = ContentTypeField(
|
||||||
@ -153,7 +153,7 @@ class FHRPGroupAssignmentSerializer(PrimaryModelSerializer):
|
|||||||
# VLANs
|
# VLANs
|
||||||
#
|
#
|
||||||
|
|
||||||
class RoleSerializer(PrimaryModelSerializer):
|
class RoleSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
vlan_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')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||||
scope_type = ContentTypeField(
|
scope_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(
|
queryset=ContentType.objects.filter(
|
||||||
@ -196,7 +196,7 @@ class VLANGroupSerializer(PrimaryModelSerializer):
|
|||||||
return serializer(obj.scope, context=context).data
|
return serializer(obj.scope, context=context).data
|
||||||
|
|
||||||
|
|
||||||
class VLANSerializer(PrimaryModelSerializer):
|
class VLANSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail')
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None)
|
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)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||||
@ -251,7 +251,7 @@ class CreateAvailableVLANSerializer(PrimaryModelSerializer):
|
|||||||
# Prefixes
|
# Prefixes
|
||||||
#
|
#
|
||||||
|
|
||||||
class PrefixSerializer(PrimaryModelSerializer):
|
class PrefixSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
site = NestedSiteSerializer(required=False, allow_null=True)
|
site = NestedSiteSerializer(required=False, allow_null=True)
|
||||||
@ -323,7 +323,7 @@ class AvailablePrefixSerializer(serializers.Serializer):
|
|||||||
# IP ranges
|
# IP ranges
|
||||||
#
|
#
|
||||||
|
|
||||||
class IPRangeSerializer(PrimaryModelSerializer):
|
class IPRangeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
@ -345,7 +345,7 @@ class IPRangeSerializer(PrimaryModelSerializer):
|
|||||||
# IP addresses
|
# IP addresses
|
||||||
#
|
#
|
||||||
|
|
||||||
class IPAddressSerializer(PrimaryModelSerializer):
|
class IPAddressSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail')
|
||||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||||
@ -403,7 +403,7 @@ class AvailableIPSerializer(serializers.Serializer):
|
|||||||
# Services
|
# Services
|
||||||
#
|
#
|
||||||
|
|
||||||
class ServiceTemplateSerializer(PrimaryModelSerializer):
|
class ServiceTemplateSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail')
|
||||||
protocol = ChoiceField(choices=ServiceProtocolChoices, required=False)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail')
|
||||||
device = NestedDeviceSerializer(required=False, allow_null=True)
|
device = NestedDeviceSerializer(required=False, allow_null=True)
|
||||||
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True)
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django_pglocks import advisory_lock
|
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django_pglocks import advisory_lock
|
||||||
from drf_yasg.utils import swagger_auto_schema
|
from drf_yasg.utils import swagger_auto_schema
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
|
||||||
from dcim.models import Site
|
from dcim.models import Site
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
|
||||||
from ipam import filtersets
|
from ipam import filtersets
|
||||||
from ipam.models import *
|
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 netbox.config import get_config
|
||||||
from utilities.constants import ADVISORY_LOCK_KEYS
|
from utilities.constants import ADVISORY_LOCK_KEYS
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -32,13 +31,13 @@ class IPAMRootView(APIRootView):
|
|||||||
# Viewsets
|
# Viewsets
|
||||||
#
|
#
|
||||||
|
|
||||||
class ASNViewSet(CustomFieldModelViewSet):
|
class ASNViewSet(NetBoxModelViewSet):
|
||||||
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
|
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
|
||||||
serializer_class = serializers.ASNSerializer
|
serializer_class = serializers.ASNSerializer
|
||||||
filterset_class = filtersets.ASNFilterSet
|
filterset_class = filtersets.ASNFilterSet
|
||||||
|
|
||||||
|
|
||||||
class VRFViewSet(CustomFieldModelViewSet):
|
class VRFViewSet(NetBoxModelViewSet):
|
||||||
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
|
queryset = VRF.objects.prefetch_related('tenant').prefetch_related(
|
||||||
'import_targets', 'export_targets', 'tags'
|
'import_targets', 'export_targets', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -49,13 +48,13 @@ class VRFViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.VRFFilterSet
|
filterset_class = filtersets.VRFFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RouteTargetViewSet(CustomFieldModelViewSet):
|
class RouteTargetViewSet(NetBoxModelViewSet):
|
||||||
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
|
queryset = RouteTarget.objects.prefetch_related('tenant').prefetch_related('tags')
|
||||||
serializer_class = serializers.RouteTargetSerializer
|
serializer_class = serializers.RouteTargetSerializer
|
||||||
filterset_class = filtersets.RouteTargetFilterSet
|
filterset_class = filtersets.RouteTargetFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RIRViewSet(CustomFieldModelViewSet):
|
class RIRViewSet(NetBoxModelViewSet):
|
||||||
queryset = RIR.objects.annotate(
|
queryset = RIR.objects.annotate(
|
||||||
aggregate_count=count_related(Aggregate, 'rir')
|
aggregate_count=count_related(Aggregate, 'rir')
|
||||||
).prefetch_related('tags')
|
).prefetch_related('tags')
|
||||||
@ -63,13 +62,13 @@ class RIRViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.RIRFilterSet
|
filterset_class = filtersets.RIRFilterSet
|
||||||
|
|
||||||
|
|
||||||
class AggregateViewSet(CustomFieldModelViewSet):
|
class AggregateViewSet(NetBoxModelViewSet):
|
||||||
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
|
queryset = Aggregate.objects.prefetch_related('rir').prefetch_related('tags')
|
||||||
serializer_class = serializers.AggregateSerializer
|
serializer_class = serializers.AggregateSerializer
|
||||||
filterset_class = filtersets.AggregateFilterSet
|
filterset_class = filtersets.AggregateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class RoleViewSet(CustomFieldModelViewSet):
|
class RoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = Role.objects.annotate(
|
queryset = Role.objects.annotate(
|
||||||
prefix_count=count_related(Prefix, 'role'),
|
prefix_count=count_related(Prefix, 'role'),
|
||||||
vlan_count=count_related(VLAN, 'role')
|
vlan_count=count_related(VLAN, 'role')
|
||||||
@ -78,7 +77,7 @@ class RoleViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.RoleFilterSet
|
filterset_class = filtersets.RoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class PrefixViewSet(CustomFieldModelViewSet):
|
class PrefixViewSet(NetBoxModelViewSet):
|
||||||
queryset = Prefix.objects.prefetch_related(
|
queryset = Prefix.objects.prefetch_related(
|
||||||
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
|
'site', 'vrf__tenant', 'tenant', 'vlan', 'role', 'tags'
|
||||||
)
|
)
|
||||||
@ -93,7 +92,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
return super().get_serializer_class()
|
return super().get_serializer_class()
|
||||||
|
|
||||||
|
|
||||||
class IPRangeViewSet(CustomFieldModelViewSet):
|
class IPRangeViewSet(NetBoxModelViewSet):
|
||||||
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
|
queryset = IPRange.objects.prefetch_related('vrf', 'role', 'tenant', 'tags')
|
||||||
serializer_class = serializers.IPRangeSerializer
|
serializer_class = serializers.IPRangeSerializer
|
||||||
filterset_class = filtersets.IPRangeFilterSet
|
filterset_class = filtersets.IPRangeFilterSet
|
||||||
@ -101,7 +100,7 @@ class IPRangeViewSet(CustomFieldModelViewSet):
|
|||||||
parent_model = IPRange # AvailableIPsMixin
|
parent_model = IPRange # AvailableIPsMixin
|
||||||
|
|
||||||
|
|
||||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
class IPAddressViewSet(NetBoxModelViewSet):
|
||||||
queryset = IPAddress.objects.prefetch_related(
|
queryset = IPAddress.objects.prefetch_related(
|
||||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||||
)
|
)
|
||||||
@ -109,20 +108,20 @@ class IPAddressViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.IPAddressFilterSet
|
filterset_class = filtersets.IPAddressFilterSet
|
||||||
|
|
||||||
|
|
||||||
class FHRPGroupViewSet(CustomFieldModelViewSet):
|
class FHRPGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
|
queryset = FHRPGroup.objects.prefetch_related('ip_addresses', 'tags')
|
||||||
serializer_class = serializers.FHRPGroupSerializer
|
serializer_class = serializers.FHRPGroupSerializer
|
||||||
filterset_class = filtersets.FHRPGroupFilterSet
|
filterset_class = filtersets.FHRPGroupFilterSet
|
||||||
brief_prefetch_fields = ('ip_addresses',)
|
brief_prefetch_fields = ('ip_addresses',)
|
||||||
|
|
||||||
|
|
||||||
class FHRPGroupAssignmentViewSet(CustomFieldModelViewSet):
|
class FHRPGroupAssignmentViewSet(NetBoxModelViewSet):
|
||||||
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
|
queryset = FHRPGroupAssignment.objects.prefetch_related('group', 'interface')
|
||||||
serializer_class = serializers.FHRPGroupAssignmentSerializer
|
serializer_class = serializers.FHRPGroupAssignmentSerializer
|
||||||
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
filterset_class = filtersets.FHRPGroupAssignmentFilterSet
|
||||||
|
|
||||||
|
|
||||||
class VLANGroupViewSet(CustomFieldModelViewSet):
|
class VLANGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = VLANGroup.objects.annotate(
|
queryset = VLANGroup.objects.annotate(
|
||||||
vlan_count=count_related(VLAN, 'group')
|
vlan_count=count_related(VLAN, 'group')
|
||||||
).prefetch_related('tags')
|
).prefetch_related('tags')
|
||||||
@ -130,7 +129,7 @@ class VLANGroupViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.VLANGroupFilterSet
|
filterset_class = filtersets.VLANGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class VLANViewSet(CustomFieldModelViewSet):
|
class VLANViewSet(NetBoxModelViewSet):
|
||||||
queryset = VLAN.objects.prefetch_related(
|
queryset = VLAN.objects.prefetch_related(
|
||||||
'site', 'group', 'tenant', 'role', 'tags'
|
'site', 'group', 'tenant', 'role', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -140,13 +139,13 @@ class VLANViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.VLANFilterSet
|
filterset_class = filtersets.VLANFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ServiceTemplateViewSet(CustomFieldModelViewSet):
|
class ServiceTemplateViewSet(NetBoxModelViewSet):
|
||||||
queryset = ServiceTemplate.objects.prefetch_related('tags')
|
queryset = ServiceTemplate.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ServiceTemplateSerializer
|
serializer_class = serializers.ServiceTemplateSerializer
|
||||||
filterset_class = filtersets.ServiceTemplateFilterSet
|
filterset_class = filtersets.ServiceTemplateFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ServiceViewSet(CustomFieldModelViewSet):
|
class ServiceViewSet(NetBoxModelViewSet):
|
||||||
queryset = Service.objects.prefetch_related(
|
queryset = Service.objects.prefetch_related(
|
||||||
'device', 'virtual_machine', 'tags', 'ipaddresses'
|
'device', 'virtual_machine', 'tags', 'ipaddresses'
|
||||||
)
|
)
|
||||||
|
@ -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()
|
|
27
netbox/netbox/api/serializers/__init__.py
Normal file
27
netbox/netbox/api/serializers/__init__.py
Normal 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()
|
43
netbox/netbox/api/serializers/base.py
Normal file
43
netbox/netbox/api/serializers/base.py
Normal 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
|
80
netbox/netbox/api/serializers/features.py
Normal file
80
netbox/netbox/api/serializers/features.py
Normal 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
|
62
netbox/netbox/api/serializers/nested.py
Normal file
62
netbox/netbox/api/serializers/nested.py
Normal 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']
|
@ -1,292 +1,17 @@
|
|||||||
import logging
|
|
||||||
import platform
|
import platform
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django import __version__ as DJANGO_VERSION
|
from django import __version__ as DJANGO_VERSION
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
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 django_rq.queues import get_connection
|
||||||
from rest_framework import status
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.viewsets import ModelViewSet as ModelViewSet_
|
|
||||||
from rq.worker import Worker
|
from rq.worker import Worker
|
||||||
|
|
||||||
from extras.models import ExportTemplate
|
|
||||||
from netbox.api import BulkOperationSerializer
|
|
||||||
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
|
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):
|
class APIRootView(APIView):
|
||||||
"""
|
"""
|
||||||
|
182
netbox/netbox/api/viewsets/__init__.py
Normal file
182
netbox/netbox/api/viewsets/__init__.py
Normal 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)
|
113
netbox/netbox/api/viewsets/mixins.py
Normal file
113
netbox/netbox/api/viewsets/mixins.py
Normal 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)
|
@ -3,7 +3,7 @@ from drf_yasg.utils import swagger_serializer_method
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from netbox.api import ChoiceField, ContentTypeField
|
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.choices import ContactPriorityChoices
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.api import get_serializer_for_model
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
|
||||||
group = NestedTenantGroupSerializer(required=False, allow_null=True)
|
group = NestedTenantGroupSerializer(required=False, allow_null=True)
|
||||||
circuit_count = serializers.IntegerField(read_only=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')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -77,7 +77,7 @@ class ContactRoleSerializer(PrimaryModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ContactSerializer(PrimaryModelSerializer):
|
class ContactSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
|
||||||
group = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
|
||||||
content_type = ContentTypeField(
|
content_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.all()
|
queryset=ContentType.objects.all()
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from dcim.models import Device, Rack, Site, Cable
|
from dcim.models import Device, Rack, Site
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
|
||||||
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||||
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from tenancy import filtersets
|
from tenancy import filtersets
|
||||||
from tenancy.models import *
|
from tenancy.models import *
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -23,7 +23,7 @@ class TenancyRootView(APIRootView):
|
|||||||
# Tenants
|
# Tenants
|
||||||
#
|
#
|
||||||
|
|
||||||
class TenantGroupViewSet(CustomFieldModelViewSet):
|
class TenantGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = TenantGroup.objects.add_related_count(
|
queryset = TenantGroup.objects.add_related_count(
|
||||||
TenantGroup.objects.all(),
|
TenantGroup.objects.all(),
|
||||||
Tenant,
|
Tenant,
|
||||||
@ -35,7 +35,7 @@ class TenantGroupViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.TenantGroupFilterSet
|
filterset_class = filtersets.TenantGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class TenantViewSet(CustomFieldModelViewSet):
|
class TenantViewSet(NetBoxModelViewSet):
|
||||||
queryset = Tenant.objects.prefetch_related(
|
queryset = Tenant.objects.prefetch_related(
|
||||||
'group', 'tags'
|
'group', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -58,7 +58,7 @@ class TenantViewSet(CustomFieldModelViewSet):
|
|||||||
# Contacts
|
# Contacts
|
||||||
#
|
#
|
||||||
|
|
||||||
class ContactGroupViewSet(CustomFieldModelViewSet):
|
class ContactGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = ContactGroup.objects.add_related_count(
|
queryset = ContactGroup.objects.add_related_count(
|
||||||
ContactGroup.objects.all(),
|
ContactGroup.objects.all(),
|
||||||
Contact,
|
Contact,
|
||||||
@ -70,19 +70,19 @@ class ContactGroupViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.ContactGroupFilterSet
|
filterset_class = filtersets.ContactGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactRoleViewSet(CustomFieldModelViewSet):
|
class ContactRoleViewSet(NetBoxModelViewSet):
|
||||||
queryset = ContactRole.objects.prefetch_related('tags')
|
queryset = ContactRole.objects.prefetch_related('tags')
|
||||||
serializer_class = serializers.ContactRoleSerializer
|
serializer_class = serializers.ContactRoleSerializer
|
||||||
filterset_class = filtersets.ContactRoleFilterSet
|
filterset_class = filtersets.ContactRoleFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactViewSet(CustomFieldModelViewSet):
|
class ContactViewSet(NetBoxModelViewSet):
|
||||||
queryset = Contact.objects.prefetch_related('group', 'tags')
|
queryset = Contact.objects.prefetch_related('group', 'tags')
|
||||||
serializer_class = serializers.ContactSerializer
|
serializer_class = serializers.ContactSerializer
|
||||||
filterset_class = filtersets.ContactFilterSet
|
filterset_class = filtersets.ContactFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ContactAssignmentViewSet(CustomFieldModelViewSet):
|
class ContactAssignmentViewSet(NetBoxModelViewSet):
|
||||||
queryset = ContactAssignment.objects.prefetch_related('object', 'contact', 'role')
|
queryset = ContactAssignment.objects.prefetch_related('object', 'contact', 'role')
|
||||||
serializer_class = serializers.ContactAssignmentSerializer
|
serializer_class = serializers.ContactAssignmentSerializer
|
||||||
filterset_class = filtersets.ContactAssignmentFilterSet
|
filterset_class = filtersets.ContactAssignmentFilterSet
|
||||||
|
@ -9,7 +9,7 @@ from rest_framework.status import HTTP_201_CREATED
|
|||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
from rest_framework.viewsets import ViewSet
|
from rest_framework.viewsets import ViewSet
|
||||||
|
|
||||||
from netbox.api.views import ModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from users import filtersets
|
from users import filtersets
|
||||||
from users.models import ObjectPermission, Token, UserConfig
|
from users.models import ObjectPermission, Token, UserConfig
|
||||||
from utilities.querysets import RestrictedQuerySet
|
from utilities.querysets import RestrictedQuerySet
|
||||||
@ -29,13 +29,13 @@ class UsersRootView(APIRootView):
|
|||||||
# Users and groups
|
# Users and groups
|
||||||
#
|
#
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
|
queryset = RestrictedQuerySet(model=User).prefetch_related('groups').order_by('username')
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
filterset_class = filtersets.UserFilterSet
|
filterset_class = filtersets.UserFilterSet
|
||||||
|
|
||||||
|
|
||||||
class GroupViewSet(ModelViewSet):
|
class GroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
|
queryset = RestrictedQuerySet(model=Group).annotate(user_count=Count('user')).order_by('name')
|
||||||
serializer_class = serializers.GroupSerializer
|
serializer_class = serializers.GroupSerializer
|
||||||
filterset_class = filtersets.GroupFilterSet
|
filterset_class = filtersets.GroupFilterSet
|
||||||
@ -45,7 +45,7 @@ class GroupViewSet(ModelViewSet):
|
|||||||
# REST API tokens
|
# REST API tokens
|
||||||
#
|
#
|
||||||
|
|
||||||
class TokenViewSet(ModelViewSet):
|
class TokenViewSet(NetBoxModelViewSet):
|
||||||
queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
|
queryset = RestrictedQuerySet(model=Token).prefetch_related('user')
|
||||||
serializer_class = serializers.TokenSerializer
|
serializer_class = serializers.TokenSerializer
|
||||||
filterset_class = filtersets.TokenFilterSet
|
filterset_class = filtersets.TokenFilterSet
|
||||||
@ -94,7 +94,7 @@ class TokenProvisionView(APIView):
|
|||||||
# ObjectPermissions
|
# ObjectPermissions
|
||||||
#
|
#
|
||||||
|
|
||||||
class ObjectPermissionViewSet(ModelViewSet):
|
class ObjectPermissionViewSet(NetBoxModelViewSet):
|
||||||
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
queryset = ObjectPermission.objects.prefetch_related('object_types', 'groups', 'users')
|
||||||
serializer_class = serializers.ObjectPermissionSerializer
|
serializer_class = serializers.ObjectPermissionSerializer
|
||||||
filterset_class = filtersets.ObjectPermissionFilterSet
|
filterset_class = filtersets.ObjectPermissionFilterSet
|
||||||
|
@ -6,7 +6,7 @@ from dcim.choices import InterfaceModeChoices
|
|||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, SerializedPKRelatedField
|
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 tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -17,7 +17,7 @@ from .nested_serializers import *
|
|||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
|
||||||
class ClusterTypeSerializer(PrimaryModelSerializer):
|
class ClusterTypeSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||||
cluster_count = serializers.IntegerField(read_only=True)
|
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')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||||
type = NestedClusterTypeSerializer()
|
type = NestedClusterTypeSerializer()
|
||||||
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
|
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
@ -62,7 +62,7 @@ class ClusterSerializer(PrimaryModelSerializer):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineSerializer(PrimaryModelSerializer):
|
class VirtualMachineSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
|
||||||
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
|
||||||
site = NestedSiteSerializer(read_only=True)
|
site = NestedSiteSerializer(read_only=True)
|
||||||
@ -103,7 +103,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
|||||||
# VM interfaces
|
# VM interfaces
|
||||||
#
|
#
|
||||||
|
|
||||||
class VMInterfaceSerializer(PrimaryModelSerializer):
|
class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
|
||||||
virtual_machine = NestedVirtualMachineSerializer()
|
virtual_machine = NestedVirtualMachineSerializer()
|
||||||
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from dcim.models import Device
|
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 utilities.utils import count_related
|
||||||
from virtualization import filtersets
|
from virtualization import filtersets
|
||||||
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
|
||||||
@ -20,7 +21,7 @@ class VirtualizationRootView(APIRootView):
|
|||||||
# Clusters
|
# Clusters
|
||||||
#
|
#
|
||||||
|
|
||||||
class ClusterTypeViewSet(CustomFieldModelViewSet):
|
class ClusterTypeViewSet(NetBoxModelViewSet):
|
||||||
queryset = ClusterType.objects.annotate(
|
queryset = ClusterType.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'type')
|
cluster_count=count_related(Cluster, 'type')
|
||||||
).prefetch_related('tags')
|
).prefetch_related('tags')
|
||||||
@ -28,7 +29,7 @@ class ClusterTypeViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.ClusterTypeFilterSet
|
filterset_class = filtersets.ClusterTypeFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ClusterGroupViewSet(CustomFieldModelViewSet):
|
class ClusterGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = ClusterGroup.objects.annotate(
|
queryset = ClusterGroup.objects.annotate(
|
||||||
cluster_count=count_related(Cluster, 'group')
|
cluster_count=count_related(Cluster, 'group')
|
||||||
).prefetch_related('tags')
|
).prefetch_related('tags')
|
||||||
@ -36,7 +37,7 @@ class ClusterGroupViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.ClusterGroupFilterSet
|
filterset_class = filtersets.ClusterGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class ClusterViewSet(CustomFieldModelViewSet):
|
class ClusterViewSet(NetBoxModelViewSet):
|
||||||
queryset = Cluster.objects.prefetch_related(
|
queryset = Cluster.objects.prefetch_related(
|
||||||
'type', 'group', 'tenant', 'site', 'tags'
|
'type', 'group', 'tenant', 'site', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
@ -51,7 +52,7 @@ class ClusterViewSet(CustomFieldModelViewSet):
|
|||||||
# Virtual machines
|
# Virtual machines
|
||||||
#
|
#
|
||||||
|
|
||||||
class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet):
|
class VirtualMachineViewSet(ConfigContextQuerySetMixin, NetBoxModelViewSet):
|
||||||
queryset = VirtualMachine.objects.prefetch_related(
|
queryset = VirtualMachine.objects.prefetch_related(
|
||||||
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'tags'
|
||||||
)
|
)
|
||||||
@ -78,7 +79,7 @@ class VirtualMachineViewSet(ConfigContextQuerySetMixin, CustomFieldModelViewSet)
|
|||||||
return serializers.VirtualMachineWithConfigContextSerializer
|
return serializers.VirtualMachineWithConfigContextSerializer
|
||||||
|
|
||||||
|
|
||||||
class VMInterfaceViewSet(ModelViewSet):
|
class VMInterfaceViewSet(NetBoxModelViewSet):
|
||||||
queryset = VMInterface.objects.prefetch_related(
|
queryset = VMInterface.objects.prefetch_related(
|
||||||
'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
|
'virtual_machine', 'parent', 'tags', 'untagged_vlan', 'tagged_vlans', 'vrf', 'ip_addresses',
|
||||||
'fhrp_group_assignments',
|
'fhrp_group_assignments',
|
||||||
|
@ -4,7 +4,7 @@ from dcim.choices import LinkStatusChoices
|
|||||||
from dcim.api.serializers import NestedInterfaceSerializer
|
from dcim.api.serializers import NestedInterfaceSerializer
|
||||||
from ipam.api.serializers import NestedVLANSerializer
|
from ipam.api.serializers import NestedVLANSerializer
|
||||||
from netbox.api import ChoiceField
|
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.choices import *
|
||||||
from wireless.models import *
|
from wireless.models import *
|
||||||
from .nested_serializers 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')
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
|
||||||
group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
|
group = NestedWirelessLANGroupSerializer(required=False, allow_null=True)
|
||||||
vlan = NestedVLANSerializer(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')
|
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
|
||||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
interface_a = NestedInterfaceSerializer()
|
interface_a = NestedInterfaceSerializer()
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from netbox.api.viewsets import NetBoxModelViewSet
|
||||||
from wireless import filtersets
|
from wireless import filtersets
|
||||||
from wireless.models import *
|
from wireless.models import *
|
||||||
from . import serializers
|
from . import serializers
|
||||||
@ -14,7 +14,7 @@ class WirelessRootView(APIRootView):
|
|||||||
return 'Wireless'
|
return 'Wireless'
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANGroupViewSet(CustomFieldModelViewSet):
|
class WirelessLANGroupViewSet(NetBoxModelViewSet):
|
||||||
queryset = WirelessLANGroup.objects.add_related_count(
|
queryset = WirelessLANGroup.objects.add_related_count(
|
||||||
WirelessLANGroup.objects.all(),
|
WirelessLANGroup.objects.all(),
|
||||||
WirelessLAN,
|
WirelessLAN,
|
||||||
@ -26,13 +26,13 @@ class WirelessLANGroupViewSet(CustomFieldModelViewSet):
|
|||||||
filterset_class = filtersets.WirelessLANGroupFilterSet
|
filterset_class = filtersets.WirelessLANGroupFilterSet
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANViewSet(CustomFieldModelViewSet):
|
class WirelessLANViewSet(NetBoxModelViewSet):
|
||||||
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
|
queryset = WirelessLAN.objects.prefetch_related('vlan', 'tags')
|
||||||
serializer_class = serializers.WirelessLANSerializer
|
serializer_class = serializers.WirelessLANSerializer
|
||||||
filterset_class = filtersets.WirelessLANFilterSet
|
filterset_class = filtersets.WirelessLANFilterSet
|
||||||
|
|
||||||
|
|
||||||
class WirelessLinkViewSet(CustomFieldModelViewSet):
|
class WirelessLinkViewSet(NetBoxModelViewSet):
|
||||||
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
|
queryset = WirelessLink.objects.prefetch_related('interface_a', 'interface_b', 'tags')
|
||||||
serializer_class = serializers.WirelessLinkSerializer
|
serializer_class = serializers.WirelessLinkSerializer
|
||||||
filterset_class = filtersets.WirelessLinkFilterSet
|
filterset_class = filtersets.WirelessLinkFilterSet
|
||||||
|
Loading…
Reference in New Issue
Block a user