1
0
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:
Arthur Hanson
2023-03-30 11:32:59 -07:00
committed by GitHub
parent 1be626e5ee
commit ecd0c56554
35 changed files with 514 additions and 340 deletions

View File

@ -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

View File

@ -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)

View File

@ -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
View 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

View File

@ -6,3 +6,4 @@ class CoreConfig(AppConfig):
def ready(self):
from . import data_backends, search
from core.api import schema # noqa: E402

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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',
}

View File

@ -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):

View File

@ -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.

View File

@ -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):

View File

@ -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"

View File

@ -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)

View File

@ -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']}

View File

@ -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

View File

@ -16,8 +16,6 @@ from virtualization.models import VirtualMachine, VMInterface
from .choices import *
from .models import *
from rest_framework import serializers
__all__ = (
'AggregateFilterSet',
'ASNFilterSet',

View File

@ -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>'

View File

@ -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)

View File

@ -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']}

View File

@ -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 = {}

View File

@ -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)
#

View File

@ -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'),

View File

@ -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)

View File

@ -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']}

View File

@ -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()]

View File

@ -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})"

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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