mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Closes #9608: Move from drf-yasg to spectacular
Co-authored-by: arthanson <worldnomad@gmail.com> Co-authored-by: jeremystretch <jstretch@netboxlabs.com>
This commit is contained in:
@ -66,9 +66,9 @@ django-timezone-field
|
||||
# https://github.com/encode/django-rest-framework
|
||||
djangorestframework
|
||||
|
||||
# Swagger/OpenAPI schema generation for REST APIs
|
||||
# https://github.com/axnsan12/drf-yasg
|
||||
drf-yasg[validation]
|
||||
# Sane and flexible OpenAPI 3 schema generation for Django REST framework.
|
||||
# https://github.com/tfranzel/drf-spectacular
|
||||
drf-spectacular
|
||||
|
||||
# RSS feed parser
|
||||
# https://github.com/kurtmckee/feedparser
|
||||
|
@ -1,3 +1,5 @@
|
||||
from drf_spectacular.utils import extend_schema_field, extend_schema_serializer
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
|
||||
from circuits.models import *
|
||||
@ -29,6 +31,9 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
|
||||
# Providers
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedProviderSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
@ -54,6 +59,9 @@ class NestedProviderAccountSerializer(WritableNestedSerializer):
|
||||
# Circuits
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('circuit_count',),
|
||||
)
|
||||
class NestedCircuitTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail')
|
||||
circuit_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -92,8 +92,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer):
|
||||
|
||||
class CircuitCircuitTerminationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail')
|
||||
site = NestedSiteSerializer()
|
||||
provider_network = NestedProviderNetworkSerializer()
|
||||
site = NestedSiteSerializer(allow_null=True)
|
||||
provider_network = NestedProviderNetworkSerializer(allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = CircuitTermination
|
||||
@ -110,8 +110,8 @@ class CircuitSerializer(NetBoxModelSerializer):
|
||||
status = ChoiceField(choices=CircuitStatusChoices, required=False)
|
||||
type = NestedCircuitTypeSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True)
|
||||
termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Circuit
|
||||
|
224
netbox/core/api/schema.py
Normal file
224
netbox/core/api/schema.py
Normal file
@ -0,0 +1,224 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from drf_spectacular.extensions import (
|
||||
OpenApiSerializerFieldExtension,
|
||||
OpenApiViewExtension,
|
||||
)
|
||||
from drf_spectacular.openapi import AutoSchema
|
||||
from drf_spectacular.plumbing import (
|
||||
ComponentRegistry,
|
||||
ResolvedComponent,
|
||||
build_basic_type,
|
||||
build_media_type_object,
|
||||
build_object_type,
|
||||
is_serializer,
|
||||
)
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
# see netbox.api.routers.NetBoxRouter
|
||||
BULK_ACTIONS = ("bulk_destroy", "bulk_partial_update", "bulk_update")
|
||||
WRITABLE_ACTIONS = ("PATCH", "POST", "PUT")
|
||||
|
||||
|
||||
class FixTimeZoneSerializerField(OpenApiSerializerFieldExtension):
|
||||
target_class = 'timezone_field.rest_framework.TimeZoneSerializerField'
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
return build_basic_type(OpenApiTypes.STR)
|
||||
|
||||
|
||||
class ChoiceFieldFix(OpenApiSerializerFieldExtension):
|
||||
target_class = 'netbox.api.fields.ChoiceField'
|
||||
|
||||
def map_serializer_field(self, auto_schema, direction):
|
||||
if direction == 'request':
|
||||
return build_basic_type(OpenApiTypes.STR)
|
||||
|
||||
elif direction == "response":
|
||||
return build_object_type(
|
||||
properties={
|
||||
"value": build_basic_type(OpenApiTypes.STR),
|
||||
"label": build_basic_type(OpenApiTypes.STR),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class NetBoxAutoSchema(AutoSchema):
|
||||
"""
|
||||
Overrides to drf_spectacular.openapi.AutoSchema to fix following issues:
|
||||
1. bulk serializers cause operation_id conflicts with non-bulk ones
|
||||
2. bulk operations should specify a list
|
||||
3. bulk operations don't have filter params
|
||||
4. bulk operations don't have pagination
|
||||
5. bulk delete should specify input
|
||||
"""
|
||||
|
||||
writable_serializers = {}
|
||||
|
||||
@property
|
||||
def is_bulk_action(self):
|
||||
if hasattr(self.view, "action") and self.view.action in BULK_ACTIONS:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_operation_id(self):
|
||||
"""
|
||||
bulk serializers cause operation_id conflicts with non-bulk ones
|
||||
bulk operations cause id conflicts in spectacular resulting in numerous:
|
||||
Warning: operationId "xxx" has collisions [xxx]. "resolving with numeral suffixes"
|
||||
code is modified from drf_spectacular.openapi.AutoSchema.get_operation_id
|
||||
"""
|
||||
if self.is_bulk_action:
|
||||
tokenized_path = self._tokenize_path()
|
||||
# replace dashes as they can be problematic later in code generation
|
||||
tokenized_path = [t.replace('-', '_') for t in tokenized_path]
|
||||
|
||||
if self.method == 'GET' and self._is_list_view():
|
||||
# this shouldn't happen, but keeping it here to follow base code
|
||||
action = 'list'
|
||||
else:
|
||||
# action = self.method_mapping[self.method.lower()]
|
||||
# use bulk name so partial_update -> bulk_partial_update
|
||||
action = self.view.action.lower()
|
||||
|
||||
if not tokenized_path:
|
||||
tokenized_path.append('root')
|
||||
|
||||
if re.search(r'<drf_format_suffix\w*:\w+>', self.path_regex):
|
||||
tokenized_path.append('formatted')
|
||||
|
||||
return '_'.join(tokenized_path + [action])
|
||||
|
||||
# if not bulk - just return normal id
|
||||
return super().get_operation_id()
|
||||
|
||||
def get_request_serializer(self) -> typing.Any:
|
||||
# bulk operations should specify a list
|
||||
serializer = super().get_request_serializer()
|
||||
|
||||
if self.is_bulk_action:
|
||||
return type(serializer)(many=True)
|
||||
|
||||
# handle mapping for Writable serializers - adapted from dansheps original code
|
||||
# for drf-yasg
|
||||
if serializer is not None and self.method in WRITABLE_ACTIONS:
|
||||
writable_class = self.get_writable_class(serializer)
|
||||
if writable_class is not None:
|
||||
if hasattr(serializer, "child"):
|
||||
child_serializer = self.get_writable_class(serializer.child)
|
||||
serializer = writable_class(context=serializer.context, child=child_serializer)
|
||||
else:
|
||||
serializer = writable_class(context=serializer.context)
|
||||
|
||||
return serializer
|
||||
|
||||
def get_response_serializers(self) -> typing.Any:
|
||||
# bulk operations should specify a list
|
||||
response_serializers = super().get_response_serializers()
|
||||
|
||||
if self.is_bulk_action:
|
||||
return type(response_serializers)(many=True)
|
||||
|
||||
return response_serializers
|
||||
|
||||
def get_serializer_ref_name(self, serializer):
|
||||
# from drf-yasg.utils
|
||||
"""Get serializer's ref_name (or None for ModelSerializer if it is named 'NestedSerializer')
|
||||
:param serializer: Serializer instance
|
||||
:return: Serializer's ``ref_name`` or ``None`` for inline serializer
|
||||
:rtype: str or None
|
||||
"""
|
||||
serializer_meta = getattr(serializer, 'Meta', None)
|
||||
serializer_name = type(serializer).__name__
|
||||
if hasattr(serializer_meta, 'ref_name'):
|
||||
ref_name = serializer_meta.ref_name
|
||||
elif serializer_name == 'NestedSerializer' and isinstance(serializer, serializers.ModelSerializer):
|
||||
ref_name = None
|
||||
else:
|
||||
ref_name = serializer_name
|
||||
if ref_name.endswith('Serializer'):
|
||||
ref_name = ref_name[: -len('Serializer')]
|
||||
return ref_name
|
||||
|
||||
def get_writable_class(self, serializer):
|
||||
properties = {}
|
||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||
|
||||
for child_name, child in fields.items():
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
properties[child_name] = None
|
||||
|
||||
if not properties:
|
||||
return None
|
||||
|
||||
if type(serializer) not in self.writable_serializers:
|
||||
writable_name = 'Writable' + type(serializer).__name__
|
||||
meta_class = getattr(type(serializer), 'Meta', None)
|
||||
if meta_class:
|
||||
ref_name = 'Writable' + self.get_serializer_ref_name(serializer)
|
||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
|
||||
properties['Meta'] = writable_meta
|
||||
|
||||
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
||||
|
||||
writable_class = self.writable_serializers[type(serializer)]
|
||||
return writable_class
|
||||
|
||||
def get_filter_backends(self):
|
||||
# bulk operations don't have filter params
|
||||
if self.is_bulk_action:
|
||||
return []
|
||||
return super().get_filter_backends()
|
||||
|
||||
def _get_paginator(self):
|
||||
# bulk operations don't have pagination
|
||||
if self.is_bulk_action:
|
||||
return None
|
||||
return super()._get_paginator()
|
||||
|
||||
def _get_request_body(self, direction='request'):
|
||||
# bulk delete should specify input
|
||||
if (not self.is_bulk_action) or (self.method != 'DELETE'):
|
||||
return super()._get_request_body(direction)
|
||||
|
||||
# rest from drf_spectacular.openapi.AutoSchema._get_request_body
|
||||
# but remove the unsafe method check
|
||||
|
||||
request_serializer = self.get_request_serializer()
|
||||
|
||||
if isinstance(request_serializer, dict):
|
||||
content = []
|
||||
request_body_required = True
|
||||
for media_type, serializer in request_serializer.items():
|
||||
schema, partial_request_body_required = self._get_request_for_media_type(serializer, direction)
|
||||
examples = self._get_examples(serializer, direction, media_type)
|
||||
if schema is None:
|
||||
continue
|
||||
content.append((media_type, schema, examples))
|
||||
request_body_required &= partial_request_body_required
|
||||
else:
|
||||
schema, request_body_required = self._get_request_for_media_type(request_serializer, direction)
|
||||
if schema is None:
|
||||
return None
|
||||
content = [
|
||||
(media_type, schema, self._get_examples(request_serializer, direction, media_type))
|
||||
for media_type in self.map_parsers()
|
||||
]
|
||||
|
||||
request_body = {
|
||||
'content': {
|
||||
media_type: build_media_type_object(schema, examples) for media_type, schema, examples in content
|
||||
}
|
||||
}
|
||||
if request_body_required:
|
||||
request_body['required'] = request_body_required
|
||||
return request_body
|
@ -6,3 +6,4 @@ class CoreConfig(AppConfig):
|
||||
|
||||
def ready(self):
|
||||
from . import data_backends, search
|
||||
from core.api import schema # noqa: E402
|
||||
|
@ -1,3 +1,4 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim import models
|
||||
@ -53,6 +54,9 @@ __all__ = [
|
||||
# Regions/sites
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedRegionSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
@ -63,6 +67,9 @@ class NestedRegionSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'site_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('site_count',),
|
||||
)
|
||||
class NestedSiteGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
|
||||
site_count = serializers.IntegerField(read_only=True)
|
||||
@ -85,6 +92,9 @@ class NestedSiteSerializer(WritableNestedSerializer):
|
||||
# Racks
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedLocationSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
@ -95,6 +105,9 @@ class NestedLocationSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('rack_count',),
|
||||
)
|
||||
class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
|
||||
rack_count = serializers.IntegerField(read_only=True)
|
||||
@ -104,6 +117,9 @@ class NestedRackRoleSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'rack_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedRackSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@ -129,6 +145,9 @@ class NestedRackReservationSerializer(WritableNestedSerializer):
|
||||
# Device/module types
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('devicetype_count',),
|
||||
)
|
||||
class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
|
||||
devicetype_count = serializers.IntegerField(read_only=True)
|
||||
@ -138,6 +157,9 @@ class NestedManufacturerSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'devicetype_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count',),
|
||||
)
|
||||
class NestedDeviceTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
|
||||
manufacturer = NestedManufacturerSerializer(read_only=True)
|
||||
@ -247,6 +269,9 @@ class NestedInventoryItemTemplateSerializer(WritableNestedSerializer):
|
||||
# Devices
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@ -257,6 +282,9 @@ class NestedDeviceRoleSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'device_count', 'virtualmachine_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('device_count', 'virtualmachine_count'),
|
||||
)
|
||||
class NestedPlatformSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
@ -386,7 +414,7 @@ class NestedFrontPortSerializer(WritableNestedSerializer):
|
||||
|
||||
class NestedModuleBaySerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
|
||||
module = NestedModuleSerializer(read_only=True)
|
||||
module = NestedModuleSerializer(required=False, read_only=True, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = models.ModuleBay
|
||||
@ -412,6 +440,9 @@ class NestedInventoryItemSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'device', 'name', '_depth']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('inventoryitem_count',),
|
||||
)
|
||||
class NestedInventoryItemRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
|
||||
inventoryitem_count = serializers.IntegerField(read_only=True)
|
||||
@ -437,6 +468,9 @@ class NestedCableSerializer(BaseModelSerializer):
|
||||
# Virtual chassis
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('member_count',),
|
||||
)
|
||||
class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
|
||||
master = NestedDeviceSerializer()
|
||||
@ -451,6 +485,9 @@ class NestedVirtualChassisSerializer(WritableNestedSerializer):
|
||||
# Power panels/feeds
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('powerfeed_count',),
|
||||
)
|
||||
class NestedPowerPanelSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -2,7 +2,8 @@ import decimal
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import gettext as _
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
from timezone_field.rest_framework import TimeZoneSerializerField
|
||||
|
||||
@ -33,12 +34,13 @@ from .nested_serializers import *
|
||||
|
||||
|
||||
class CabledObjectSerializer(serializers.ModelSerializer):
|
||||
cable = NestedCableSerializer(read_only=True)
|
||||
cable = NestedCableSerializer(read_only=True, allow_null=True)
|
||||
cable_end = serializers.CharField(read_only=True)
|
||||
link_peers_type = serializers.SerializerMethodField(read_only=True)
|
||||
link_peers = serializers.SerializerMethodField(read_only=True)
|
||||
_occupied = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_link_peers_type(self, obj):
|
||||
"""
|
||||
Return the type of the peer link terminations, or None.
|
||||
@ -51,7 +53,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
||||
|
||||
return None
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField)
|
||||
def get_link_peers(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the link termination model.
|
||||
@ -64,7 +66,7 @@ class CabledObjectSerializer(serializers.ModelSerializer):
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(obj.link_peers, context=context, many=True).data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
def get__occupied(self, obj):
|
||||
return obj._occupied
|
||||
|
||||
@ -77,11 +79,12 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
connected_endpoints = serializers.SerializerMethodField(read_only=True)
|
||||
connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_connected_endpoints_type(self, obj):
|
||||
if endpoints := obj.connected_endpoints:
|
||||
return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField)
|
||||
def get_connected_endpoints(self, obj):
|
||||
"""
|
||||
Return the appropriate serializer for the type of connected object.
|
||||
@ -91,7 +94,7 @@ class ConnectedEndpointsSerializer(serializers.ModelSerializer):
|
||||
context = {'request': self.context['request']}
|
||||
return serializer(endpoints, many=True, context=context).data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.BooleanField)
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
def get_connected_endpoints_reachable(self, obj):
|
||||
return obj._path and obj._path.is_complete and obj._path.is_active
|
||||
|
||||
@ -198,12 +201,12 @@ class RackSerializer(NetBoxModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=RackStatusChoices, required=False)
|
||||
role = NestedRackRoleSerializer(required=False, allow_null=True)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False)
|
||||
type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
|
||||
facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
|
||||
default=None)
|
||||
width = ChoiceField(choices=RackWidthChoices, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||
outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
powerfeed_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
@ -232,6 +235,7 @@ class RackUnitSerializer(serializers.Serializer):
|
||||
occupied = serializers.BooleanField(read_only=True)
|
||||
display = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_display(self, obj):
|
||||
return obj['name']
|
||||
|
||||
@ -318,9 +322,9 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
min_value=0,
|
||||
default=1.0
|
||||
)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||
subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||
airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
device_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -335,7 +339,7 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
|
||||
class ModuleTypeSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
|
||||
manufacturer = NestedManufacturerSerializer()
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False)
|
||||
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = ModuleType
|
||||
@ -416,7 +420,8 @@ class PowerPortTemplateSerializer(ValidatedModelSerializer):
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -442,7 +447,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
power_port = NestedPowerPortTemplateSerializer(
|
||||
required=False,
|
||||
@ -451,7 +457,8 @@ class PowerOutletTemplateSerializer(ValidatedModelSerializer):
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -482,12 +489,14 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
|
||||
poe_mode = ChoiceField(
|
||||
choices=InterfacePoEModeChoices,
|
||||
required=False,
|
||||
allow_blank=True
|
||||
allow_blank=True,
|
||||
allow_null=True
|
||||
)
|
||||
poe_type = ChoiceField(
|
||||
choices=InterfacePoETypeChoices,
|
||||
required=False,
|
||||
allow_blank=True
|
||||
allow_blank=True,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -589,7 +598,7 @@ class InventoryItemTemplateSerializer(ValidatedModelSerializer):
|
||||
'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
@ -640,7 +649,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
site = NestedSiteSerializer()
|
||||
location = NestedLocationSerializer(required=False, allow_null=True, default=None)
|
||||
rack = NestedRackSerializer(required=False, allow_null=True, default=None)
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default='')
|
||||
face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
|
||||
position = serializers.DecimalField(
|
||||
max_digits=4,
|
||||
decimal_places=1,
|
||||
@ -669,7 +678,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)
|
||||
@extend_schema_field(NestedDeviceSerializer)
|
||||
def get_parent_device(self, obj):
|
||||
try:
|
||||
device_bay = obj.parent_bay
|
||||
@ -682,7 +691,7 @@ class DeviceSerializer(NetBoxModelSerializer):
|
||||
|
||||
|
||||
class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
config_context = serializers.SerializerMethodField()
|
||||
config_context = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
class Meta(DeviceSerializer.Meta):
|
||||
fields = [
|
||||
@ -692,7 +701,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
|
||||
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
@ -701,7 +710,7 @@ class VirtualDeviceContextSerializer(NetBoxModelSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
|
||||
device = NestedDeviceSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True, default=None)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True)
|
||||
primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
|
||||
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
|
||||
|
||||
@ -806,7 +815,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
type = ChoiceField(
|
||||
choices=PowerOutletTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
power_port = NestedPowerPortSerializer(
|
||||
required=False,
|
||||
@ -815,7 +825,8 @@ class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, Conne
|
||||
feed_leg = ChoiceField(
|
||||
choices=PowerOutletFeedLegChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -838,7 +849,8 @@ class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
type = ChoiceField(
|
||||
choices=PowerPortTypeChoices,
|
||||
allow_blank=True,
|
||||
required=False
|
||||
required=False,
|
||||
allow_null=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -868,12 +880,12 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
parent = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
lag = NestedInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
|
||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
|
||||
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
|
||||
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True, allow_null=True)
|
||||
duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
|
||||
rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True, allow_null=True)
|
||||
rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True, allow_null=True)
|
||||
poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True, allow_null=True)
|
||||
poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@ -882,8 +894,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
many=True
|
||||
)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
||||
wireless_link = NestedWirelessLinkSerializer(read_only=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||
wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
|
||||
wireless_lans = SerializedPKRelatedField(
|
||||
queryset=WirelessLAN.objects.all(),
|
||||
serializer=NestedWirelessLANSerializer,
|
||||
@ -892,6 +904,8 @@ class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||
mac_address = serializers.CharField(required=False, default=None)
|
||||
wwn = serializers.CharField(required=False, default=None)
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
@ -1015,7 +1029,7 @@ class InventoryItemSerializer(NetBoxModelSerializer):
|
||||
'custom_fields', 'created', 'last_updated', '_depth',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_component(self, obj):
|
||||
if obj.component is None:
|
||||
return None
|
||||
@ -1050,7 +1064,7 @@ class CableSerializer(NetBoxModelSerializer):
|
||||
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
model = Cable
|
||||
@ -1086,7 +1100,7 @@ class CableTerminationSerializer(NetBoxModelSerializer):
|
||||
'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination'
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_termination(self, obj):
|
||||
serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
@ -1100,7 +1114,7 @@ class CablePathSerializer(serializers.ModelSerializer):
|
||||
model = CablePath
|
||||
fields = ['id', 'path', 'is_active', 'is_complete', 'is_split']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField)
|
||||
def get_path(self, obj):
|
||||
ret = []
|
||||
for nodes in obj.path_objects:
|
||||
@ -1159,19 +1173,19 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
|
||||
)
|
||||
type = ChoiceField(
|
||||
choices=PowerFeedTypeChoices,
|
||||
default=PowerFeedTypeChoices.TYPE_PRIMARY
|
||||
default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY,
|
||||
)
|
||||
status = ChoiceField(
|
||||
choices=PowerFeedStatusChoices,
|
||||
default=PowerFeedStatusChoices.STATUS_ACTIVE
|
||||
default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE,
|
||||
)
|
||||
supply = ChoiceField(
|
||||
choices=PowerFeedSupplyChoices,
|
||||
default=PowerFeedSupplyChoices.SUPPLY_AC
|
||||
default=lambda: PowerFeedSupplyChoices.SUPPLY_AC,
|
||||
)
|
||||
phase = ChoiceField(
|
||||
choices=PowerFeedPhaseChoices,
|
||||
default=PowerFeedPhaseChoices.PHASE_SINGLE
|
||||
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -1,8 +1,7 @@
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.openapi import Parameter
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.response import Response
|
||||
@ -194,10 +193,6 @@ class RackViewSet(NetBoxModelViewSet):
|
||||
serializer_class = serializers.RackSerializer
|
||||
filterset_class = filtersets.RackFilterSet
|
||||
|
||||
@swagger_auto_schema(
|
||||
responses={200: serializers.RackUnitSerializer(many=True)},
|
||||
query_serializer=serializers.RackElevationDetailFilterSerializer
|
||||
)
|
||||
@action(detail=True)
|
||||
def elevation(self, request, pk=None):
|
||||
"""
|
||||
@ -622,28 +617,26 @@ class ConnectedDeviceViewSet(ViewSet):
|
||||
* `peer_interface`: The name of the peer interface
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_device_param = Parameter(
|
||||
_device_param = OpenApiParameter(
|
||||
name='peer_device',
|
||||
in_='query',
|
||||
location='query',
|
||||
description='The name of the peer device',
|
||||
required=True,
|
||||
type=openapi.TYPE_STRING
|
||||
type=OpenApiTypes.STR
|
||||
)
|
||||
_interface_param = Parameter(
|
||||
_interface_param = OpenApiParameter(
|
||||
name='peer_interface',
|
||||
in_='query',
|
||||
location='query',
|
||||
description='The name of the peer interface',
|
||||
required=True,
|
||||
type=openapi.TYPE_STRING
|
||||
type=OpenApiTypes.STR
|
||||
)
|
||||
serializer_class = serializers.DeviceSerializer
|
||||
|
||||
def get_view_name(self):
|
||||
return "Connected Device Locator"
|
||||
|
||||
@swagger_auto_schema(
|
||||
manual_parameters=[_device_param, _interface_param],
|
||||
responses={'200': serializers.DeviceSerializer}
|
||||
)
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def list(self, request):
|
||||
|
||||
peer_device_name = request.query_params.get(self._device_param.name)
|
||||
|
@ -1674,7 +1674,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase):
|
||||
|
||||
class ModuleBayTest(APIViewTestCases.APIViewTestCase):
|
||||
model = ModuleBay
|
||||
brief_fields = ['display', 'id', 'name', 'url']
|
||||
brief_fields = ['display', 'id', 'module', 'name', 'url']
|
||||
bulk_update_data = {
|
||||
'description': 'New description',
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.fields import Field
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
@ -36,6 +38,7 @@ class CustomFieldDefaultValues:
|
||||
return value
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||
class CustomFieldsDataField(Field):
|
||||
|
||||
def _get_custom_fields(self):
|
||||
|
@ -1,7 +1,6 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from rest_framework import serializers
|
||||
|
||||
from core.api.serializers import JobSerializer
|
||||
@ -11,6 +10,8 @@ from dcim.api.nested_serializers import (
|
||||
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
|
||||
)
|
||||
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from extras.choices import *
|
||||
from extras.models import *
|
||||
from extras.utils import FeatureQuery
|
||||
@ -103,6 +104,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_data_type(self, obj):
|
||||
types = CustomFieldTypeChoices
|
||||
if obj.type == types.TYPE_INTEGER:
|
||||
@ -230,7 +232,7 @@ class ImageAttachmentSerializer(ValidatedModelSerializer):
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_parent(self, obj):
|
||||
serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
return serializer(obj.parent, context={'request': self.context['request']}).data
|
||||
@ -280,7 +282,7 @@ class JournalEntrySerializer(NetBoxModelSerializer):
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
@ -453,7 +455,7 @@ class ScriptSerializer(serializers.Serializer):
|
||||
vars = serializers.SerializerMethodField(read_only=True)
|
||||
result = NestedJobSerializer()
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_vars(self, instance):
|
||||
return {
|
||||
k: v.__class__.__name__ for k, v in instance._get_vars().items()
|
||||
@ -514,7 +516,7 @@ class ObjectChangeSerializer(BaseModelSerializer):
|
||||
'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_changed_object(self, obj):
|
||||
"""
|
||||
Serialize a nested representation of the changed object.
|
||||
|
@ -168,7 +168,7 @@ class ConfigTemplateViewSet(SyncedDataMixin, ConfigTemplateRenderMixin, NetBoxMo
|
||||
class ReportViewSet(ViewSet):
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
schema = None
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def _get_report(self, pk):
|
||||
@ -270,7 +270,7 @@ class ReportViewSet(ViewSet):
|
||||
class ScriptViewSet(ViewSet):
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
schema = None
|
||||
lookup_value_regex = '[^/]+' # Allow dots
|
||||
|
||||
def _get_script(self, pk):
|
||||
|
@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from django.shortcuts import render
|
||||
from django.urls.exceptions import NoReverseMatch
|
||||
from django.views.generic import View
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
@ -22,14 +23,14 @@ class InstalledPluginsAdminView(View):
|
||||
})
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
class InstalledPluginsAPIView(APIView):
|
||||
"""
|
||||
API view for listing all installed plugins
|
||||
"""
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "Installed Plugins"
|
||||
@ -49,10 +50,10 @@ class InstalledPluginsAPIView(APIView):
|
||||
return Response([self._get_plugin_data(apps.get_app_config(plugin)) for plugin in settings.PLUGINS])
|
||||
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
class PluginsAPIRootView(APIView):
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "Plugins"
|
||||
|
@ -1,3 +1,4 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from ipam import models
|
||||
@ -54,6 +55,9 @@ class NestedASNSerializer(WritableNestedSerializer):
|
||||
# VRFs
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('prefix_count',),
|
||||
)
|
||||
class NestedVRFSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail')
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
@ -79,6 +83,9 @@ class NestedRouteTargetSerializer(WritableNestedSerializer):
|
||||
# RIRs/aggregates
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('aggregate_count',),
|
||||
)
|
||||
class NestedRIRSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
|
||||
aggregate_count = serializers.IntegerField(read_only=True)
|
||||
@ -121,6 +128,9 @@ class NestedFHRPGroupAssignmentSerializer(WritableNestedSerializer):
|
||||
# VLANs
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('prefix_count', 'vlan_count'),
|
||||
)
|
||||
class NestedRoleSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail')
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
@ -131,6 +141,9 @@ class NestedRoleSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'prefix_count', 'vlan_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('vlan_count',),
|
||||
)
|
||||
class NestedVLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail')
|
||||
vlan_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer
|
||||
@ -136,6 +136,7 @@ class AggregateSerializer(NetBoxModelSerializer):
|
||||
family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True)
|
||||
rir = NestedRIRSerializer()
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
prefix = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
model = Aggregate
|
||||
@ -177,7 +178,7 @@ class FHRPGroupAssignmentSerializer(NetBoxModelSerializer):
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_interface(self, obj):
|
||||
if obj.interface is None:
|
||||
return None
|
||||
@ -225,7 +226,7 @@ class VLANGroupSerializer(NetBoxModelSerializer):
|
||||
]
|
||||
validators = []
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_scope(self, obj):
|
||||
if obj.scope_id is None:
|
||||
return None
|
||||
@ -242,7 +243,7 @@ class VLANSerializer(NetBoxModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=VLANStatusChoices, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||
prefix_count = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
@ -302,6 +303,7 @@ class PrefixSerializer(NetBoxModelSerializer):
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
children = serializers.IntegerField(read_only=True)
|
||||
_depth = serializers.IntegerField(read_only=True)
|
||||
prefix = serializers.CharField()
|
||||
|
||||
class Meta:
|
||||
model = Prefix
|
||||
@ -371,13 +373,13 @@ class IPRangeSerializer(NetBoxModelSerializer):
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPRangeStatusChoices, required=False)
|
||||
role = NestedRoleSerializer(required=False, allow_null=True)
|
||||
children = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IPRange
|
||||
fields = [
|
||||
'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role',
|
||||
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children',
|
||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
read_only_fields = ['family']
|
||||
|
||||
@ -392,7 +394,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||
status = ChoiceField(choices=IPAddressStatusChoices, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False)
|
||||
role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False, allow_null=True)
|
||||
assigned_object_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS),
|
||||
required=False,
|
||||
@ -410,7 +412,7 @@ class IPAddressSerializer(NetBoxModelSerializer):
|
||||
'tags', 'custom_fields', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_assigned_object(self, obj):
|
||||
if obj.assigned_object is None:
|
||||
return None
|
||||
@ -519,7 +521,7 @@ class L2VPNTerminationSerializer(NetBoxModelSerializer):
|
||||
'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_assigned_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
|
@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_pglocks import advisory_lock
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.routers import APIRootView
|
||||
@ -210,7 +210,7 @@ def get_results_limit(request):
|
||||
class AvailableASNsView(ObjectValidationMixin, APIView):
|
||||
queryset = ASN.objects.all()
|
||||
|
||||
@swagger_auto_schema(responses={200: serializers.AvailableASNSerializer(many=True)})
|
||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableASNSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
asnrange = get_object_or_404(ASNRange.objects.restrict(request.user), pk=pk)
|
||||
limit = get_results_limit(request)
|
||||
@ -224,10 +224,7 @@ class AvailableASNsView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.AvailableASNSerializer,
|
||||
responses={201: serializers.ASNSerializer(many=True)}
|
||||
)
|
||||
@extend_schema(methods=["post"], responses={201: serializers.ASNSerializer(many=True)})
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-asns'])
|
||||
def post(self, request, pk):
|
||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||
@ -274,11 +271,17 @@ class AvailableASNsView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return serializers.AvailableASNSerializer
|
||||
|
||||
return serializers.ASNSerializer
|
||||
|
||||
|
||||
class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||
queryset = Prefix.objects.all()
|
||||
|
||||
@swagger_auto_schema(responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||
@extend_schema(methods=["get"], responses={200: serializers.AvailablePrefixSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
prefix = get_object_or_404(Prefix.objects.restrict(request.user), pk=pk)
|
||||
available_prefixes = prefix.get_available_prefixes()
|
||||
@ -290,10 +293,7 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.PrefixLengthSerializer,
|
||||
responses={201: serializers.PrefixSerializer(many=True)}
|
||||
)
|
||||
@extend_schema(methods=["post"], responses={201: serializers.PrefixSerializer(many=True)})
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-prefixes'])
|
||||
def post(self, request, pk):
|
||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||
@ -356,6 +356,12 @@ class AvailablePrefixesView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return serializers.AvailablePrefixSerializer
|
||||
|
||||
return serializers.PrefixLengthSerializer
|
||||
|
||||
|
||||
class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||
queryset = IPAddress.objects.all()
|
||||
@ -363,7 +369,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||
def get_parent(self, request, pk):
|
||||
raise NotImplemented()
|
||||
|
||||
@swagger_auto_schema(responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableIPSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
parent = self.get_parent(request, pk)
|
||||
limit = get_results_limit(request)
|
||||
@ -382,10 +388,7 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.AvailableIPSerializer,
|
||||
responses={201: serializers.IPAddressSerializer(many=True)}
|
||||
)
|
||||
@extend_schema(methods=["post"], responses={201: serializers.IPAddressSerializer(many=True)})
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-ips'])
|
||||
def post(self, request, pk):
|
||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||
@ -430,6 +433,12 @@ class AvailableIPAddressesView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return serializers.AvailableIPSerializer
|
||||
|
||||
return serializers.IPAddressSerializer
|
||||
|
||||
|
||||
class PrefixAvailableIPAddressesView(AvailableIPAddressesView):
|
||||
|
||||
@ -446,7 +455,7 @@ class IPRangeAvailableIPAddressesView(AvailableIPAddressesView):
|
||||
class AvailableVLANsView(ObjectValidationMixin, APIView):
|
||||
queryset = VLAN.objects.all()
|
||||
|
||||
@swagger_auto_schema(responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||
@extend_schema(methods=["get"], responses={200: serializers.AvailableVLANSerializer(many=True)})
|
||||
def get(self, request, pk):
|
||||
vlangroup = get_object_or_404(VLANGroup.objects.restrict(request.user), pk=pk)
|
||||
limit = get_results_limit(request)
|
||||
@ -459,10 +468,7 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@swagger_auto_schema(
|
||||
request_body=serializers.CreateAvailableVLANSerializer,
|
||||
responses={201: serializers.VLANSerializer(many=True)}
|
||||
)
|
||||
@extend_schema(methods=["post"], responses={201: serializers.VLANSerializer(many=True)})
|
||||
@advisory_lock(ADVISORY_LOCK_KEYS['available-vlans'])
|
||||
def post(self, request, pk):
|
||||
self.queryset = self.queryset.restrict(request.user, 'add')
|
||||
@ -514,3 +520,9 @@ class AvailableVLANsView(ObjectValidationMixin, APIView):
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == "GET":
|
||||
return serializers.AvailableVLANSerializer
|
||||
|
||||
return serializers.VLANSerializer
|
||||
|
@ -16,8 +16,6 @@ from virtualization.models import VirtualMachine, VMInterface
|
||||
from .choices import *
|
||||
from .models import *
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
__all__ = (
|
||||
'AggregateFilterSet',
|
||||
'ASNFilterSet',
|
||||
|
@ -1,4 +1,6 @@
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from netaddr import IPNetwork
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
@ -12,6 +14,7 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class ChoiceField(serializers.Field):
|
||||
"""
|
||||
Represent a ChoiceField as {'value': <DB value>, 'label': <string>}. Accepts a single value on write.
|
||||
@ -86,6 +89,7 @@ class ChoiceField(serializers.Field):
|
||||
return self._choices
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class ContentTypeField(RelatedField):
|
||||
"""
|
||||
Represent a ContentType as '<app_label>.<model>'
|
||||
|
@ -1,5 +1,7 @@
|
||||
from django.db.models import ManyToManyField
|
||||
from rest_framework import serializers
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
__all__ = (
|
||||
'BaseModelSerializer',
|
||||
@ -10,6 +12,7 @@ __all__ = (
|
||||
class BaseModelSerializer(serializers.ModelSerializer):
|
||||
display = serializers.SerializerMethodField(read_only=True)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_display(self, obj):
|
||||
return str(obj)
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField
|
||||
@ -38,7 +38,7 @@ class GenericObjectSerializer(serializers.Serializer):
|
||||
|
||||
return data
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_object(self, obj):
|
||||
serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX)
|
||||
# context = {'request': self.context['request']}
|
||||
|
@ -4,6 +4,8 @@ from django import __version__ as DJANGO_VERSION
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django_rq.queues import get_connection
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.reverse import reverse
|
||||
from rest_framework.views import APIView
|
||||
@ -17,12 +19,12 @@ class APIRootView(APIView):
|
||||
This is the root of NetBox's REST API. API endpoints are arranged by app and model name; e.g. `/api/dcim/sites/`.
|
||||
"""
|
||||
_ignore_model_permissions = True
|
||||
exclude_from_schema = True
|
||||
swagger_schema = None
|
||||
# schema = None
|
||||
|
||||
def get_view_name(self):
|
||||
return "API Root"
|
||||
|
||||
@extend_schema(exclude=True)
|
||||
def get(self, request, format=None):
|
||||
|
||||
return Response({
|
||||
@ -46,6 +48,7 @@ class StatusView(APIView):
|
||||
"""
|
||||
permission_classes = [IsAuthenticatedOrLoginNotRequired]
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def get(self, request):
|
||||
# Gather the version numbers from all installed Django apps
|
||||
installed_apps = {}
|
||||
|
@ -344,7 +344,7 @@ INSTALLED_APPS = [
|
||||
'virtualization',
|
||||
'wireless',
|
||||
'django_rq', # Must come after extras to allow overriding management commands
|
||||
'drf_yasg',
|
||||
'drf_spectacular',
|
||||
]
|
||||
|
||||
# Middleware
|
||||
@ -561,6 +561,7 @@ REST_FRAMEWORK = {
|
||||
'rest_framework.renderers.JSONRenderer',
|
||||
'netbox.api.renderers.FormlessBrowsableAPIRenderer',
|
||||
),
|
||||
'DEFAULT_SCHEMA_CLASS': 'core.api.schema.NetBoxAutoSchema',
|
||||
'DEFAULT_VERSION': REST_FRAMEWORK_VERSION,
|
||||
'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.AcceptHeaderVersioning',
|
||||
'SCHEMA_COERCE_METHOD_NAMES': {
|
||||
@ -573,6 +574,17 @@ REST_FRAMEWORK = {
|
||||
'VIEW_NAME_FUNCTION': 'utilities.api.get_view_name',
|
||||
}
|
||||
|
||||
#
|
||||
# DRF Spectacular
|
||||
#
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "NetBox API",
|
||||
"DESCRIPTION": "API to access NetBox",
|
||||
"LICENSE": {"name": "Apache v2 License"},
|
||||
"VERSION": VERSION,
|
||||
'COMPONENT_SPLIT_REQUEST': True,
|
||||
}
|
||||
|
||||
#
|
||||
# Graphene
|
||||
@ -585,49 +597,6 @@ GRAPHENE = {
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# drf_yasg (OpenAPI/Swagger)
|
||||
#
|
||||
|
||||
SWAGGER_SETTINGS = {
|
||||
'DEFAULT_AUTO_SCHEMA_CLASS': 'utilities.custom_inspectors.NetBoxSwaggerAutoSchema',
|
||||
'DEFAULT_FIELD_INSPECTORS': [
|
||||
'utilities.custom_inspectors.CustomFieldsDataFieldInspector',
|
||||
'utilities.custom_inspectors.NullableBooleanFieldInspector',
|
||||
'utilities.custom_inspectors.ChoiceFieldInspector',
|
||||
'utilities.custom_inspectors.SerializedPKRelatedFieldInspector',
|
||||
'drf_yasg.inspectors.CamelCaseJSONFilter',
|
||||
'drf_yasg.inspectors.ReferencingSerializerInspector',
|
||||
'drf_yasg.inspectors.RelatedFieldInspector',
|
||||
'drf_yasg.inspectors.ChoiceFieldInspector',
|
||||
'drf_yasg.inspectors.FileFieldInspector',
|
||||
'drf_yasg.inspectors.DictFieldInspector',
|
||||
'drf_yasg.inspectors.JSONFieldInspector',
|
||||
'drf_yasg.inspectors.SerializerMethodFieldInspector',
|
||||
'drf_yasg.inspectors.SimpleFieldInspector',
|
||||
'drf_yasg.inspectors.StringDefaultFieldInspector',
|
||||
],
|
||||
'DEFAULT_FILTER_INSPECTORS': [
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
],
|
||||
'DEFAULT_INFO': 'netbox.urls.openapi_info',
|
||||
'DEFAULT_MODEL_DEPTH': 1,
|
||||
'DEFAULT_PAGINATOR_INSPECTORS': [
|
||||
'utilities.custom_inspectors.NullablePaginatorInspector',
|
||||
'drf_yasg.inspectors.DjangoRestResponsePagination',
|
||||
'drf_yasg.inspectors.CoreAPICompatInspector',
|
||||
],
|
||||
'SECURITY_DEFINITIONS': {
|
||||
'Bearer': {
|
||||
'type': 'apiKey',
|
||||
'name': 'Authorization',
|
||||
'in': 'header',
|
||||
}
|
||||
},
|
||||
'VALIDATOR_URL': None,
|
||||
}
|
||||
|
||||
|
||||
#
|
||||
# Django RQ (Webhooks backend)
|
||||
#
|
||||
|
@ -3,8 +3,7 @@ from django.conf.urls import include
|
||||
from django.urls import path, re_path
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.static import serve
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.views import get_schema_view
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView, SpectacularSwaggerView
|
||||
|
||||
from extras.plugins.urls import plugin_admin_patterns, plugin_patterns, plugin_api_patterns
|
||||
from netbox.api.views import APIRootView, StatusView
|
||||
@ -14,20 +13,6 @@ from netbox.views import HomeView, StaticMediaFailureView, SearchView, htmx
|
||||
from users.views import LoginView, LogoutView
|
||||
from .admin import admin_site
|
||||
|
||||
openapi_info = openapi.Info(
|
||||
title="NetBox API",
|
||||
default_version='v3',
|
||||
description="API to access NetBox",
|
||||
terms_of_service="https://github.com/netbox-community/netbox",
|
||||
license=openapi.License(name="Apache v2 License"),
|
||||
)
|
||||
|
||||
schema_view = get_schema_view(
|
||||
openapi_info,
|
||||
validators=['flex', 'ssv'],
|
||||
public=True,
|
||||
permission_classes=()
|
||||
)
|
||||
|
||||
_patterns = [
|
||||
|
||||
@ -66,9 +51,10 @@ _patterns = [
|
||||
path('api/virtualization/', include('virtualization.api.urls')),
|
||||
path('api/wireless/', include('wireless.api.urls')),
|
||||
path('api/status/', StatusView.as_view(), name='api-status'),
|
||||
path('api/docs/', schema_view.with_ui('swagger', cache_timeout=86400), name='api_docs'),
|
||||
path('api/redoc/', schema_view.with_ui('redoc', cache_timeout=86400), name='api_redocs'),
|
||||
re_path(r'^api/swagger(?P<format>.json|.yaml)$', schema_view.without_ui(cache_timeout=86400), name='schema_swagger'),
|
||||
|
||||
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='api_docs'),
|
||||
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='api_redocs'),
|
||||
|
||||
# GraphQL
|
||||
path('graphql/', csrf_exempt(GraphQLView.as_view(graphiql=True, schema=schema)), name='graphql'),
|
||||
|
@ -1,3 +1,4 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
@ -17,6 +18,9 @@ __all__ = [
|
||||
# Tenants
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('tenant_count',),
|
||||
)
|
||||
class NestedTenantGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
|
||||
tenant_count = serializers.IntegerField(read_only=True)
|
||||
@ -39,6 +43,9 @@ class NestedTenantSerializer(WritableNestedSerializer):
|
||||
# Contacts
|
||||
#
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('contact_count',),
|
||||
)
|
||||
class NestedContactGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
|
||||
contact_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -1,5 +1,6 @@
|
||||
from django.contrib.auth.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||
@ -98,7 +99,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
||||
object = serializers.SerializerMethodField(read_only=True)
|
||||
contact = NestedContactSerializer()
|
||||
role = NestedContactRoleSerializer(required=False, allow_null=True)
|
||||
priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default='')
|
||||
priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
|
||||
|
||||
class Meta:
|
||||
model = ContactAssignment
|
||||
@ -107,7 +108,7 @@ class ContactAssignmentSerializer(NetBoxModelSerializer):
|
||||
'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(OpenApiTypes.OBJECT)
|
||||
def get_object(self, instance):
|
||||
serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
|
||||
context = {'request': self.context['request']}
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField
|
||||
@ -30,6 +31,7 @@ class NestedUserSerializer(WritableNestedSerializer):
|
||||
model = User
|
||||
fields = ['id', 'url', 'display', 'username']
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_display(self, obj):
|
||||
if full_name := obj.get_full_name():
|
||||
return f"{obj.username} ({full_name})"
|
||||
@ -57,10 +59,10 @@ class NestedObjectPermissionSerializer(WritableNestedSerializer):
|
||||
model = ObjectPermission
|
||||
fields = ['id', 'url', 'display', 'name', 'enabled', 'object_types', 'groups', 'users', 'actions']
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField)
|
||||
def get_groups(self, obj):
|
||||
return [g.name for g in obj.groups.all()]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.ListField)
|
||||
@extend_schema_field(serializers.ListField)
|
||||
def get_users(self, obj):
|
||||
return [u.username for u in obj.users.all()]
|
||||
|
@ -1,6 +1,8 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
|
||||
@ -47,6 +49,7 @@ class UserSerializer(ValidatedModelSerializer):
|
||||
|
||||
return user
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
def get_display(self, obj):
|
||||
if full_name := obj.get_full_name():
|
||||
return f"{obj.username} ({full_name})"
|
||||
|
@ -1,6 +1,8 @@
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db.models import Count
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
@ -55,9 +57,6 @@ class TokenViewSet(NetBoxModelViewSet):
|
||||
Limit the non-superusers to their own Tokens.
|
||||
"""
|
||||
queryset = super().get_queryset()
|
||||
# Workaround for schema generation (drf_yasg)
|
||||
if getattr(self, 'swagger_fake_view', False):
|
||||
return queryset.none()
|
||||
if not self.request.user.is_authenticated:
|
||||
return queryset.none()
|
||||
if self.request.user.is_superuser:
|
||||
@ -71,6 +70,7 @@ class TokenProvisionView(APIView):
|
||||
"""
|
||||
permission_classes = []
|
||||
|
||||
# @extend_schema(methods=["post"], responses={201: serializers.TokenSerializer})
|
||||
def post(self, request):
|
||||
serializer = serializers.TokenProvisionSerializer(data=request.data)
|
||||
serializer.is_valid()
|
||||
@ -93,6 +93,9 @@ class TokenProvisionView(APIView):
|
||||
|
||||
return Response(data, status=HTTP_201_CREATED)
|
||||
|
||||
def get_serializer_class(self):
|
||||
return serializers.TokenSerializer
|
||||
|
||||
|
||||
#
|
||||
# ObjectPermissions
|
||||
@ -117,6 +120,7 @@ class UserConfigViewSet(ViewSet):
|
||||
def get_queryset(self):
|
||||
return UserConfig.objects.filter(user=self.request.user)
|
||||
|
||||
@extend_schema(responses={200: OpenApiTypes.OBJECT})
|
||||
def list(self, request):
|
||||
"""
|
||||
Return the UserConfig for the currently authenticated User.
|
||||
@ -125,6 +129,7 @@ class UserConfigViewSet(ViewSet):
|
||||
|
||||
return Response(userconfig.data)
|
||||
|
||||
@extend_schema(methods=["patch"], responses={201: OpenApiTypes.OBJECT})
|
||||
def patch(self, request):
|
||||
"""
|
||||
Update the UserConfig for the currently authenticated User.
|
||||
|
@ -1,142 +0,0 @@
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.inspectors import FieldInspector, NotHandled, PaginatorInspector, SwaggerAutoSchema
|
||||
from drf_yasg.utils import get_serializer_ref_name
|
||||
from rest_framework.fields import ChoiceField
|
||||
from rest_framework.relations import ManyRelatedField
|
||||
|
||||
from extras.api.customfields import CustomFieldsDataField
|
||||
from netbox.api.fields import ChoiceField, SerializedPKRelatedField
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
|
||||
|
||||
class NetBoxSwaggerAutoSchema(SwaggerAutoSchema):
|
||||
writable_serializers = {}
|
||||
|
||||
def get_operation_id(self, operation_keys=None):
|
||||
operation_keys = operation_keys or self.operation_keys
|
||||
operation_id = self.overrides.get('operation_id', '')
|
||||
if not operation_id:
|
||||
# Overwrite the action for bulk update/bulk delete views to ensure they get an operation ID that's
|
||||
# unique from their single-object counterparts (see #3436)
|
||||
if operation_keys[-1] in ('delete', 'partial_update', 'update') and not getattr(self.view, 'detail', None):
|
||||
operation_keys[-1] = f'bulk_{operation_keys[-1]}'
|
||||
operation_id = '_'.join(operation_keys)
|
||||
|
||||
return operation_id
|
||||
|
||||
def get_request_serializer(self):
|
||||
serializer = super().get_request_serializer()
|
||||
|
||||
if serializer is not None and not isinstance(serializer, openapi.Schema) and self.method in self.implicit_body_methods:
|
||||
if writable_class := self.get_writable_class(serializer):
|
||||
if hasattr(serializer, 'child'):
|
||||
child_serializer = self.get_writable_class(serializer.child)
|
||||
serializer = writable_class(context=serializer.context, child=child_serializer)
|
||||
else:
|
||||
serializer = writable_class(context=serializer.context)
|
||||
return serializer
|
||||
|
||||
def get_writable_class(self, serializer):
|
||||
properties = {}
|
||||
fields = {} if hasattr(serializer, 'child') else serializer.fields
|
||||
for child_name, child in fields.items():
|
||||
if isinstance(child, (ChoiceField, WritableNestedSerializer)):
|
||||
properties[child_name] = None
|
||||
elif isinstance(child, ManyRelatedField) and isinstance(child.child_relation, SerializedPKRelatedField):
|
||||
properties[child_name] = None
|
||||
|
||||
if properties:
|
||||
if type(serializer) not in self.writable_serializers:
|
||||
writable_name = 'Writable' + type(serializer).__name__
|
||||
meta_class = getattr(type(serializer), 'Meta', None)
|
||||
if meta_class:
|
||||
ref_name = 'Writable' + get_serializer_ref_name(serializer)
|
||||
writable_meta = type('Meta', (meta_class,), {'ref_name': ref_name})
|
||||
properties['Meta'] = writable_meta
|
||||
|
||||
self.writable_serializers[type(serializer)] = type(writable_name, (type(serializer),), properties)
|
||||
|
||||
writable_class = self.writable_serializers[type(serializer)]
|
||||
return writable_class
|
||||
|
||||
|
||||
class SerializedPKRelatedFieldInspector(FieldInspector):
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
if isinstance(field, SerializedPKRelatedField):
|
||||
return self.probe_field_inspectors(field.serializer(), ChildSwaggerType, use_references)
|
||||
|
||||
return NotHandled
|
||||
|
||||
|
||||
class ChoiceFieldInspector(FieldInspector):
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
# this returns a callable which extracts title, description and other stuff
|
||||
# https://drf-yasg.readthedocs.io/en/stable/_modules/drf_yasg/inspectors/base.html#FieldInspector._get_partial_types
|
||||
SwaggerType, _ = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
|
||||
if isinstance(field, ChoiceField):
|
||||
choices = field._choices
|
||||
choice_value = list(choices.keys())
|
||||
choice_label = list(choices.values())
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_STRING, enum=choice_value)
|
||||
|
||||
if set([None] + choice_value) == {None, True, False}:
|
||||
# DeviceType.subdevice_role and Device.face need to be differentiated since they each have
|
||||
# subtly different values in their choice keys.
|
||||
# - subdevice_role and connection_status are booleans, although subdevice_role includes None
|
||||
# - face is an integer set {0, 1} which is easily confused with {False, True}
|
||||
schema_type = openapi.TYPE_STRING
|
||||
if all(type(x) == bool for x in [c for c in choice_value if c is not None]):
|
||||
schema_type = openapi.TYPE_BOOLEAN
|
||||
value_schema = openapi.Schema(type=schema_type, enum=choice_value)
|
||||
value_schema['x-nullable'] = True
|
||||
|
||||
if all(type(x) == int for x in [c for c in choice_value if c is not None]):
|
||||
# Change value_schema for IPAddressFamilyChoices, RackWidthChoices
|
||||
value_schema = openapi.Schema(type=openapi.TYPE_INTEGER, enum=choice_value)
|
||||
|
||||
schema = SwaggerType(type=openapi.TYPE_OBJECT, required=["label", "value"], properties={
|
||||
"label": openapi.Schema(type=openapi.TYPE_STRING, enum=choice_label),
|
||||
"value": value_schema
|
||||
})
|
||||
|
||||
return schema
|
||||
|
||||
return NotHandled
|
||||
|
||||
|
||||
class NullableBooleanFieldInspector(FieldInspector):
|
||||
def process_result(self, result, method_name, obj, **kwargs):
|
||||
|
||||
if isinstance(result, openapi.Schema) and isinstance(obj, ChoiceField) and result.type == 'boolean':
|
||||
keys = obj.choices.keys()
|
||||
if set(keys) == {None, True, False}:
|
||||
result['x-nullable'] = True
|
||||
result.type = 'boolean'
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class CustomFieldsDataFieldInspector(FieldInspector):
|
||||
|
||||
def field_to_swagger_object(self, field, swagger_object_type, use_references, **kwargs):
|
||||
SwaggerType, ChildSwaggerType = self._get_partial_types(field, swagger_object_type, use_references, **kwargs)
|
||||
|
||||
if isinstance(field, CustomFieldsDataField) and swagger_object_type == openapi.Schema:
|
||||
return SwaggerType(type=openapi.TYPE_OBJECT)
|
||||
|
||||
return NotHandled
|
||||
|
||||
|
||||
class NullablePaginatorInspector(PaginatorInspector):
|
||||
def process_result(self, result, method_name, obj, **kwargs):
|
||||
if method_name == 'get_paginated_response' and isinstance(result, openapi.Schema):
|
||||
next = result.properties['next']
|
||||
if isinstance(next, openapi.Schema):
|
||||
next['x-nullable'] = True
|
||||
previous = result.properties['previous']
|
||||
if isinstance(previous, openapi.Schema):
|
||||
previous['x-nullable'] = True
|
||||
|
||||
return result
|
@ -3,6 +3,8 @@ from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django_filters.constants import EMPTY_VALUES
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
|
||||
|
||||
def multivalue_field_factory(field_class):
|
||||
@ -37,26 +39,32 @@ def multivalue_field_factory(field_class):
|
||||
# Filters
|
||||
#
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class MultiValueCharFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.CharField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATE)
|
||||
class MultiValueDateFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.DateField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DATETIME)
|
||||
class MultiValueDateTimeFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.DateTimeField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT32)
|
||||
class MultiValueNumberFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.IntegerField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.DECIMAL)
|
||||
class MultiValueDecimalFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.DecimalField)
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.TIME)
|
||||
class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.TimeField)
|
||||
|
||||
@ -65,6 +73,7 @@ class MACAddressFilter(django_filters.CharFilter):
|
||||
pass
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.CharField)
|
||||
|
||||
@ -75,6 +84,7 @@ class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
||||
return qs.none()
|
||||
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
class MultiValueWWNFilter(django_filters.MultipleChoiceFilter):
|
||||
field_class = multivalue_field_factory(forms.CharField)
|
||||
|
||||
|
@ -249,9 +249,9 @@ class APIDocsTestCase(TestCase):
|
||||
def test_api_docs(self):
|
||||
|
||||
url = reverse('api_docs')
|
||||
params = {
|
||||
"format": "openapi",
|
||||
}
|
||||
|
||||
response = self.client.get('{}?{}'.format(url, urllib.parse.urlencode(params)))
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
url = reverse('schema')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -1,3 +1,4 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
@ -16,6 +17,9 @@ __all__ = [
|
||||
#
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('cluster_count',),
|
||||
)
|
||||
class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
@ -25,6 +29,9 @@ class NestedClusterTypeSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('cluster_count',),
|
||||
)
|
||||
class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
|
||||
cluster_count = serializers.IntegerField(read_only=True)
|
||||
@ -34,6 +41,9 @@ class NestedClusterGroupSerializer(WritableNestedSerializer):
|
||||
fields = ['id', 'url', 'display', 'name', 'slug', 'cluster_count']
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('virtualmachine_count',),
|
||||
)
|
||||
class NestedClusterSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
|
||||
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -1,4 +1,4 @@
|
||||
from drf_yasg.utils import swagger_serializer_method
|
||||
from drf_spectacular.utils import extend_schema_field
|
||||
from rest_framework import serializers
|
||||
|
||||
from dcim.api.nested_serializers import (
|
||||
@ -100,7 +100,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
|
||||
'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated',
|
||||
]
|
||||
|
||||
@swagger_serializer_method(serializer_or_field=serializers.JSONField)
|
||||
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||
def get_config_context(self, obj):
|
||||
return obj.get_config_context()
|
||||
|
||||
@ -114,7 +114,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
virtual_machine = NestedVirtualMachineSerializer()
|
||||
parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||
bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
|
||||
mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False, allow_null=True)
|
||||
untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
|
||||
tagged_vlans = SerializedPKRelatedField(
|
||||
queryset=VLAN.objects.all(),
|
||||
@ -123,9 +123,10 @@ class VMInterfaceSerializer(NetBoxModelSerializer):
|
||||
many=True
|
||||
)
|
||||
vrf = NestedVRFSerializer(required=False, allow_null=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True)
|
||||
l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
|
||||
count_ipaddresses = serializers.IntegerField(read_only=True)
|
||||
count_fhrp_groups = serializers.IntegerField(read_only=True)
|
||||
mac_address = serializers.CharField(required=False, default=None)
|
||||
|
||||
class Meta:
|
||||
model = VMInterface
|
||||
|
@ -1,3 +1,4 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from netbox.api.serializers import WritableNestedSerializer
|
||||
@ -10,6 +11,9 @@ __all__ = (
|
||||
)
|
||||
|
||||
|
||||
@extend_schema_serializer(
|
||||
exclude_fields=('wirelesslan_count',),
|
||||
)
|
||||
class NestedWirelessLANGroupSerializer(WritableNestedSerializer):
|
||||
url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
|
||||
wirelesslan_count = serializers.IntegerField(read_only=True)
|
||||
|
@ -15,7 +15,7 @@ django-tables2==2.5.3
|
||||
django-taggit==3.1.0
|
||||
django-timezone-field==5.0
|
||||
djangorestframework==3.14.0
|
||||
drf-yasg[validation]==1.21.5
|
||||
drf-spectacular==0.25.1
|
||||
feedparser==6.0.10
|
||||
graphene-django==3.0.0
|
||||
gunicorn==20.1.0
|
||||
|
Reference in New Issue
Block a user