diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 574924c4a..c9341853b 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -2,13 +2,12 @@ from rest_framework import serializers from circuits.choices import CircuitStatusChoices from circuits.models import * -from dcim.api.nested_serializers import NestedSiteSerializer -from dcim.api.serializers import CabledObjectSerializer +from dcim.api.serializers import CabledObjectSerializer, SiteSerializer from ipam.api.nested_serializers import NestedASNSerializer from ipam.models import ASN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from .nested_serializers import * @@ -49,7 +48,7 @@ class ProviderSerializer(NetBoxModelSerializer): class ProviderAccountSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') - provider = NestedProviderSerializer() + provider = ProviderSerializer(nested=True) class Meta: model = ProviderAccount @@ -66,7 +65,7 @@ class ProviderAccountSerializer(NetBoxModelSerializer): class ProviderNetworkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') - provider = NestedProviderSerializer() + provider = ProviderSerializer(nested=True) class Meta: model = ProviderNetwork @@ -98,8 +97,8 @@ class CircuitTypeSerializer(NetBoxModelSerializer): class CircuitCircuitTerminationSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - site = NestedSiteSerializer(allow_null=True) - provider_network = NestedProviderNetworkSerializer(allow_null=True) + site = SiteSerializer(nested=True, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) class Meta: model = CircuitTermination @@ -111,11 +110,11 @@ class CircuitCircuitTerminationSerializer(WritableNestedSerializer): class CircuitSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') - provider = NestedProviderSerializer() - provider_account = NestedProviderAccountSerializer(required=False, allow_null=True) + provider = ProviderSerializer(nested=True) + provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=CircuitStatusChoices, required=False) - type = NestedCircuitTypeSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + type = CircuitTypeSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) @@ -131,9 +130,9 @@ class CircuitSerializer(NetBoxModelSerializer): class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - circuit = NestedCircuitSerializer() - site = NestedSiteSerializer(required=False, allow_null=True) - provider_network = NestedProviderNetworkSerializer(required=False, allow_null=True) + circuit = CircuitSerializer(nested=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, required=False, allow_null=True) class Meta: model = CircuitTermination diff --git a/netbox/core/api/nested_serializers.py b/netbox/core/api/nested_serializers.py index d99738cbe..efb748ee0 100644 --- a/netbox/core/api/nested_serializers.py +++ b/netbox/core/api/nested_serializers.py @@ -4,7 +4,7 @@ from core.choices import JobStatusChoices from core.models import * from netbox.api.fields import ChoiceField from netbox.api.serializers import WritableNestedSerializer -from users.api.nested_serializers import NestedUserSerializer +from users.api.serializers import UserSerializer __all__ = ( 'NestedDataFileSerializer', @@ -32,7 +32,8 @@ class NestedDataFileSerializer(WritableNestedSerializer): class NestedJobSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') status = ChoiceField(choices=JobStatusChoices) - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) diff --git a/netbox/core/api/serializers.py b/netbox/core/api/serializers.py index be3e9ff4a..3a2794304 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -5,8 +5,7 @@ from core.models import * from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer from netbox.utils import get_data_backend_choices -from users.api.nested_serializers import NestedUserSerializer -from .nested_serializers import * +from users.api.serializers import UserSerializer __all__ = ( 'DataFileSerializer', @@ -43,7 +42,8 @@ class DataFileSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='core-api:datafile-detail' ) - source = NestedDataSourceSerializer( + source = DataSourceSerializer( + nested=True, read_only=True ) @@ -57,7 +57,8 @@ class DataFileSerializer(NetBoxModelSerializer): class JobSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) status = ChoiceField(choices=JobStatusChoices, read_only=True) diff --git a/netbox/dcim/api/nested_serializers.py b/netbox/dcim/api/nested_serializers.py index 9a59af8e2..1d9828ee3 100644 --- a/netbox/dcim/api/nested_serializers.py +++ b/netbox/dcim/api/nested_serializers.py @@ -6,8 +6,6 @@ from netbox.api.fields import RelatedObjectCountField from netbox.api.serializers import WritableNestedSerializer __all__ = [ - 'ComponentNestedModuleSerializer', - 'ModuleBayNestedModuleSerializer', 'NestedCableSerializer', 'NestedConsolePortSerializer', 'NestedConsolePortTemplateSerializer', @@ -311,26 +309,6 @@ class ModuleNestedModuleBaySerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name'] -class ModuleBayNestedModuleSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - - class Meta: - model = models.Module - fields = ['id', 'url', 'display', 'serial'] - - -class ComponentNestedModuleSerializer(WritableNestedSerializer): - """ - Used by device component serializers. - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - module_bay = ModuleNestedModuleBaySerializer(read_only=True) - - class Meta: - model = models.Module - fields = ['id', 'url', 'display', 'device', 'module_bay'] - - class NestedModuleSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') device = NestedDeviceSerializer(read_only=True) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 1bf4969e2..af0e4b2d6 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -10,7 +10,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.api.nested_serializers import NestedConfigTemplateSerializer +from extras.api.serializers import ConfigTemplateSerializer from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, ) @@ -22,8 +22,8 @@ from netbox.api.serializers import ( ) from netbox.config import ConfigItem from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.nested_serializers import NestedTenantSerializer -from users.api.nested_serializers import NestedUserSerializer +from tenancy.api.serializers import TenantSerializer +from users.api.serializers import UserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import NestedClusterSerializer from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer @@ -33,1126 +33,6 @@ from wireless.models import WirelessLAN from .nested_serializers import * -class CabledObjectSerializer(serializers.ModelSerializer): - 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. - """ - if not obj.cable: - return None - - if obj.link_peers: - return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' - - return None - - @extend_schema_field(serializers.ListField) - def get_link_peers(self, obj): - """ - Return the appropriate serializer for the link termination model. - """ - if not obj.link_peers: - return [] - - # Return serialized peer termination objects - serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.link_peers, context=context, many=True).data - - @extend_schema_field(serializers.BooleanField) - def get__occupied(self, obj): - return obj._occupied - - -class ConnectedEndpointsSerializer(serializers.ModelSerializer): - """ - Legacy serializer for pre-v3.3 connections - """ - connected_endpoints_type = serializers.SerializerMethodField(read_only=True) - 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}' - - @extend_schema_field(serializers.ListField) - def get_connected_endpoints(self, obj): - """ - Return the appropriate serializer for the type of connected object. - """ - if endpoints := obj.connected_endpoints: - serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(endpoints, many=True, context=context).data - - @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 - - -# -# Regions/sites -# - -class RegionSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') - parent = NestedRegionSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Region - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') - parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) - site_count = serializers.IntegerField(read_only=True) - - class Meta: - model = SiteGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'site_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') - - -class SiteSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') - status = ChoiceField(choices=SiteStatusChoices, required=False) - region = NestedRegionSerializer(required=False, allow_null=True) - group = NestedSiteGroupSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - time_zone = TimeZoneSerializerField(required=False, allow_null=True) - asns = SerializedPKRelatedField( - queryset=ASN.objects.all(), - serializer=NestedASNSerializer, - required=False, - many=True - ) - - # Related object counts - circuit_count = RelatedObjectCountField('circuit_terminations') - device_count = RelatedObjectCountField('devices') - prefix_count = RelatedObjectCountField('prefixes') - rack_count = RelatedObjectCountField('racks') - vlan_count = RelatedObjectCountField('vlans') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Site - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', - 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', - 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', - 'virtualmachine_count', 'vlan_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') - - -# -# Racks -# - -class LocationSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') - site = NestedSiteSerializer() - parent = NestedLocationSerializer(required=False, allow_null=True) - status = ChoiceField(choices=LocationStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) - rack_count = serializers.IntegerField(read_only=True) - device_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Location - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') - - -class RackRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') - - # Related object counts - rack_count = RelatedObjectCountField('racks') - - class Meta: - model = RackRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'rack_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') - - -class RackSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') - site = NestedSiteSerializer() - location = NestedLocationSerializer(required=False, allow_null=True, default=None) - 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, 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, allow_null=True) - weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - - # Related object counts - device_count = RelatedObjectCountField('devices') - powerfeed_count = RelatedObjectCountField('powerfeeds') - - class Meta: - model = Rack - fields = [ - 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', - 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', - 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') - - -class RackUnitSerializer(serializers.Serializer): - """ - A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. - """ - id = serializers.DecimalField( - max_digits=4, - decimal_places=1, - read_only=True - ) - name = serializers.CharField(read_only=True) - face = ChoiceField(choices=DeviceFaceChoices, read_only=True) - device = NestedDeviceSerializer(read_only=True) - 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'] - - -class RackReservationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') - rack = NestedRackSerializer() - user = NestedUserSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) - - class Meta: - model = RackReservation - fields = [ - 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', - 'comments', 'tags', 'custom_fields', - ] - brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') - - -class RackElevationDetailFilterSerializer(serializers.Serializer): - q = serializers.CharField( - required=False, - default=None - ) - face = serializers.ChoiceField( - choices=DeviceFaceChoices, - default=DeviceFaceChoices.FACE_FRONT - ) - render = serializers.ChoiceField( - choices=RackElevationDetailRenderChoices, - default=RackElevationDetailRenderChoices.RENDER_JSON - ) - unit_width = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') - ) - unit_height = serializers.IntegerField( - default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') - ) - legend_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH - ) - margin_width = serializers.IntegerField( - default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH - ) - exclude = serializers.IntegerField( - required=False, - default=None - ) - expand_devices = serializers.BooleanField( - required=False, - default=True - ) - include_images = serializers.BooleanField( - required=False, - default=True - ) - - -# -# Device/module types -# - -class ManufacturerSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') - - # Related object counts - devicetype_count = RelatedObjectCountField('device_types') - inventoryitem_count = RelatedObjectCountField('inventory_items') - platform_count = RelatedObjectCountField('platforms') - - class Meta: - model = Manufacturer - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'devicetype_count', 'inventoryitem_count', 'platform_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') - - -class DeviceTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') - manufacturer = NestedManufacturerSerializer() - default_platform = NestedPlatformSerializer(required=False, allow_null=True) - u_height = serializers.DecimalField( - max_digits=4, - decimal_places=1, - label=_('Position (U)'), - min_value=0, - default=1.0 - ) - 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) - front_image = serializers.URLField(allow_null=True, required=False) - rear_image = serializers.URLField(allow_null=True, required=False) - - # Counter fields - console_port_template_count = serializers.IntegerField(read_only=True) - console_server_port_template_count = serializers.IntegerField(read_only=True) - power_port_template_count = serializers.IntegerField(read_only=True) - power_outlet_template_count = serializers.IntegerField(read_only=True) - interface_template_count = serializers.IntegerField(read_only=True) - front_port_template_count = serializers.IntegerField(read_only=True) - rear_port_template_count = serializers.IntegerField(read_only=True) - device_bay_template_count = serializers.IntegerField(read_only=True) - module_bay_template_count = serializers.IntegerField(read_only=True) - inventory_item_template_count = serializers.IntegerField(read_only=True) - - # Related object counts - device_count = RelatedObjectCountField('instances') - - class Meta: - model = DeviceType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', - 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', - 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', - 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', - 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', - 'inventory_item_template_count', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') - - -class ModuleTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') - manufacturer = NestedManufacturerSerializer() - weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) - - class Meta: - model = ModuleType - fields = [ - 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') - - -# -# Component templates -# - -class ConsolePortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsolePortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - - class Meta: - model = ConsoleServerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', - 'allocated_draw', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class PowerOutletTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = NestedPowerPortTemplateSerializer( - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutletTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InterfaceTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=InterfaceTypeChoices) - bridge = NestedInterfaceTemplateSerializer( - required=False, - 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 - ) - rf_role = ChoiceField( - choices=WirelessRoleChoices, - required=False, - allow_blank=True, - allow_null=True - ) - - class Meta: - model = InterfaceTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', - 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class RearPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', - 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class FrontPortTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') - device_type = NestedDeviceTypeSerializer( - required=False, - allow_null=True, - default=None - ) - module_type = NestedModuleTypeSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = NestedRearPortTemplateSerializer() - - class Meta: - model = FrontPortTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ModuleBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') - device_type = NestedDeviceTypeSerializer() - - class Meta: - model = ModuleBayTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class DeviceBayTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') - device_type = NestedDeviceTypeSerializer() - - class Meta: - model = DeviceBayTemplate - fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class InventoryItemTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') - device_type = NestedDeviceTypeSerializer() - parent = serializers.PrimaryKeyRelatedField( - queryset=InventoryItemTemplate.objects.all(), - allow_null=True, - default=None - ) - role = NestedInventoryItemRoleSerializer(required=False, allow_null=True) - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItemTemplate - fields = [ - 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', - 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Devices -# - -class DeviceRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = DeviceRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -class PlatformSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True) - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Platform - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') - - -class DeviceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') - device_type = NestedDeviceTypeSerializer() - role = NestedDeviceRoleSerializer() - device_role = NestedDeviceRoleSerializer(read_only=True, help_text='Deprecated in v3.6 in favor of `role`.') - tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) - platform = NestedPlatformSerializer(required=False, allow_null=True) - 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=lambda: '') - position = serializers.DecimalField( - max_digits=4, - decimal_places=1, - allow_null=True, - label=_('Position (U)'), - min_value=decimal.Decimal(0.5), - default=None - ) - status = ChoiceField(choices=DeviceStatusChoices, required=False) - airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) - primary_ip = NestedIPAddressSerializer(read_only=True) - primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) - primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) - parent_device = serializers.SerializerMethodField() - cluster = NestedClusterSerializer(required=False, allow_null=True) - virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) - vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) - - # Counter fields - console_port_count = serializers.IntegerField(read_only=True) - console_server_port_count = serializers.IntegerField(read_only=True) - power_port_count = serializers.IntegerField(read_only=True) - power_outlet_count = serializers.IntegerField(read_only=True) - interface_count = serializers.IntegerField(read_only=True) - front_port_count = serializers.IntegerField(read_only=True) - rear_port_count = serializers.IntegerField(read_only=True) - device_bay_count = serializers.IntegerField(read_only=True) - module_bay_count = serializers.IntegerField(read_only=True) - inventory_item_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Device - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', - 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', - 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', - 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(NestedDeviceSerializer) - def get_parent_device(self, obj): - try: - device_bay = obj.parent_bay - except DeviceBay.DoesNotExist: - return None - context = {'request': self.context['request']} - data = NestedDeviceSerializer(instance=device_bay.device, context=context).data - data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data - return data - - def get_device_role(self, obj): - return obj.role - - -class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) - - class Meta(DeviceSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', - 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', - 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', - 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', - 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', - 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', - 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_config_context(self, obj): - return obj.get_config_context() - - -class VirtualDeviceContextSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') - device = NestedDeviceSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True, default=None) - 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) - status = ChoiceField(choices=VirtualDeviceContextStatusChoices) - - # Related object counts - interface_count = RelatedObjectCountField('interfaces') - - class Meta: - model = VirtualDeviceContext - fields = [ - 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', - 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') - - -class ModuleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') - device = NestedDeviceSerializer() - module_bay = NestedModuleBaySerializer() - module_type = NestedModuleTypeSerializer() - status = ChoiceField(choices=ModuleStatusChoices, required=False) - - class Meta: - model = Module - fields = [ - 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') - - -# -# Device components -# - -class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsoleServerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=ConsolePortTypeChoices, - allow_blank=True, - required=False - ) - speed = ChoiceField( - choices=ConsolePortSpeedChoices, - allow_null=True, - required=False - ) - - class Meta: - model = ConsolePort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerOutletTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - power_port = NestedPowerPortSerializer( - required=False, - allow_null=True - ) - feed_leg = ChoiceField( - choices=PowerOutletFeedLegChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerOutlet - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField( - choices=PowerPortTypeChoices, - allow_blank=True, - required=False, - allow_null=True - ) - - class Meta: - model = PowerPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', - 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', - 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') - device = NestedDeviceSerializer() - vdcs = SerializedPKRelatedField( - queryset=VirtualDeviceContext.objects.all(), - serializer=NestedVirtualDeviceContextSerializer, - required=False, - many=True - ) - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=InterfaceTypeChoices) - 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, allow_null=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) - untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) - tagged_vlans = SerializedPKRelatedField( - queryset=VLAN.objects.all(), - serializer=NestedVLANSerializer, - required=False, - many=True - ) - vrf = NestedVRFSerializer(required=False, allow_null=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, - required=False, - many=True - ) - count_ipaddresses = serializers.IntegerField(read_only=True) - count_fhrp_groups = serializers.IntegerField(read_only=True) - mac_address = serializers.CharField( - required=False, - default=None, - allow_blank=True, - allow_null=True - ) - wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) - - class Meta: - model = Interface - fields = [ - 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', - 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', - 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', - 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', - 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', - 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', - 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - def validate(self, data): - - # Validate many-to-many VLAN assignments - device = self.instance.device if self.instance else data.get('device') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [device.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, or " - f"it must be global." - }) - - return super().validate(data) - - -class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - - class Meta: - model = RearPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', - 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', - 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class FrontPortRearPortSerializer(WritableNestedSerializer): - """ - NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') - - class Meta: - model = RearPort - fields = ['id', 'url', 'display', 'name', 'label', 'description'] - - -class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') - device = NestedDeviceSerializer() - module = ComponentNestedModuleSerializer( - required=False, - allow_null=True - ) - type = ChoiceField(choices=PortTypeChoices) - rear_port = FrontPortRearPortSerializer() - - class Meta: - model = FrontPort - fields = [ - 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', - 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') - - -class ModuleBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') - device = NestedDeviceSerializer() - installed_module = ModuleBayNestedModuleSerializer(required=False, allow_null=True) - - class Meta: - model = ModuleBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') - - -class DeviceBaySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') - device = NestedDeviceSerializer() - installed_device = NestedDeviceSerializer(required=False, allow_null=True) - - class Meta: - model = DeviceBay - fields = [ - 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') - - -class InventoryItemSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') - device = NestedDeviceSerializer() - parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) - role = NestedInventoryItemRoleSerializer(required=False, allow_null=True) - manufacturer = NestedManufacturerSerializer(required=False, allow_null=True, default=None) - component_type = ContentTypeField( - queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), - required=False, - allow_null=True - ) - component = serializers.SerializerMethodField(read_only=True) - _depth = serializers.IntegerField(source='level', read_only=True) - - class Meta: - model = InventoryItem - fields = [ - 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', - 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', - 'custom_fields', 'created', 'last_updated', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_component(self, obj): - if obj.component is None: - return None - serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.component, context=context).data - - -# -# Device component roles -# - -class InventoryItemRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') - - # Related object counts - inventoryitem_count = RelatedObjectCountField('inventory_items') - - class Meta: - model = InventoryItemRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'inventoryitem_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') - - # # Cables # @@ -1162,7 +42,7 @@ class CableSerializer(NetBoxModelSerializer): a_terminations = GenericObjectSerializer(many=True, required=False) b_terminations = GenericObjectSerializer(many=True, required=False) status = ChoiceField(choices=LinkStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) class Meta: @@ -1225,6 +105,660 @@ class CablePathSerializer(serializers.ModelSerializer): return ret +class CabledObjectSerializer(serializers.ModelSerializer): + cable = CableSerializer(nested=True, 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. + """ + if not obj.cable: + return None + + if obj.link_peers: + return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}' + + return None + + @extend_schema_field(serializers.ListField) + def get_link_peers(self, obj): + """ + Return the appropriate serializer for the link termination model. + """ + if not obj.link_peers: + return [] + + # Return serialized peer termination objects + serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.link_peers, context=context, many=True).data + + @extend_schema_field(serializers.BooleanField) + def get__occupied(self, obj): + return obj._occupied + + +# +# Regions/sites +# + +class RegionSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail') + parent = NestedRegionSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Region + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail') + parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None) + site_count = serializers.IntegerField(read_only=True) + + class Meta: + model = SiteGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'site_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth') + + +class SiteSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail') + status = ChoiceField(choices=SiteStatusChoices, required=False) + region = RegionSerializer(nested=True, required=False, allow_null=True) + group = SiteGroupSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(required=False, allow_null=True) + time_zone = TimeZoneSerializerField(required=False, allow_null=True) + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=NestedASNSerializer, + required=False, + many=True + ) + + # Related object counts + circuit_count = RelatedObjectCountField('circuit_terminations') + device_count = RelatedObjectCountField('devices') + prefix_count = RelatedObjectCountField('prefixes') + rack_count = RelatedObjectCountField('racks') + vlan_count = RelatedObjectCountField('vlans') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Site + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', + 'virtualmachine_count', 'vlan_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug') + + +# +# Racks +# + +class LocationSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail') + site = SiteSerializer(nested=True) + parent = NestedLocationSerializer(required=False, allow_null=True) + status = ChoiceField(choices=LocationStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + rack_count = serializers.IntegerField(read_only=True) + device_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Location + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth') + + +class RackRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail') + + # Related object counts + rack_count = RelatedObjectCountField('racks') + + class Meta: + model = RackRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'rack_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count') + + +class RackSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail') + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=RackStatusChoices, required=False) + role = RackRoleSerializer(nested=True, required=False, allow_null=True) + 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, allow_null=True) + weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) + + # Related object counts + device_count = RelatedObjectCountField('devices') + powerfeed_count = RelatedObjectCountField('powerfeeds') + + class Meta: + model = Rack + fields = [ + 'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial', + 'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit', + 'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count') + + +class RackReservationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') + rack = RackSerializer(nested=True) + user = UserSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RackReservation + fields = [ + 'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description', + 'comments', 'tags', 'custom_fields', + ] + brief_fields = ('id', 'url', 'display', 'user', 'description', 'units') + + +class RackElevationDetailFilterSerializer(serializers.Serializer): + q = serializers.CharField( + required=False, + default=None + ) + face = serializers.ChoiceField( + choices=DeviceFaceChoices, + default=DeviceFaceChoices.FACE_FRONT + ) + render = serializers.ChoiceField( + choices=RackElevationDetailRenderChoices, + default=RackElevationDetailRenderChoices.RENDER_JSON + ) + unit_width = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH') + ) + unit_height = serializers.IntegerField( + default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT') + ) + legend_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH + ) + margin_width = serializers.IntegerField( + default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH + ) + exclude = serializers.IntegerField( + required=False, + default=None + ) + expand_devices = serializers.BooleanField( + required=False, + default=True + ) + include_images = serializers.BooleanField( + required=False, + default=True + ) + + +class ManufacturerSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail') + + # Related object counts + devicetype_count = RelatedObjectCountField('device_types') + inventoryitem_count = RelatedObjectCountField('inventory_items') + platform_count = RelatedObjectCountField('platforms') + + class Meta: + model = Manufacturer + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'devicetype_count', 'inventoryitem_count', 'platform_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count') + + +class DeviceRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail') + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = DeviceRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + + +class InventoryItemRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail') + + # Related object counts + inventoryitem_count = RelatedObjectCountField('inventory_items') + + class Meta: + model = InventoryItemRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'inventoryitem_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count') + + +class PlatformSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail') + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Related object counts + device_count = RelatedObjectCountField('devices') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Platform + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count') + + +# +# Device/module types +# + +class DeviceTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail') + manufacturer = ManufacturerSerializer(nested=True) + default_platform = PlatformSerializer(nested=True, required=False, allow_null=True) + u_height = serializers.DecimalField( + max_digits=4, + decimal_places=1, + label=_('Position (U)'), + min_value=0, + default=1.0 + ) + 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) + front_image = serializers.URLField(allow_null=True, required=False) + rear_image = serializers.URLField(allow_null=True, required=False) + + # Counter fields + console_port_template_count = serializers.IntegerField(read_only=True) + console_server_port_template_count = serializers.IntegerField(read_only=True) + power_port_template_count = serializers.IntegerField(read_only=True) + power_outlet_template_count = serializers.IntegerField(read_only=True) + interface_template_count = serializers.IntegerField(read_only=True) + front_port_template_count = serializers.IntegerField(read_only=True) + rear_port_template_count = serializers.IntegerField(read_only=True) + device_bay_template_count = serializers.IntegerField(read_only=True) + module_bay_template_count = serializers.IntegerField(read_only=True) + inventory_item_template_count = serializers.IntegerField(read_only=True) + + # Related object counts + device_count = RelatedObjectCountField('instances') + + class Meta: + model = DeviceType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', + 'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', + 'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count', + 'power_outlet_template_count', 'interface_template_count', 'front_port_template_count', + 'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count', + 'inventory_item_template_count', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count') + + +class ModuleTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail') + manufacturer = ManufacturerSerializer(nested=True) + weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True) + + class Meta: + model = ModuleType + fields = [ + 'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description') + + +# +# Component templates +# + +class ConsolePortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsolePortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + + class Meta: + model = ConsoleServerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw', + 'allocated_draw', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class PowerOutletTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortTemplateSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutletTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InterfaceTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=InterfaceTypeChoices) + bridge = NestedInterfaceTemplateSerializer( + required=False, + 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 + ) + rf_role = ChoiceField( + choices=WirelessRoleChoices, + required=False, + allow_blank=True, + allow_null=True + ) + + class Meta: + model = InterfaceTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', + 'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class RearPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail') + device_type = DeviceTypeSerializer( + required=False, + nested=True, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions', + 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class FrontPortTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail') + device_type = DeviceTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + module_type = ModuleTypeSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = RearPortTemplateSerializer(nested=True) + + class Meta: + model = FrontPortTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ModuleBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = ModuleBayTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class DeviceBayTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + + class Meta: + model = DeviceBayTemplate + fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated'] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class InventoryItemTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail') + device_type = DeviceTypeSerializer( + nested=True + ) + parent = serializers.PrimaryKeyRelatedField( + queryset=InventoryItemTemplate.objects.all(), + allow_null=True, + default=None + ) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItemTemplate + fields = [ + 'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', + 'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data + + # # Virtual chassis # @@ -1245,14 +779,513 @@ class VirtualChassisSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') +# +# Devices +# + +class DeviceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') + device_type = DeviceTypeSerializer(nested=True) + role = DeviceRoleSerializer(nested=True) + device_role = DeviceRoleSerializer( + nested=True, + read_only=True, + help_text='Deprecated in v3.6 in favor of `role`.' + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True) + location = LocationSerializer(nested=True, required=False, allow_null=True, default=None) + rack = RackSerializer(nested=True, required=False, allow_null=True, default=None) + face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '') + position = serializers.DecimalField( + max_digits=4, + decimal_places=1, + allow_null=True, + label=_('Position (U)'), + min_value=decimal.Decimal(0.5), + default=None + ) + status = ChoiceField(choices=DeviceStatusChoices, required=False) + airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False) + primary_ip = NestedIPAddressSerializer(read_only=True) + primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) + primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) + oob_ip = NestedIPAddressSerializer(required=False, allow_null=True) + parent_device = serializers.SerializerMethodField() + cluster = NestedClusterSerializer(required=False, allow_null=True) + virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None) + vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Counter fields + console_port_count = serializers.IntegerField(read_only=True) + console_server_port_count = serializers.IntegerField(read_only=True) + power_port_count = serializers.IntegerField(read_only=True) + power_outlet_count = serializers.IntegerField(read_only=True) + interface_count = serializers.IntegerField(read_only=True) + front_port_count = serializers.IntegerField(read_only=True) + rear_port_count = serializers.IntegerField(read_only=True) + device_bay_count = serializers.IntegerField(read_only=True) + module_bay_count = serializers.IntegerField(read_only=True) + inventory_item_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Device + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags', + 'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count', + 'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count', + 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(NestedDeviceSerializer) + def get_parent_device(self, obj): + try: + device_bay = obj.parent_bay + except DeviceBay.DoesNotExist: + return None + context = {'request': self.context['request']} + data = NestedDeviceSerializer(instance=device_bay.device, context=context).data + data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data + return data + + def get_device_role(self, obj): + return obj.role + + +class DeviceWithConfigContextSerializer(DeviceSerializer): + config_context = serializers.SerializerMethodField(read_only=True) + + class Meta(DeviceSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial', + 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', + 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', + 'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context', + 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count', + 'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count', + 'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_config_context(self, obj): + return obj.get_config_context() + + +class VirtualDeviceContextSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail') + device = DeviceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None) + 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) + status = ChoiceField(choices=VirtualDeviceContextStatusChoices) + + # Related object counts + interface_count = RelatedObjectCountField('interfaces') + + class Meta: + model = VirtualDeviceContext + fields = [ + 'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4', + 'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'interface_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description') + + +class ModuleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail') + device = DeviceSerializer(nested=True) + module_bay = NestedModuleBaySerializer() + module_type = ModuleTypeSerializer(nested=True) + status = ChoiceField(choices=ModuleStatusChoices, required=False) + + class Meta: + model = Module + fields = [ + 'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description') + + +# +# Device components +# + +class ConnectedEndpointsSerializer(serializers.ModelSerializer): + """ + Legacy serializer for pre-v3.3 connections + """ + connected_endpoints_type = serializers.SerializerMethodField(read_only=True) + 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}' + + @extend_schema_field(serializers.ListField) + def get_connected_endpoints(self, obj): + """ + Return the appropriate serializer for the type of connected object. + """ + if endpoints := obj.connected_endpoints: + serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(endpoints, many=True, context=context).data + + @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 + + +class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsoleServerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=ConsolePortTypeChoices, + allow_blank=True, + required=False + ) + speed = ChoiceField( + choices=ConsolePortSpeedChoices, + allow_null=True, + required=False + ) + + class Meta: + model = ConsolePort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerPortTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField( + choices=PowerOutletTypeChoices, + allow_blank=True, + required=False, + allow_null=True + ) + power_port = PowerPortSerializer( + nested=True, + required=False, + allow_null=True + ) + feed_leg = ChoiceField( + choices=PowerOutletFeedLegChoices, + allow_blank=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerOutlet + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg', + 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', + 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail') + device = DeviceSerializer(nested=True) + vdcs = SerializedPKRelatedField( + queryset=VirtualDeviceContext.objects.all(), + serializer=NestedVirtualDeviceContextSerializer, + required=False, + many=True + ) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=InterfaceTypeChoices) + 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, allow_null=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) + untagged_vlan = NestedVLANSerializer(required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=NestedVLANSerializer, + required=False, + many=True + ) + vrf = NestedVRFSerializer(required=False, allow_null=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, + required=False, + many=True + ) + count_ipaddresses = serializers.IntegerField(read_only=True) + count_fhrp_groups = serializers.IntegerField(read_only=True) + mac_address = serializers.CharField( + required=False, + default=None, + allow_blank=True, + allow_null=True + ) + wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True) + + class Meta: + model = Interface + fields = [ + 'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge', + 'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role', + 'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', + 'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers', + 'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints', + 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created', + 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + def validate(self, data): + + # Validate many-to-many VLAN assignments + if not self.nested: + device = self.instance.device if self.instance else data.get('device') + for vlan in data.get('tagged_vlans', []): + if vlan.site not in [device.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, " + f"or it must be global." + }) + + return super().validate(data) + + +class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + + class Meta: + model = RearPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description', + 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created', + 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class FrontPortRearPortSerializer(WritableNestedSerializer): + """ + NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device) + """ + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') + + class Meta: + model = RearPort + fields = ['id', 'url', 'display', 'name', 'label', 'description'] + + +class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'device', 'module_bay'), + required=False, + allow_null=True + ) + type = ChoiceField(choices=PortTypeChoices) + rear_port = FrontPortRearPortSerializer() + + class Meta: + model = FrontPort + fields = [ + 'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port', + 'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', + 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied') + + +class ModuleBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail') + device = DeviceSerializer(nested=True) + installed_module = ModuleSerializer( + nested=True, + requested_fields=('id', 'url', 'display', 'serial', 'description'), + required=False, + allow_null=True + ) + + class Meta: + model = ModuleBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description') + + +class DeviceBaySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') + device = DeviceSerializer(nested=True) + installed_device = DeviceSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = DeviceBay + fields = [ + 'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description') + + +class InventoryItemSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') + device = DeviceSerializer(nested=True) + parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None) + role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True) + manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None) + component_type = ContentTypeField( + queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS), + required=False, + allow_null=True + ) + component = serializers.SerializerMethodField(read_only=True) + _depth = serializers.IntegerField(source='level', read_only=True) + + class Meta: + model = InventoryItem + fields = [ + 'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', + 'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags', + 'custom_fields', 'created', 'last_updated', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_component(self, obj): + if obj.component is None: + return None + serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX) + context = {'request': self.context['request']} + return serializer(obj.component, context=context).data + + # # Power panels # class PowerPanelSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') - site = NestedSiteSerializer() - location = NestedLocationSerializer( + site = SiteSerializer(nested=True) + location = LocationSerializer( + nested=True, required=False, allow_null=True, default=None @@ -1272,8 +1305,9 @@ class PowerPanelSerializer(NetBoxModelSerializer): class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') - power_panel = NestedPowerPanelSerializer() - rack = NestedRackSerializer( + power_panel = PowerPanelSerializer(nested=True) + rack = RackSerializer( + nested=True, required=False, allow_null=True, default=None @@ -1294,7 +1328,8 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect choices=PowerFeedPhaseChoices, default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, ) - tenant = NestedTenantSerializer( + tenant = TenantSerializer( + nested=True, required=False, allow_null=True ) @@ -1308,3 +1343,23 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', ] brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') + + +class RackUnitSerializer(serializers.Serializer): + """ + A rack unit is an abstraction formed by the set (rack, position, face); it does not exist as a row in the database. + """ + id = serializers.DecimalField( + max_digits=4, + decimal_places=1, + read_only=True + ) + name = serializers.CharField(read_only=True) + face = ChoiceField(choices=DeviceFaceChoices, read_only=True) + device = DeviceSerializer(nested=True, read_only=True) + 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'] diff --git a/netbox/extras/api/mixins.py b/netbox/extras/api/mixins.py index 1737ff9f8..aafdf32d4 100644 --- a/netbox/extras/api/mixins.py +++ b/netbox/extras/api/mixins.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST from netbox.api.renderers import TextRenderer -from .nested_serializers import NestedConfigTemplateSerializer +from .serializers import ConfigTemplateSerializer __all__ = ( 'ConfigContextQuerySetMixin', @@ -52,7 +52,7 @@ class ConfigTemplateRenderMixin: if request.accepted_renderer.format == 'txt': return Response(output) - template_serializer = NestedConfigTemplateSerializer(configtemplate, context={'request': request}) + template_serializer = ConfigTemplateSerializer(configtemplate, nested=True, context={'request': request}) return Response({ 'configtemplate': template_serializer.data, diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 7dad95263..4c8c15159 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -5,8 +5,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from core.api.nested_serializers import NestedDataSourceSerializer, NestedDataFileSerializer, NestedJobSerializer -from core.api.serializers import JobSerializer +from core.api.serializers import DataFileSerializer, DataSourceSerializer, JobSerializer from core.models import ContentType from dcim.api.nested_serializers import ( NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, @@ -22,7 +21,7 @@ from netbox.api.serializers.features import TaggableModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup -from users.api.nested_serializers import NestedUserSerializer +from users.api.serializers import UserSerializer from utilities.api import get_serializer_for_model from virtualization.api.nested_serializers import ( NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, @@ -115,6 +114,28 @@ class WebhookSerializer(NetBoxModelSerializer): # Custom fields # +class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') + base_choices = ChoiceField( + choices=CustomFieldChoiceSetBaseChoices, + required=False + ) + extra_choices = serializers.ListField( + child=serializers.ListField( + min_length=2, + max_length=2 + ) + ) + + class Meta: + model = CustomFieldChoiceSet + fields = [ + 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', + 'choices_count', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') + + class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') content_types = ContentTypeField( @@ -129,7 +150,8 @@ class CustomFieldSerializer(ValidatedModelSerializer): ) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) data_type = serializers.SerializerMethodField() - choice_set = NestedCustomFieldChoiceSetSerializer( + choice_set = CustomFieldChoiceSetSerializer( + nested=True, required=False, allow_null=True ) @@ -168,28 +190,6 @@ class CustomFieldSerializer(ValidatedModelSerializer): return 'string' -class CustomFieldChoiceSetSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail') - base_choices = ChoiceField( - choices=CustomFieldChoiceSetBaseChoices, - required=False - ) - extra_choices = serializers.ListField( - child=serializers.ListField( - min_length=2, - max_length=2 - ) - ) - - class Meta: - model = CustomFieldChoiceSet - fields = [ - 'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically', - 'choices_count', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count') - - # # Custom links # @@ -220,10 +220,12 @@ class ExportTemplateSerializer(ValidatedModelSerializer): queryset=ContentType.objects.with_feature('export_templates'), many=True ) - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, read_only=True ) @@ -267,7 +269,7 @@ class BookmarkSerializer(ValidatedModelSerializer): queryset=ContentType.objects.with_feature('bookmarks'), ) object = serializers.SerializerMethodField(read_only=True) - user = NestedUserSerializer() + user = UserSerializer(nested=True) class Meta: model = Bookmark @@ -482,10 +484,12 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, read_only=True ) @@ -506,10 +510,12 @@ class ConfigContextSerializer(ValidatedModelSerializer): class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') - data_source = NestedDataSourceSerializer( + data_source = DataSourceSerializer( + nested=True, required=False ) - data_file = NestedDataFileSerializer( + data_file = DataFileSerializer( + nested=True, required=False ) @@ -530,7 +536,7 @@ class ScriptSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:script-detail') description = serializers.SerializerMethodField(read_only=True) vars = serializers.SerializerMethodField(read_only=True) - result = NestedJobSerializer(read_only=True) + result = JobSerializer(nested=True, read_only=True) class Meta: model = Script @@ -596,7 +602,8 @@ class ScriptInputSerializer(serializers.Serializer): class ObjectChangeSerializer(BaseModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) action = ChoiceField( diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a9c0ce7cb..c4b1b1799 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -2,29 +2,48 @@ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from dcim.api.nested_serializers import NestedDeviceSerializer, NestedSiteSerializer +from dcim.api.serializers import DeviceSerializer, SiteSerializer from ipam.choices import * from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES from ipam.models import * from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedVirtualMachineSerializer +from virtualization.api.serializers import VirtualMachineSerializer from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer from .field_serializers import IPAddressField, IPNetworkField from .nested_serializers import * +# +# RIRs +# + +class RIRSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') + + # Related object counts + aggregate_count = RelatedObjectCountField('aggregates') + + class Meta: + model = RIR + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'aggregate_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') + + # # ASN ranges # class ASNRangeSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') - rir = NestedRIRSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) asn_count = serializers.IntegerField(read_only=True) class Meta: @@ -42,8 +61,8 @@ class ASNRangeSerializer(NetBoxModelSerializer): class ASNSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') - rir = NestedRIRSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + rir = RIRSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) # Related object counts site_count = RelatedObjectCountField('sites') @@ -66,7 +85,7 @@ class AvailableASNSerializer(serializers.Serializer): description = serializers.CharField(required=False) def to_representation(self, asn): - rir = NestedRIRSerializer(self.context['range'].rir, context={ + rir = RIRSerializer(self.context['range'].rir, nested=True, context={ 'request': self.context['request'] }).data return { @@ -81,7 +100,7 @@ class AvailableASNSerializer(serializers.Serializer): class VRFSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) import_targets = SerializedPKRelatedField( queryset=RouteTarget.objects.all(), serializer=NestedRouteTargetSerializer, @@ -109,13 +128,9 @@ class VRFSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') -# -# Route targets -# - class RouteTargetSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = RouteTarget @@ -127,29 +142,14 @@ class RouteTargetSerializer(NetBoxModelSerializer): # -# RIRs/aggregates +# Aggregates # -class RIRSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail') - - # Related object counts - aggregate_count = RelatedObjectCountField('aggregates') - - class Meta: - model = RIR - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'aggregate_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count') - - class AggregateSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - rir = NestedRIRSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) prefix = IPNetworkField() class Meta: @@ -180,7 +180,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer): class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') - group = NestedFHRPGroupSerializer() + group = FHRPGroupSerializer(nested=True) interface_type = ContentTypeField( queryset=ContentType.objects.all() ) @@ -261,11 +261,11 @@ class VLANGroupSerializer(NetBoxModelSerializer): class VLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') - site = NestedSiteSerializer(required=False, allow_null=True) - group = NestedVLANGroupSerializer(required=False, allow_null=True, default=None) - tenant = NestedTenantSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + group = VLANGroupSerializer(nested=True, required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) # Related object counts @@ -285,23 +285,24 @@ class AvailableVLANSerializer(serializers.Serializer): Representation of a VLAN which does not exist in the database. """ vid = serializers.IntegerField(read_only=True) - group = NestedVLANGroupSerializer(read_only=True) + group = VLANGroupSerializer(nested=True, read_only=True) def to_representation(self, instance): return { 'vid': instance, - 'group': NestedVLANGroupSerializer( + 'group': VLANGroupSerializer( self.context['group'], + nested=True, context={'request': self.context['request']} ).data, } class CreateAvailableVLANSerializer(NetBoxModelSerializer): - site = NestedSiteSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) class Meta: model = VLAN @@ -321,12 +322,12 @@ class CreateAvailableVLANSerializer(NetBoxModelSerializer): class PrefixSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) - site = NestedSiteSerializer(required=False, allow_null=True) - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - vlan = NestedVLANSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=PrefixStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) children = serializers.IntegerField(read_only=True) _depth = serializers.IntegerField(read_only=True) prefix = IPNetworkField() @@ -374,11 +375,11 @@ class AvailablePrefixSerializer(serializers.Serializer): """ family = serializers.IntegerField(read_only=True) prefix = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) def to_representation(self, instance): if self.context.get('vrf'): - vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data else: vrf = None return { @@ -397,10 +398,10 @@ class IPRangeSerializer(NetBoxModelSerializer): family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) start_address = IPAddressField() end_address = IPAddressField() - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=IPRangeStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) + role = RoleSerializer(nested=True, required=False, allow_null=True) class Meta: model = IPRange @@ -420,8 +421,8 @@ class IPAddressSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) address = IPAddressField() - vrf = NestedVRFSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=IPAddressStatusChoices, required=False) role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False) assigned_object_type = ContentTypeField( @@ -457,12 +458,12 @@ class AvailableIPSerializer(serializers.Serializer): """ family = serializers.IntegerField(read_only=True) address = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) description = serializers.CharField(required=False) def to_representation(self, instance): if self.context.get('vrf'): - vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data else: vrf = None return { @@ -491,8 +492,8 @@ class ServiceTemplateSerializer(NetBoxModelSerializer): class ServiceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') - device = NestedDeviceSerializer(required=False, allow_null=True) - virtual_machine = NestedVirtualMachineSerializer(required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + virtual_machine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) ipaddresses = SerializedPKRelatedField( queryset=IPAddress.objects.all(), diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index c715b2d26..7fe68d4ef 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -1,8 +1,9 @@ -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 +from utilities.api import get_related_object_by_attrs + __all__ = ( 'BaseModelSerializer', 'ValidatedModelSerializer', @@ -12,15 +13,30 @@ __all__ = ( class BaseModelSerializer(serializers.ModelSerializer): display = serializers.SerializerMethodField(read_only=True) - def __init__(self, *args, requested_fields=None, **kwargs): + def __init__(self, *args, nested=False, requested_fields=None, **kwargs): super().__init__(*args, **kwargs) + self.nested = nested + + if nested and not requested_fields: + requested_fields = getattr(self.Meta, 'brief_fields', None) + # If specific fields have been requested, omit the others if requested_fields: for field in list(self.fields.keys()): if field not in requested_fields: self.fields.pop(field) + def to_internal_value(self, data): + + # If initialized as a nested serializer, we should expect to receive the attrs or PK + # identifying a related object. + if self.nested: + queryset = self.Meta.model.objects.all() + return get_related_object_by_attrs(queryset, data) + + return super().to_internal_value(data) + @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): return str(obj) @@ -32,6 +48,11 @@ class ValidatedModelSerializer(BaseModelSerializer): validation. (DRF does not do this by default; see https://github.com/encode/django-rest-framework/issues/3144) """ def validate(self, data): + + # Skip validation if we're being used to represent a nested object + if self.nested: + return data + attrs = data.copy() # Remove custom field data (if any) prior to model validation diff --git a/netbox/netbox/api/serializers/nested.py b/netbox/netbox/api/serializers/nested.py index 027f3d11e..e43fd7428 100644 --- a/netbox/netbox/api/serializers/nested.py +++ b/netbox/netbox/api/serializers/nested.py @@ -1,10 +1,7 @@ -from django.core.exceptions import FieldError, MultipleObjectsReturned, ObjectDoesNotExist -from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError from extras.models import Tag -from utilities.utils import dict_to_filter_params +from utilities.api import get_related_object_by_attrs from .base import BaseModelSerializer __all__ = ( @@ -20,43 +17,8 @@ class WritableNestedSerializer(BaseModelSerializer): subclassed to return a full representation of the related object on read. """ def to_internal_value(self, data): - - if data is None: - return None - - # Dictionary of related object attributes - if isinstance(data, dict): - params = dict_to_filter_params(data) - queryset = self.Meta.model.objects - try: - return queryset.get(**params) - except ObjectDoesNotExist: - raise ValidationError( - _("Related object not found using the provided attributes: {params}").format(params=params)) - except MultipleObjectsReturned: - raise ValidationError( - _("Multiple objects match the provided attributes: {params}").format(params=params) - ) - except FieldError as e: - raise ValidationError(e) - - # Integer PK of related object - try: - # Cast as integer in case a PK was mistakenly sent as a string - pk = int(data) - except (TypeError, ValueError): - raise ValidationError( - _( - "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " - "unrecognized value: {value}" - ).format(value=data) - ) - - # Look up object by PK - try: - return self.Meta.model.objects.get(pk=pk) - except ObjectDoesNotExist: - raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk)) + queryset = self.Meta.model.objects.all() + return get_related_object_by_attrs(queryset, data) # Declared here for use by PrimaryModelSerializer, but should be imported from extras.api.nested_serializers diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 325d3b439..103eb492b 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -32,7 +32,7 @@ class TenantGroupSerializer(NestedGroupModelSerializer): class TenantSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') - group = NestedTenantGroupSerializer(required=False, allow_null=True) + group = TenantGroupSerializer(nested=True, required=False, allow_null=True) # Related object counts circuit_count = RelatedObjectCountField('circuits') @@ -87,7 +87,7 @@ class ContactRoleSerializer(NetBoxModelSerializer): class ContactSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') - group = NestedContactGroupSerializer(required=False, allow_null=True, default=None) + group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None) class Meta: model = Contact @@ -104,8 +104,8 @@ class ContactAssignmentSerializer(NetBoxModelSerializer): queryset=ContentType.objects.all() ) object = serializers.SerializerMethodField(read_only=True) - contact = NestedContactSerializer() - role = NestedContactRoleSerializer(required=False, allow_null=True) + contact = ContactSerializer(nested=True) + role = ContactRoleSerializer(nested=True, required=False, allow_null=True) priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '') class Meta: diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index 0eef61dc8..49150f9c9 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -89,7 +89,7 @@ class TokenSerializer(ValidatedModelSerializer): required=False, write_only=not settings.ALLOW_TOKEN_RETRIEVAL ) - user = NestedUserSerializer() + user = UserSerializer(nested=True) allowed_ips = serializers.ListField( child=IPNetworkSerializer(), required=False, @@ -122,7 +122,8 @@ class TokenSerializer(ValidatedModelSerializer): class TokenProvisionSerializer(TokenSerializer): - user = NestedUserSerializer( + user = UserSerializer( + nested=True, read_only=True ) username = serializers.CharField( diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index a13e62bfd..1248ed296 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -3,23 +3,26 @@ import sys from django.conf import settings from django.contrib.contenttypes.fields import GenericForeignKey -from django.core.exceptions import FieldDoesNotExist +from django.core.exceptions import ( + FieldDoesNotExist, FieldError, MultipleObjectsReturned, ObjectDoesNotExist, ValidationError, +) from django.db.models.fields.related import ManyToOneRel, RelatedField from django.http import JsonResponse from django.urls import reverse +from django.utils.translation import gettext_lazy as _ from rest_framework import status from rest_framework.serializers import Serializer from rest_framework.utils import formatting from netbox.api.fields import RelatedObjectCountField from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound -from utilities.utils import count_related -from .utils import dynamic_import +from .utils import count_related, dict_to_filter_params, dynamic_import __all__ = ( 'get_annotations_for_serializer', 'get_graphql_type_for_model', 'get_prefetches_for_serializer', + 'get_related_object_by_attrs', 'get_serializer_for_model', 'get_view_name', 'is_api_request', @@ -103,7 +106,7 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None): """ model = serializer_class.Meta.model - # If specific fields are not specified, default to all + # If fields are not specified, default to all if not fields_to_include: fields_to_include = serializer_class.Meta.fields @@ -128,7 +131,9 @@ def get_prefetches_for_serializer(serializer_class, fields_to_include=None): # for the related object. if serializer_field: if issubclass(type(serializer_field), Serializer): - for subfield in get_prefetches_for_serializer(type(serializer_field)): + # Determine which fields to prefetch for the nested object + subfields = serializer_field.Meta.brief_fields if serializer_field.nested else None + for subfield in get_prefetches_for_serializer(type(serializer_field), subfields): prefetch_fields.append(f'{field_name}__{subfield}') return prefetch_fields @@ -154,6 +159,48 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None): return annotations +def get_related_object_by_attrs(queryset, attrs): + """ + Return an object identified by either a dictionary of attributes or its numeric primary key (ID). This is used + for referencing related objects when creating/updating objects via the REST API. + """ + if attrs is None: + return None + + # Dictionary of related object attributes + if isinstance(attrs, dict): + params = dict_to_filter_params(attrs) + try: + return queryset.get(**params) + except ObjectDoesNotExist: + raise ValidationError( + _("Related object not found using the provided attributes: {params}").format(params=params)) + except MultipleObjectsReturned: + raise ValidationError( + _("Multiple objects match the provided attributes: {params}").format(params=params) + ) + except FieldError as e: + raise ValidationError(e) + + # Integer PK of related object + try: + # Cast as integer in case a PK was mistakenly sent as a string + pk = int(attrs) + except (TypeError, ValueError): + raise ValidationError( + _( + "Related objects must be referenced by numeric ID or by dictionary of attributes. Received an " + "unrecognized value: {value}" + ).format(value=attrs) + ) + + # Look up object by PK + try: + return queryset.get(pk=pk) + except ObjectDoesNotExist: + raise ValidationError(_("Related object not found using the provided numeric ID: {id}").format(id=pk)) + + def rest_api_server_error(request, *args, **kwargs): """ Handle exceptions and return a useful error message for REST API requests. diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 4126c3f36..9f07f6f90 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,16 +1,14 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from dcim.api.nested_serializers import ( - NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer, -) +from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer from dcim.choices import InterfaceModeChoices -from extras.api.nested_serializers import NestedConfigTemplateSerializer +from extras.api.serializers import ConfigTemplateSerializer from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer from ipam.models import VLAN from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from virtualization.choices import * from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer @@ -53,11 +51,11 @@ class ClusterGroupSerializer(NetBoxModelSerializer): class ClusterSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') - type = NestedClusterTypeSerializer() - group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None) + type = ClusterTypeSerializer(nested=True) + group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) status = ChoiceField(choices=ClusterStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) - site = NestedSiteSerializer(required=False, allow_null=True, default=None) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True, default=None) # Related object counts device_count = RelatedObjectCountField('devices') @@ -79,16 +77,16 @@ class ClusterSerializer(NetBoxModelSerializer): class VirtualMachineSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) - site = NestedSiteSerializer(required=False, allow_null=True) - cluster = NestedClusterSerializer(required=False, allow_null=True) - device = NestedDeviceSerializer(required=False, allow_null=True) - role = NestedDeviceRoleSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - platform = NestedPlatformSerializer(required=False, allow_null=True) + site = SiteSerializer(nested=True, required=False, allow_null=True) + cluster = ClusterSerializer(nested=True, required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + role = DeviceRoleSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + platform = PlatformSerializer(nested=True, required=False, allow_null=True) primary_ip = NestedIPAddressSerializer(read_only=True) primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True) primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True) - config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) # Counter fields interface_count = serializers.IntegerField(read_only=True) @@ -128,7 +126,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): class VMInterfaceSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') - virtual_machine = NestedVirtualMachineSerializer() + virtual_machine = VirtualMachineSerializer(nested=True) parent = NestedVMInterfaceSerializer(required=False, allow_null=True) bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) @@ -178,7 +176,7 @@ class VMInterfaceSerializer(NetBoxModelSerializer): class VirtualDiskSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') - virtual_machine = NestedVirtualMachineSerializer() + virtual_machine = VirtualMachineSerializer(nested=True) class Meta: model = VirtualDisk diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index 53c1f898c..7ba374fc3 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -2,12 +2,13 @@ from django.contrib.contenttypes.models import ContentType from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedRouteTargetSerializer +from ipam.api.serializers import IPAddressSerializer +from ipam.api.nested_serializers import NestedRouteTargetSerializer from ipam.models import RouteTarget from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from utilities.api import get_serializer_for_model from vpn.choices import * from vpn.models import * @@ -27,90 +28,6 @@ __all__ = ( ) -class TunnelGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') - - # Related object counts - tunnel_count = RelatedObjectCountField('tunnels') - - class Meta: - model = TunnelGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'tunnel_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') - - -class TunnelSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunnel-detail' - ) - status = ChoiceField( - choices=TunnelStatusChoices - ) - group = NestedTunnelGroupSerializer( - required=False, - allow_null=True - ) - encapsulation = ChoiceField( - choices=TunnelEncapsulationChoices - ) - ipsec_profile = NestedIPSecProfileSerializer( - required=False, - allow_null=True - ) - tenant = NestedTenantSerializer( - required=False, - allow_null=True - ) - - # Related object counts - terminations_count = RelatedObjectCountField('terminations') - - class Meta: - model = Tunnel - fields = ( - 'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class TunnelTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:tunneltermination-detail' - ) - tunnel = NestedTunnelSerializer() - role = ChoiceField( - choices=TunnelTerminationRoleChoices - ) - termination_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - termination = serializers.SerializerMethodField( - read_only=True - ) - outside_ip = NestedIPAddressSerializer( - required=False, - allow_null=True - ) - - class Meta: - model = TunnelTermination - fields = ( - 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', - 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display') - - @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']} - return serializer(obj.termination, context=context).data - - class IKEProposalSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField( view_name='vpn-api:ikeproposal-detail' @@ -215,8 +132,12 @@ class IPSecProfileSerializer(NetBoxModelSerializer): mode = ChoiceField( choices=IPSecModeChoices ) - ike_policy = NestedIKEPolicySerializer() - ipsec_policy = NestedIPSecPolicySerializer() + ike_policy = IKEPolicySerializer( + nested=True + ) + ipsec_policy = IPSecPolicySerializer( + nested=True + ) class Meta: model = IPSecProfile @@ -227,6 +148,100 @@ class IPSecProfileSerializer(NetBoxModelSerializer): brief_fields = ('id', 'url', 'display', 'name', 'description') +# +# Tunnels +# + +class TunnelGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail') + + # Related object counts + tunnel_count = RelatedObjectCountField('tunnels') + + class Meta: + model = TunnelGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'tunnel_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count') + + +class TunnelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunnel-detail' + ) + status = ChoiceField( + choices=TunnelStatusChoices + ) + group = TunnelGroupSerializer( + nested=True, + required=False, + allow_null=True + ) + encapsulation = ChoiceField( + choices=TunnelEncapsulationChoices + ) + ipsec_profile = IPSecProfileSerializer( + nested=True, + required=False, + allow_null=True + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True + ) + + # Related object counts + terminations_count = RelatedObjectCountField('terminations') + + class Meta: + model = Tunnel + fields = ( + 'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class TunnelTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:tunneltermination-detail' + ) + tunnel = TunnelSerializer( + nested=True + ) + role = ChoiceField( + choices=TunnelTerminationRoleChoices + ) + termination_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + termination = serializers.SerializerMethodField( + read_only=True + ) + outside_ip = IPAddressSerializer( + nested=True, + required=False, + allow_null=True + ) + + class Meta: + model = TunnelTermination + fields = ( + 'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip', + 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display') + + @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']} + return serializer(obj.termination, context=context).data + + # # L2VPN # @@ -246,7 +261,7 @@ class L2VPNSerializer(NetBoxModelSerializer): required=False, many=True ) - tenant = NestedTenantSerializer(required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) class Meta: model = L2VPN @@ -259,7 +274,9 @@ class L2VPNSerializer(NetBoxModelSerializer): class L2VPNTerminationSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') - l2vpn = NestedL2VPNSerializer() + l2vpn = L2VPNSerializer( + nested=True + ) assigned_object_type = ContentTypeField( queryset=ContentType.objects.all() ) diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 0aec97fd2..94fd0a94b 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,11 +1,11 @@ from rest_framework import serializers from dcim.choices import LinkStatusChoices -from dcim.api.serializers import NestedInterfaceSerializer -from ipam.api.serializers import NestedVLANSerializer +from dcim.api.serializers import InterfaceSerializer +from ipam.api.serializers import VLANSerializer from netbox.api.fields import ChoiceField from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer +from tenancy.api.serializers import TenantSerializer from wireless.choices import * from wireless.models import * from .nested_serializers import * @@ -33,10 +33,10 @@ class WirelessLANGroupSerializer(NestedGroupModelSerializer): class WirelessLANSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') - group = NestedWirelessLANGroupSerializer(required=False, allow_null=True) + group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True) - vlan = NestedVLANSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) + vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) @@ -52,9 +52,9 @@ class WirelessLANSerializer(NetBoxModelSerializer): class WirelessLinkSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') status = ChoiceField(choices=LinkStatusChoices, required=False) - interface_a = NestedInterfaceSerializer() - interface_b = NestedInterfaceSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) + interface_a = InterfaceSerializer(nested=True) + interface_b = InterfaceSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)