diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 574924c4a..5e048218c 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -1,145 +1,3 @@ -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 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 .serializers_.providers import * +from .serializers_.circuits import * from .nested_serializers import * - - -# -# Providers -# - -class ProviderSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') - accounts = SerializedPKRelatedField( - queryset=ProviderAccount.objects.all(), - serializer=NestedProviderAccountSerializer, - required=False, - many=True - ) - asns = SerializedPKRelatedField( - queryset=ASN.objects.all(), - serializer=NestedASNSerializer, - required=False, - many=True - ) - - # Related object counts - circuit_count = RelatedObjectCountField('circuits') - - class Meta: - model = Provider - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', - 'custom_fields', 'created', 'last_updated', 'circuit_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') - - -# -# Provider Accounts -# - -class ProviderAccountSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') - provider = NestedProviderSerializer() - - class Meta: - model = ProviderAccount - fields = [ - 'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') - - -# -# Provider networks -# - -class ProviderNetworkSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') - provider = NestedProviderSerializer() - - class Meta: - model = ProviderNetwork - fields = [ - 'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Circuits -# - -class CircuitTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') - - # Related object counts - circuit_count = RelatedObjectCountField('circuits') - - class Meta: - model = CircuitType - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'circuit_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') - - -class CircuitCircuitTerminationSerializer(WritableNestedSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') - site = NestedSiteSerializer(allow_null=True) - provider_network = NestedProviderNetworkSerializer(allow_null=True) - - class Meta: - model = CircuitTermination - fields = [ - 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', - 'description', - ] - - -class CircuitSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') - provider = NestedProviderSerializer() - provider_account = NestedProviderAccountSerializer(required=False, allow_null=True) - status = ChoiceField(choices=CircuitStatusChoices, required=False) - type = NestedCircuitTypeSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) - termination_a = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) - termination_z = CircuitCircuitTerminationSerializer(read_only=True, allow_null=True) - - class Meta: - model = Circuit - fields = [ - 'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', - 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'cid', 'description') - - -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) - - class Meta: - model = CircuitTermination - fields = [ - 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', - 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', - 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') diff --git a/netbox/circuits/api/serializers_/__init__.py b/netbox/circuits/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/circuits/api/serializers_/circuits.py b/netbox/circuits/api/serializers_/circuits.py new file mode 100644 index 000000000..b59c73f09 --- /dev/null +++ b/netbox/circuits/api/serializers_/circuits.py @@ -0,0 +1,81 @@ +from rest_framework import serializers + +from circuits.choices import CircuitStatusChoices +from circuits.models import Circuit, CircuitTermination, CircuitType +from dcim.api.serializers_.cables import CabledObjectSerializer +from dcim.api.serializers_.sites import SiteSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from tenancy.api.serializers_.tenants import TenantSerializer + +from .providers import ProviderAccountSerializer, ProviderNetworkSerializer, ProviderSerializer + +__all__ = ( + 'CircuitSerializer', + 'CircuitTerminationSerializer', + 'CircuitTypeSerializer', +) + + +class CircuitTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittype-detail') + + # Related object counts + circuit_count = RelatedObjectCountField('circuits') + + class Meta: + model = CircuitType + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'circuit_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') + + +class CircuitCircuitTerminationSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + site = SiteSerializer(nested=True, allow_null=True) + provider_network = ProviderNetworkSerializer(nested=True, allow_null=True) + + class Meta: + model = CircuitTermination + fields = [ + 'id', 'url', 'display', 'site', 'provider_network', 'port_speed', 'upstream_speed', 'xconnect_id', + 'description', + ] + + +class CircuitSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuit-detail') + provider = ProviderSerializer(nested=True) + provider_account = ProviderAccountSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=CircuitStatusChoices, required=False) + 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) + + class Meta: + model = Circuit + fields = [ + 'id', 'url', 'display', 'cid', 'provider', 'provider_account', 'type', 'status', 'tenant', 'install_date', + 'termination_date', 'commit_rate', 'description', 'termination_a', 'termination_z', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'cid', 'description') + + +class CircuitTerminationSerializer(NetBoxModelSerializer, CabledObjectSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:circuittermination-detail') + 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 + fields = [ + 'id', 'url', 'display', 'circuit', 'term_side', 'site', 'provider_network', 'port_speed', 'upstream_speed', + 'xconnect_id', 'pp_info', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', + 'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'circuit', 'term_side', 'description', 'cable', '_occupied') diff --git a/netbox/circuits/api/serializers_/providers.py b/netbox/circuits/api/serializers_/providers.py new file mode 100644 index 000000000..302c2da5a --- /dev/null +++ b/netbox/circuits/api/serializers_/providers.py @@ -0,0 +1,68 @@ +from rest_framework import serializers + +from circuits.models import Provider, ProviderAccount, ProviderNetwork +from ipam.api.serializers_.asns import ASNSerializer +from ipam.models import ASN +from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from ..nested_serializers import * + +__all__ = ( + 'ProviderAccountSerializer', + 'ProviderNetworkSerializer', + 'ProviderSerializer', +) + + +class ProviderSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') + accounts = SerializedPKRelatedField( + queryset=ProviderAccount.objects.all(), + serializer=NestedProviderAccountSerializer, + required=False, + many=True + ) + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=ASNSerializer, + nested=True, + required=False, + many=True + ) + + # Related object counts + circuit_count = RelatedObjectCountField('circuits') + + class Meta: + model = Provider + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'accounts', 'description', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'circuit_count') + + +class ProviderAccountSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provideraccount-detail') + provider = ProviderSerializer(nested=True) + + class Meta: + model = ProviderAccount + fields = [ + 'id', 'url', 'display', 'provider', 'name', 'account', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'account', 'description') + + +class ProviderNetworkSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') + provider = ProviderSerializer(nested=True) + + class Meta: + model = ProviderNetwork + fields = [ + 'id', 'url', 'display', 'provider', 'name', 'service_id', 'description', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') 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..8553bb91c 100644 --- a/netbox/core/api/serializers.py +++ b/netbox/core/api/serializers.py @@ -1,74 +1,3 @@ -from rest_framework import serializers - -from core.choices import * -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 .serializers_.data import * +from .serializers_.jobs import * from .nested_serializers import * - -__all__ = ( - 'DataFileSerializer', - 'DataSourceSerializer', - 'JobSerializer', -) - - -class DataSourceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='core-api:datasource-detail' - ) - type = ChoiceField( - choices=get_data_backend_choices() - ) - status = ChoiceField( - choices=DataSourceStatusChoices, - read_only=True - ) - - # Related object counts - file_count = RelatedObjectCountField('datafiles') - - class Meta: - model = DataSource - fields = [ - 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', - 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class DataFileSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='core-api:datafile-detail' - ) - source = NestedDataSourceSerializer( - read_only=True - ) - - class Meta: - model = DataFile - fields = [ - 'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash', - ] - brief_fields = ('id', 'url', 'display', 'path') - - -class JobSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') - user = NestedUserSerializer( - read_only=True - ) - status = ChoiceField(choices=JobStatusChoices, read_only=True) - object_type = ContentTypeField( - read_only=True - ) - - class Meta: - model = Job - fields = [ - 'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval', - 'started', 'completed', 'user', 'data', 'error', 'job_id', - ] - brief_fields = ('url', 'created', 'completed', 'user', 'status') diff --git a/netbox/core/api/serializers_/__init__.py b/netbox/core/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/core/api/serializers_/data.py b/netbox/core/api/serializers_/data.py new file mode 100644 index 000000000..e60d8d95b --- /dev/null +++ b/netbox/core/api/serializers_/data.py @@ -0,0 +1,53 @@ +from rest_framework import serializers + +from core.choices import * +from core.models import DataFile, DataSource +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.utils import get_data_backend_choices + +__all__ = ( + 'DataFileSerializer', + 'DataSourceSerializer', +) + + +class DataSourceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datasource-detail' + ) + type = ChoiceField( + choices=get_data_backend_choices() + ) + status = ChoiceField( + choices=DataSourceStatusChoices, + read_only=True + ) + + # Related object counts + file_count = RelatedObjectCountField('datafiles') + + class Meta: + model = DataSource + fields = [ + 'id', 'url', 'display', 'name', 'type', 'source_url', 'enabled', 'status', 'description', 'comments', + 'parameters', 'ignore_rules', 'custom_fields', 'created', 'last_updated', 'file_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class DataFileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='core-api:datafile-detail' + ) + source = DataSourceSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = DataFile + fields = [ + 'id', 'url', 'display', 'source', 'path', 'last_updated', 'size', 'hash', + ] + brief_fields = ('id', 'url', 'display', 'path') diff --git a/netbox/core/api/serializers_/jobs.py b/netbox/core/api/serializers_/jobs.py new file mode 100644 index 000000000..84afa3627 --- /dev/null +++ b/netbox/core/api/serializers_/jobs.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from core.choices import * +from core.models import Job +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import BaseModelSerializer +from users.api.serializers_.users import UserSerializer + +__all__ = ( + 'JobSerializer', +) + + +class JobSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='core-api:job-detail') + user = UserSerializer( + nested=True, + read_only=True + ) + status = ChoiceField(choices=JobStatusChoices, read_only=True) + object_type = ContentTypeField( + read_only=True + ) + + class Meta: + model = Job + fields = [ + 'id', 'url', 'display', 'object_type', 'object_id', 'name', 'status', 'created', 'scheduled', 'interval', + 'started', 'completed', 'user', 'data', 'error', 'job_id', + ] + brief_fields = ('url', 'created', 'completed', 'user', 'status') 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 8fbe9fd04..4f8bbac17 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -1,1306 +1,14 @@ -import decimal - -from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext as _ -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers -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 ipam.api.nested_serializers import ( - NestedASNSerializer, NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer, -) -from ipam.models import ASN, VLAN -from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import ( - GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, - WritableNestedSerializer, -) -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 utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedClusterSerializer -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer -from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer -from wireless.choices import * -from wireless.models import WirelessLAN +from .serializers_.cables import * +from .serializers_.sites import * +from .serializers_.racks import * +from .serializers_.manufacturers import * +from .serializers_.platforms import * +from .serializers_.roles import * +from .serializers_.devicetypes import * +from .serializers_.devicetype_components import * +from .serializers_.virtualchassis import * +from .serializers_.devices import * +from .serializers_.device_components import * +from .serializers_.power import * +from .serializers_.rackunits import * 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() - 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', '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 - - -class DeviceWithConfigContextSerializer(DeviceSerializer): - config_context = serializers.SerializerMethodField(read_only=True) - - class Meta(DeviceSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'device_type', '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 -# - -class CableSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') - 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) - length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) - - class Meta: - model = Cable - fields = [ - 'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', - 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'label', 'description') - - -class TracedCableSerializer(serializers.ModelSerializer): - """ - Used only while tracing a cable path. - """ - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') - - class Meta: - model = Cable - fields = [ - 'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description', - ] - - -class CableTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail') - termination_type = ContentTypeField( - queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) - ) - termination = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = CableTermination - fields = [ - 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', - 'created', 'last_updated', - ] - - @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 CablePathSerializer(serializers.ModelSerializer): - path = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = CablePath - fields = ['id', 'path', 'is_active', 'is_complete', 'is_split'] - - @extend_schema_field(serializers.ListField) - def get_path(self, obj): - ret = [] - for nodes in obj.path_objects: - serializer = get_serializer_for_model(nodes[0], prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - ret.append(serializer(nodes, context=context, many=True).data) - return ret - - -# -# Virtual chassis -# - -class VirtualChassisSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') - master = NestedDeviceSerializer(required=False, allow_null=True, default=None) - - # Counter fields - member_count = serializers.IntegerField(read_only=True) - - class Meta: - model = VirtualChassis - fields = [ - 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'member_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') - - -# -# Power panels -# - -class PowerPanelSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') - site = NestedSiteSerializer() - location = NestedLocationSerializer( - required=False, - allow_null=True, - default=None - ) - - # Related object counts - powerfeed_count = RelatedObjectCountField('powerfeeds') - - class Meta: - model = PowerPanel - fields = [ - 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields', - 'powerfeed_count', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') - - -class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): - url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') - power_panel = NestedPowerPanelSerializer() - rack = NestedRackSerializer( - required=False, - allow_null=True, - default=None - ) - type = ChoiceField( - choices=PowerFeedTypeChoices, - default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY, - ) - status = ChoiceField( - choices=PowerFeedStatusChoices, - default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE, - ) - supply = ChoiceField( - choices=PowerFeedSupplyChoices, - default=lambda: PowerFeedSupplyChoices.SUPPLY_AC, - ) - phase = ChoiceField( - choices=PowerFeedPhaseChoices, - default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, - ) - tenant = NestedTenantSerializer( - required=False, - allow_null=True - ) - - class Meta: - model = PowerFeed - fields = [ - 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', - 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', - 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', - 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') diff --git a/netbox/dcim/api/serializers_/__init__.py b/netbox/dcim/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/dcim/api/serializers_/base.py b/netbox/dcim/api/serializers_/base.py new file mode 100644 index 000000000..48f4967e3 --- /dev/null +++ b/netbox/dcim/api/serializers_/base.py @@ -0,0 +1,37 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ConnectedEndpointsSerializer', +) + + +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]) + context = {'request': self.context['request']} + return serializer(endpoints, nested=True, 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 diff --git a/netbox/dcim/api/serializers_/cables.py b/netbox/dcim/api/serializers_/cables.py new file mode 100644 index 000000000..94a125d0c --- /dev/null +++ b/netbox/dcim/api/serializers_/cables.py @@ -0,0 +1,126 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import Cable, CablePath, CableTermination +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import GenericObjectSerializer, NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'CablePathSerializer', + 'CableSerializer', + 'CableTerminationSerializer', + 'CabledObjectSerializer', + 'TracedCableSerializer', +) + + +class CableSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') + a_terminations = GenericObjectSerializer(many=True, required=False) + b_terminations = GenericObjectSerializer(many=True, required=False) + status = ChoiceField(choices=LinkStatusChoices, required=False) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False, allow_null=True) + + class Meta: + model = Cable + fields = [ + 'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color', + 'length', 'length_unit', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'label', 'description') + + +class TracedCableSerializer(serializers.ModelSerializer): + """ + Used only while tracing a cable path. + """ + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') + + class Meta: + model = Cable + fields = [ + 'id', 'url', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'description', + ] + + +class CableTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cabletermination-detail') + termination_type = ContentTypeField( + queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) + ) + termination = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = CableTermination + fields = [ + 'id', 'url', 'display', 'cable', 'cable_end', 'termination_type', 'termination_id', 'termination', + 'created', 'last_updated', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_termination(self, obj): + serializer = get_serializer_for_model(obj.termination) + context = {'request': self.context['request']} + return serializer(obj.termination, nested=True, context=context).data + + +class CablePathSerializer(serializers.ModelSerializer): + path = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = CablePath + fields = ['id', 'path', 'is_active', 'is_complete', 'is_split'] + + @extend_schema_field(serializers.ListField) + def get_path(self, obj): + ret = [] + for nodes in obj.path_objects: + serializer = get_serializer_for_model(nodes[0]) + context = {'request': self.context['request']} + ret.append(serializer(nodes, nested=True, many=True, context=context).data) + 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]) + context = {'request': self.context['request']} + return serializer(obj.link_peers, nested=True, many=True, context=context).data + + @extend_schema_field(serializers.BooleanField) + def get__occupied(self, obj): + return obj._occupied diff --git a/netbox/dcim/api/serializers_/device_components.py b/netbox/dcim/api/serializers_/device_components.py new file mode 100644 index 000000000..87d142978 --- /dev/null +++ b/netbox/dcim/api/serializers_/device_components.py @@ -0,0 +1,368 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import ( + ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort, + RearPort, VirtualDeviceContext, +) +from ipam.api.serializers_.vlans import VLANSerializer +from ipam.api.serializers_.vrfs import VRFSerializer +from ipam.models import VLAN +from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer +from utilities.api import get_serializer_for_model +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer +from wireless.api.nested_serializers import NestedWirelessLinkSerializer +from wireless.api.serializers_.wirelesslans import WirelessLANSerializer +from wireless.choices import * +from wireless.models import WirelessLAN +from .base import ConnectedEndpointsSerializer +from .cables import CabledObjectSerializer +from .devices import DeviceSerializer, ModuleSerializer, VirtualDeviceContextSerializer +from .manufacturers import ManufacturerSerializer +from .roles import InventoryItemRoleSerializer +from ..nested_serializers import * + +__all__ = ( + 'ConsolePortSerializer', + 'ConsoleServerPortSerializer', + 'DeviceBaySerializer', + 'FrontPortSerializer', + 'InterfaceSerializer', + 'InventoryItemSerializer', + 'ModuleBaySerializer', + 'PowerOutletSerializer', + 'PowerPortSerializer', + 'RearPortSerializer', +) + + +class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail') + device = DeviceSerializer(nested=True) + module = ModuleSerializer( + nested=True, + 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, + 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, + 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, + 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=VirtualDeviceContextSerializer, + nested=True, + required=False, + many=True + ) + module = ModuleSerializer( + nested=True, + 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 = VLANSerializer(nested=True, required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=VLANSerializer, + nested=True, + required=False, + many=True + ) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True) + wireless_lans = SerializedPKRelatedField( + queryset=WirelessLAN.objects.all(), + serializer=WirelessLANSerializer, + nested=True, + 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, + 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, + 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, + 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) + context = {'request': self.context['request']} + return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/dcim/api/serializers_/devices.py b/netbox/dcim/api/serializers_/devices.py new file mode 100644 index 000000000..303c35532 --- /dev/null +++ b/netbox/dcim/api/serializers_/devices.py @@ -0,0 +1,157 @@ +import decimal + +from django.utils.translation import gettext as _ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import Device, DeviceBay, Module, VirtualDeviceContext +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer +from ipam.api.serializers_.ip import IPAddressSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.api.serializers_.clusters import ClusterSerializer +from .devicetypes import * +from .platforms import PlatformSerializer +from .racks import RackSerializer +from .roles import DeviceRoleSerializer +from .sites import LocationSerializer, SiteSerializer +from .virtualchassis import VirtualChassisSerializer +from ..nested_serializers import * + +__all__ = ( + 'DeviceSerializer', + 'DeviceWithConfigContextSerializer', + 'ModuleSerializer', + 'VirtualDeviceContextSerializer', +) + + +class DeviceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail') + device_type = DeviceTypeSerializer(nested=True) + role = DeviceRoleSerializer(nested=True) + 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 = IPAddressSerializer(nested=True, read_only=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True) + oob_ip = IPAddressSerializer(nested=True, required=False, allow_null=True) + parent_device = serializers.SerializerMethodField() + cluster = ClusterSerializer(nested=True, 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', '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 + + +class DeviceWithConfigContextSerializer(DeviceSerializer): + config_context = serializers.SerializerMethodField(read_only=True) + + class Meta(DeviceSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'device_type', '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 = IPAddressSerializer(nested=True, read_only=True, allow_null=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, 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') diff --git a/netbox/dcim/api/serializers_/devicetype_components.py b/netbox/dcim/api/serializers_/devicetype_components.py new file mode 100644 index 000000000..259a5df27 --- /dev/null +++ b/netbox/dcim/api/serializers_/devicetype_components.py @@ -0,0 +1,327 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import ( + ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate, + InventoryItemTemplate, ModuleBayTemplate, PowerOutletTemplate, PowerPortTemplate, RearPortTemplate, +) +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from utilities.api import get_serializer_for_model +from wireless.choices import * +from .devicetypes import DeviceTypeSerializer, ModuleTypeSerializer +from .manufacturers import ManufacturerSerializer +from .roles import InventoryItemRoleSerializer +from ..nested_serializers import * + +__all__ = ( + 'ConsolePortTemplateSerializer', + 'ConsoleServerPortTemplateSerializer', + 'DeviceBayTemplateSerializer', + 'FrontPortTemplateSerializer', + 'InterfaceTemplateSerializer', + 'InventoryItemTemplateSerializer', + 'ModuleBayTemplateSerializer', + 'PowerOutletTemplateSerializer', + 'PowerPortTemplateSerializer', + 'RearPortTemplateSerializer', +) + + +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) + context = {'request': self.context['request']} + return serializer(obj.component, nested=True, context=context).data diff --git a/netbox/dcim/api/serializers_/devicetypes.py b/netbox/dcim/api/serializers_/devicetypes.py new file mode 100644 index 000000000..2384f7b02 --- /dev/null +++ b/netbox/dcim/api/serializers_/devicetypes.py @@ -0,0 +1,74 @@ +from django.utils.translation import gettext as _ +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import DeviceType, ModuleType +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from .manufacturers import ManufacturerSerializer +from .platforms import PlatformSerializer + +__all__ = ( + 'DeviceTypeSerializer', + 'ModuleTypeSerializer', +) + + +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') diff --git a/netbox/dcim/api/serializers_/manufacturers.py b/netbox/dcim/api/serializers_/manufacturers.py new file mode 100644 index 000000000..fd50fe97d --- /dev/null +++ b/netbox/dcim/api/serializers_/manufacturers.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from dcim.models import Manufacturer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'ManufacturerSerializer', +) + + +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') diff --git a/netbox/dcim/api/serializers_/platforms.py b/netbox/dcim/api/serializers_/platforms.py new file mode 100644 index 000000000..7365404eb --- /dev/null +++ b/netbox/dcim/api/serializers_/platforms.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from dcim.models import Platform +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from .manufacturers import ManufacturerSerializer + +__all__ = ( + 'PlatformSerializer', +) + + +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') diff --git a/netbox/dcim/api/serializers_/power.py b/netbox/dcim/api/serializers_/power.py new file mode 100644 index 000000000..dddd54906 --- /dev/null +++ b/netbox/dcim/api/serializers_/power.py @@ -0,0 +1,80 @@ +from rest_framework import serializers + +from dcim.choices import * +from dcim.models import PowerFeed, PowerPanel +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from .base import ConnectedEndpointsSerializer +from .cables import CabledObjectSerializer +from .racks import RackSerializer +from .sites import LocationSerializer, SiteSerializer + +__all__ = ( + 'PowerFeedSerializer', + 'PowerPanelSerializer', +) + + +class PowerPanelSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') + site = SiteSerializer(nested=True) + location = LocationSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + + # Related object counts + powerfeed_count = RelatedObjectCountField('powerfeeds') + + class Meta: + model = PowerPanel + fields = [ + 'id', 'url', 'display', 'site', 'location', 'name', 'description', 'comments', 'tags', 'custom_fields', + 'powerfeed_count', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'powerfeed_count') + + +class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerfeed-detail') + power_panel = PowerPanelSerializer(nested=True) + rack = RackSerializer( + nested=True, + required=False, + allow_null=True, + default=None + ) + type = ChoiceField( + choices=PowerFeedTypeChoices, + default=lambda: PowerFeedTypeChoices.TYPE_PRIMARY, + ) + status = ChoiceField( + choices=PowerFeedStatusChoices, + default=lambda: PowerFeedStatusChoices.STATUS_ACTIVE, + ) + supply = ChoiceField( + choices=PowerFeedSupplyChoices, + default=lambda: PowerFeedSupplyChoices.SUPPLY_AC, + ) + phase = ChoiceField( + choices=PowerFeedPhaseChoices, + default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE, + ) + tenant = TenantSerializer( + nested=True, + required=False, + allow_null=True + ) + + class Meta: + model = PowerFeed + fields = [ + 'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage', + 'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', + 'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description', + 'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'cable', '_occupied') diff --git a/netbox/dcim/api/serializers_/racks.py b/netbox/dcim/api/serializers_/racks.py new file mode 100644 index 000000000..a6754cba0 --- /dev/null +++ b/netbox/dcim/api/serializers_/racks.py @@ -0,0 +1,117 @@ +from django.utils.translation import gettext as _ +from rest_framework import serializers + +from dcim.choices import * +from dcim.constants import * +from dcim.models import Rack, RackReservation, RackRole +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from netbox.config import ConfigItem +from tenancy.api.serializers_.tenants import TenantSerializer +from users.api.serializers_.users import UserSerializer +from .sites import LocationSerializer, SiteSerializer + +__all__ = ( + 'RackElevationDetailFilterSerializer', + 'RackReservationSerializer', + 'RackRoleSerializer', + 'RackSerializer', +) + + +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 + ) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py new file mode 100644 index 000000000..1f5306718 --- /dev/null +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -0,0 +1,31 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.choices import * +from netbox.api.fields import ChoiceField +from .devices import DeviceSerializer + +__all__ = ( + 'RackUnitSerializer', +) + + +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/dcim/api/serializers_/roles.py b/netbox/dcim/api/serializers_/roles.py new file mode 100644 index 000000000..41f8f377d --- /dev/null +++ b/netbox/dcim/api/serializers_/roles.py @@ -0,0 +1,43 @@ +from rest_framework import serializers + +from dcim.models import DeviceRole, InventoryItemRole +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'DeviceRoleSerializer', + 'InventoryItemRoleSerializer', +) + + +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') diff --git a/netbox/dcim/api/serializers_/sites.py b/netbox/dcim/api/serializers_/sites.py new file mode 100644 index 000000000..6fb3811ba --- /dev/null +++ b/netbox/dcim/api/serializers_/sites.py @@ -0,0 +1,98 @@ +from rest_framework import serializers +from timezone_field.rest_framework import TimeZoneSerializerField + +from dcim.choices import * +from dcim.models import Location, Region, Site, SiteGroup +from ipam.api.serializers_.asns import ASNSerializer +from ipam.models import ASN +from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from ..nested_serializers import * + +__all__ = ( + 'LocationSerializer', + 'RegionSerializer', + 'SiteGroupSerializer', + 'SiteSerializer', +) + + +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=ASNSerializer, + nested=True, + 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') + + +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') diff --git a/netbox/dcim/api/serializers_/virtualchassis.py b/netbox/dcim/api/serializers_/virtualchassis.py new file mode 100644 index 000000000..570abfc7d --- /dev/null +++ b/netbox/dcim/api/serializers_/virtualchassis.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from dcim.models import VirtualChassis +from netbox.api.serializers import NetBoxModelSerializer +from ..nested_serializers import * + +__all__ = ( + 'VirtualChassisSerializer', +) + + +class VirtualChassisSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') + master = NestedDeviceSerializer(required=False, allow_null=True, default=None) + + # Counter fields + member_count = serializers.IntegerField(read_only=True) + + class Meta: + model = VirtualChassis + fields = [ + 'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'member_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'master', 'description', 'member_count') diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 8a674656d..668af28da 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -7,7 +7,6 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.viewsets import ViewSet -from circuits.models import Circuit from dcim import filtersets from dcim.constants import CABLE_TRACE_SVG_DEFAULT_WIDTH from dcim.models import * @@ -18,10 +17,8 @@ from netbox.api.metadata import ContentTypeMetadata from netbox.api.pagination import StripCountAnnotationsPaginator from netbox.api.viewsets import NetBoxModelViewSet, MPTTLockedMixin from netbox.api.viewsets.mixins import SequentialBulkCreatesMixin -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from utilities.query_functions import CollateAsChar -from utilities.utils import count_related from . import serializers from .exceptions import MissingFilterException @@ -60,16 +57,16 @@ class PathEndpointMixin(object): # Serialize path objects, iterating over each three-tuple in the path for near_ends, cable, far_ends in obj.trace(): if near_ends: - serializer_a = get_serializer_for_model(near_ends[0], prefix=NESTED_SERIALIZER_PREFIX) - near_ends = serializer_a(near_ends, many=True, context={'request': request}).data + serializer_a = get_serializer_for_model(near_ends[0]) + near_ends = serializer_a(near_ends, nested=True, many=True, context={'request': request}).data else: # Path is split; stop here break if cable: cable = serializers.TracedCableSerializer(cable[0], context={'request': request}).data if far_ends: - serializer_b = get_serializer_for_model(far_ends[0], prefix=NESTED_SERIALIZER_PREFIX) - far_ends = serializer_b(far_ends, many=True, context={'request': request}).data + serializer_b = get_serializer_for_model(far_ends[0]) + far_ends = serializer_b(far_ends, nested=True, many=True, context={'request': request}).data path.append((near_ends, cable, far_ends)) diff --git a/netbox/extras/api/customfields.py b/netbox/extras/api/customfields.py index 6cd3a245e..fd99ce703 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,13 +1,12 @@ from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext as _ -from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field from rest_framework.fields import Field from rest_framework.serializers import ValidationError from extras.choices import CustomFieldTypeChoices from extras.models import CustomField -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model @@ -58,11 +57,11 @@ class CustomFieldsDataField(Field): for cf in self._get_custom_fields(): value = cf.deserialize(obj.get(cf.name)) if value is not None and cf.type == CustomFieldTypeChoices.TYPE_OBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - value = serializer(value, context=self.parent.context).data + serializer = get_serializer_for_model(cf.object_type.model_class()) + value = serializer(value, nested=True, context=self.parent.context).data elif value is not None and cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT: - serializer = get_serializer_for_model(cf.object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - value = serializer(value, many=True, context=self.parent.context).data + serializer = get_serializer_for_model(cf.object_type.model_class()) + value = serializer(value, nested=True, many=True, context=self.parent.context).data data[cf.name] = value return data @@ -80,12 +79,9 @@ class CustomFieldsDataField(Field): CustomFieldTypeChoices.TYPE_OBJECT, CustomFieldTypeChoices.TYPE_MULTIOBJECT ): - serializer_class = get_serializer_for_model( - model=cf.object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) + serializer_class = get_serializer_for_model(cf.object_type.model_class()) many = cf.type == CustomFieldTypeChoices.TYPE_MULTIOBJECT - serializer = serializer_class(data=data[cf.name], many=many, context=self.parent.context) + serializer = serializer_class(data=data[cf.name], nested=True, many=many, context=self.parent.context) if serializer.is_valid(): data[cf.name] = [obj['id'] for obj in serializer.data] if many else serializer.data['id'] else: 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..809bd78ed 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -1,659 +1,16 @@ -from django.contrib.auth import get_user_model -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import gettext as _ -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.models import ContentType -from dcim.api.nested_serializers import ( - NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer, - NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer, -) -from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup -from extras.choices import * -from extras.models import * -from netbox.api.exceptions import SerializerNotFound -from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField -from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer -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 utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import ( - NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, -) -from virtualization.models import Cluster, ClusterGroup, ClusterType +from .serializers_.attachments import * +from .serializers_.bookmarks import * +from .serializers_.change_logging import * +from .serializers_.contenttypes import * +from .serializers_.customfields import * +from .serializers_.customlinks import * +from .serializers_.dashboard import * +from .serializers_.events import * +from .serializers_.exporttemplates import * +from .serializers_.journaling import * +from .serializers_.configcontexts import * +from .serializers_.configtemplates import * +from .serializers_.savedfilters import * +from .serializers_.scripts import * +from .serializers_.tags import * from .nested_serializers import * - -__all__ = ( - 'BookmarkSerializer', - 'ConfigContextSerializer', - 'ConfigTemplateSerializer', - 'ContentTypeSerializer', - 'CustomFieldChoiceSetSerializer', - 'CustomFieldSerializer', - 'CustomLinkSerializer', - 'DashboardSerializer', - 'EventRuleSerializer', - 'ExportTemplateSerializer', - 'ImageAttachmentSerializer', - 'JournalEntrySerializer', - 'ObjectChangeSerializer', - 'SavedFilterSerializer', - 'ScriptDetailSerializer', - 'ScriptInputSerializer', - 'ScriptSerializer', - 'TagSerializer', - 'WebhookSerializer', -) - - -# -# Event Rules -# - -class EventRuleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), - many=True - ) - action_type = ChoiceField(choices=EventRuleActionChoices) - action_object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('event_rules'), - ) - action_object = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = EventRule - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', - 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', - 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(OpenApiTypes.OBJECT) - def get_action_object(self, instance): - context = {'request': self.context['request']} - # We need to manually instantiate the serializer for scripts - if instance.action_type == EventRuleActionChoices.SCRIPT: - script = instance.action_object - instance = script.python_class() if script.python_class else None - return NestedScriptSerializer(instance, context=context).data - else: - serializer = get_serializer_for_model( - model=instance.action_object_type.model_class(), - prefix=NESTED_SERIALIZER_PREFIX - ) - return serializer(instance.action_object, context=context).data - - -# -# Webhooks -# - -class WebhookSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail') - - class Meta: - model = Webhook - fields = [ - 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', - 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', - 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Custom fields -# - -class CustomFieldSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_fields'), - many=True - ) - type = ChoiceField(choices=CustomFieldTypeChoices) - object_type = ContentTypeField( - queryset=ContentType.objects.all(), - required=False, - allow_null=True - ) - filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) - data_type = serializers.SerializerMethodField() - choice_set = NestedCustomFieldChoiceSetSerializer( - required=False, - allow_null=True - ) - ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False) - ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False) - - class Meta: - model = CustomField - fields = [ - 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', - 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', - 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - def validate_type(self, value): - if self.instance and self.instance.type != value: - raise serializers.ValidationError(_('Changing the type of custom fields is not supported.')) - - return value - - @extend_schema_field(OpenApiTypes.STR) - def get_data_type(self, obj): - types = CustomFieldTypeChoices - if obj.type == types.TYPE_INTEGER: - return 'integer' - if obj.type == types.TYPE_DECIMAL: - return 'decimal' - if obj.type == types.TYPE_BOOLEAN: - return 'boolean' - if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT): - return 'object' - if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT): - return 'array' - 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 -# - -class CustomLinkSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('custom_links'), - many=True - ) - - class Meta: - model = CustomLink - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', - 'button_class', 'new_window', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name') - - -# -# Export templates -# - -class ExportTemplateSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.with_feature('export_templates'), - many=True - ) - data_source = NestedDataSourceSerializer( - required=False - ) - data_file = NestedDataFileSerializer( - read_only=True - ) - - class Meta: - model = ExportTemplate - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', - 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Saved filters -# - -class SavedFilterSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') - content_types = ContentTypeField( - queryset=ContentType.objects.all(), - many=True - ) - - class Meta: - model = SavedFilter - fields = [ - 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', - 'shared', 'parameters', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -# -# Bookmarks -# - -class BookmarkSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') - object_type = ContentTypeField( - queryset=ContentType.objects.with_feature('bookmarks'), - ) - object = serializers.SerializerMethodField(read_only=True) - user = NestedUserSerializer() - - class Meta: - model = Bookmark - fields = [ - 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', - ] - brief_fields = ('id', 'url', 'display', 'object_id', 'object_type') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_object(self, instance): - serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(instance.object, context={'request': self.context['request']}).data - - -# -# Tags -# - -class TagSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') - object_types = ContentTypeField( - queryset=ContentType.objects.with_feature('tags'), - many=True, - required=False - ) - - # Related object counts - tagged_items = RelatedObjectCountField('extras_taggeditem_items') - - class Meta: - model = Tag - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') - - -# -# Image attachments -# - -class ImageAttachmentSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') - content_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - parent = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = ImageAttachment - fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', - 'image_width', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'image') - - def validate(self, data): - - # Validate that the parent object exists - try: - data['content_type'].get_object_for_this_type(id=data['object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) - ) - - # Enforce model validation - super().validate(data) - - return data - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_parent(self, obj): - serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX) - return serializer(obj.parent, context={'request': self.context['request']}).data - - -# -# Journal entries -# - -class JournalEntrySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - created_by = serializers.PrimaryKeyRelatedField( - allow_null=True, - queryset=get_user_model().objects.all(), - required=False, - default=serializers.CurrentUserDefault() - ) - kind = ChoiceField( - choices=JournalEntryKindChoices, - required=False - ) - - class Meta: - model = JournalEntry - fields = [ - 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', - 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'created') - - def validate(self, data): - - # Validate that the parent object exists - if 'assigned_object_type' in data and 'assigned_object_id' in data: - try: - data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}" - ) - - # Enforce model validation - super().validate(data) - - return data - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data - - -# -# Config contexts -# - -class ConfigContextSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail') - regions = SerializedPKRelatedField( - queryset=Region.objects.all(), - serializer=NestedRegionSerializer, - required=False, - many=True - ) - site_groups = SerializedPKRelatedField( - queryset=SiteGroup.objects.all(), - serializer=NestedSiteGroupSerializer, - required=False, - many=True - ) - sites = SerializedPKRelatedField( - queryset=Site.objects.all(), - serializer=NestedSiteSerializer, - required=False, - many=True - ) - locations = SerializedPKRelatedField( - queryset=Location.objects.all(), - serializer=NestedLocationSerializer, - required=False, - many=True - ) - device_types = SerializedPKRelatedField( - queryset=DeviceType.objects.all(), - serializer=NestedDeviceTypeSerializer, - required=False, - many=True - ) - roles = SerializedPKRelatedField( - queryset=DeviceRole.objects.all(), - serializer=NestedDeviceRoleSerializer, - required=False, - many=True - ) - platforms = SerializedPKRelatedField( - queryset=Platform.objects.all(), - serializer=NestedPlatformSerializer, - required=False, - many=True - ) - cluster_types = SerializedPKRelatedField( - queryset=ClusterType.objects.all(), - serializer=NestedClusterTypeSerializer, - required=False, - many=True - ) - cluster_groups = SerializedPKRelatedField( - queryset=ClusterGroup.objects.all(), - serializer=NestedClusterGroupSerializer, - required=False, - many=True - ) - clusters = SerializedPKRelatedField( - queryset=Cluster.objects.all(), - serializer=NestedClusterSerializer, - required=False, - many=True - ) - tenant_groups = SerializedPKRelatedField( - queryset=TenantGroup.objects.all(), - serializer=NestedTenantGroupSerializer, - required=False, - many=True - ) - tenants = SerializedPKRelatedField( - queryset=Tenant.objects.all(), - serializer=NestedTenantSerializer, - required=False, - many=True - ) - tags = serializers.SlugRelatedField( - queryset=Tag.objects.all(), - slug_field='slug', - required=False, - many=True - ) - data_source = NestedDataSourceSerializer( - required=False - ) - data_file = NestedDataFileSerializer( - read_only=True - ) - - class Meta: - model = ConfigContext - fields = [ - 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', - 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', - 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Config templates -# - -class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') - data_source = NestedDataSourceSerializer( - required=False - ) - data_file = NestedDataFileSerializer( - required=False - ) - - class Meta: - model = ConfigTemplate - fields = [ - 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', - 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# Scripts -# - -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) - - class Meta: - model = Script - fields = [ - 'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_vars(self, obj): - if obj.python_class: - return { - k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items() - } - else: - return {} - - @extend_schema_field(serializers.CharField()) - def get_display(self, obj): - return f'{obj.name} ({obj.module})' - - @extend_schema_field(serializers.CharField()) - def get_description(self, obj): - if obj.python_class: - return obj.python_class().description - else: - return None - - -class ScriptDetailSerializer(ScriptSerializer): - result = serializers.SerializerMethodField(read_only=True) - - @extend_schema_field(JobSerializer()) - def get_result(self, obj): - job = obj.jobs.all().order_by('-created').first() - context = { - 'request': self.context['request'] - } - data = JobSerializer(job, context=context).data - return data - - -class ScriptInputSerializer(serializers.Serializer): - data = serializers.JSONField() - commit = serializers.BooleanField() - schedule_at = serializers.DateTimeField(required=False, allow_null=True) - interval = serializers.IntegerField(required=False, allow_null=True) - - def validate_schedule_at(self, value): - if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) - return value - - def validate_interval(self, value): - if value and not self.context['script'].scheduling_enabled: - raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) - return value - - -# -# Change logging -# - -class ObjectChangeSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') - user = NestedUserSerializer( - read_only=True - ) - action = ChoiceField( - choices=ObjectChangeActionChoices, - read_only=True - ) - changed_object_type = ContentTypeField( - read_only=True - ) - changed_object = serializers.SerializerMethodField( - read_only=True - ) - - class Meta: - model = ObjectChange - fields = [ - 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', - 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_changed_object(self, obj): - """ - Serialize a nested representation of the changed object. - """ - if obj.changed_object is None: - return None - - try: - serializer = get_serializer_for_model(obj.changed_object, prefix=NESTED_SERIALIZER_PREFIX) - except SerializerNotFound: - return obj.object_repr - context = { - 'request': self.context['request'] - } - data = serializer(obj.changed_object, context=context).data - - return data - - -# -# ContentTypes -# - -class ContentTypeSerializer(BaseModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') - - class Meta: - model = ContentType - fields = ['id', 'url', 'display', 'app_label', 'model'] - - -# -# User dashboard -# - -class DashboardSerializer(serializers.ModelSerializer): - class Meta: - model = Dashboard - fields = ('layout', 'config') diff --git a/netbox/extras/api/serializers_/__init__.py b/netbox/extras/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/extras/api/serializers_/attachments.py b/netbox/extras/api/serializers_/attachments.py new file mode 100644 index 000000000..e26d8516b --- /dev/null +++ b/netbox/extras/api/serializers_/attachments.py @@ -0,0 +1,50 @@ +from django.core.exceptions import ObjectDoesNotExist +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.models import ImageAttachment +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ImageAttachmentSerializer', +) + + +class ImageAttachmentSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail') + content_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + parent = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = ImageAttachment + fields = [ + 'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height', + 'image_width', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'image') + + def validate(self, data): + + # Validate that the parent object exists + try: + data['content_type'].get_object_for_this_type(id=data['object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id']) + ) + + # Enforce model validation + super().validate(data) + + return data + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_parent(self, obj): + serializer = get_serializer_for_model(obj.parent) + context = {'request': self.context['request']} + return serializer(obj.parent, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/bookmarks.py b/netbox/extras/api/serializers_/bookmarks.py new file mode 100644 index 000000000..8140d2d84 --- /dev/null +++ b/netbox/extras/api/serializers_/bookmarks.py @@ -0,0 +1,35 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.models import Bookmark +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer +from users.api.serializers_.users import UserSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'BookmarkSerializer', +) + + +class BookmarkSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail') + object_type = ContentTypeField( + queryset=ContentType.objects.with_feature('bookmarks'), + ) + object = serializers.SerializerMethodField(read_only=True) + user = UserSerializer(nested=True) + + class Meta: + model = Bookmark + fields = [ + 'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created', + ] + brief_fields = ('id', 'url', 'display', 'object_id', 'object_type') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_object(self, instance): + serializer = get_serializer_for_model(instance.object) + context = {'request': self.context['request']} + return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/change_logging.py b/netbox/extras/api/serializers_/change_logging.py new file mode 100644 index 000000000..32585637c --- /dev/null +++ b/netbox/extras/api/serializers_/change_logging.py @@ -0,0 +1,55 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from extras.choices import * +from extras.models import ObjectChange +from netbox.api.exceptions import SerializerNotFound +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import BaseModelSerializer +from users.api.serializers_.users import UserSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'ObjectChangeSerializer', +) + + +class ObjectChangeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:objectchange-detail') + user = UserSerializer( + nested=True, + read_only=True + ) + action = ChoiceField( + choices=ObjectChangeActionChoices, + read_only=True + ) + changed_object_type = ContentTypeField( + read_only=True + ) + changed_object = serializers.SerializerMethodField( + read_only=True + ) + + class Meta: + model = ObjectChange + fields = [ + 'id', 'url', 'display', 'time', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', + 'changed_object_id', 'changed_object', 'prechange_data', 'postchange_data', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_changed_object(self, obj): + """ + Serialize a nested representation of the changed object. + """ + if obj.changed_object is None: + return None + + try: + serializer = get_serializer_for_model(obj.changed_object) + except SerializerNotFound: + return obj.object_repr + data = serializer(obj.changed_object, nested=True, context={'request': self.context['request']}).data + + return data diff --git a/netbox/extras/api/serializers_/configcontexts.py b/netbox/extras/api/serializers_/configcontexts.py new file mode 100644 index 000000000..e9688f254 --- /dev/null +++ b/netbox/extras/api/serializers_/configcontexts.py @@ -0,0 +1,131 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from dcim.api.serializers_.devicetypes import DeviceTypeSerializer +from dcim.api.serializers_.platforms import PlatformSerializer +from dcim.api.serializers_.roles import DeviceRoleSerializer +from dcim.api.serializers_.sites import LocationSerializer, RegionSerializer, SiteSerializer, SiteGroupSerializer +from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup +from extras.models import ConfigContext, Tag +from netbox.api.fields import SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer, TenantGroupSerializer +from tenancy.models import Tenant, TenantGroup +from virtualization.api.serializers_.clusters import ClusterSerializer, ClusterGroupSerializer, ClusterTypeSerializer +from virtualization.models import Cluster, ClusterGroup, ClusterType + +__all__ = ( + 'ConfigContextSerializer', +) + + +class ConfigContextSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail') + regions = SerializedPKRelatedField( + queryset=Region.objects.all(), + serializer=RegionSerializer, + nested=True, + required=False, + many=True + ) + site_groups = SerializedPKRelatedField( + queryset=SiteGroup.objects.all(), + serializer=SiteGroupSerializer, + nested=True, + required=False, + many=True + ) + sites = SerializedPKRelatedField( + queryset=Site.objects.all(), + serializer=SiteSerializer, + nested=True, + required=False, + many=True + ) + locations = SerializedPKRelatedField( + queryset=Location.objects.all(), + serializer=LocationSerializer, + nested=True, + required=False, + many=True + ) + device_types = SerializedPKRelatedField( + queryset=DeviceType.objects.all(), + serializer=DeviceTypeSerializer, + nested=True, + required=False, + many=True + ) + roles = SerializedPKRelatedField( + queryset=DeviceRole.objects.all(), + serializer=DeviceRoleSerializer, + nested=True, + required=False, + many=True + ) + platforms = SerializedPKRelatedField( + queryset=Platform.objects.all(), + serializer=PlatformSerializer, + nested=True, + required=False, + many=True + ) + cluster_types = SerializedPKRelatedField( + queryset=ClusterType.objects.all(), + serializer=ClusterTypeSerializer, + nested=True, + required=False, + many=True + ) + cluster_groups = SerializedPKRelatedField( + queryset=ClusterGroup.objects.all(), + serializer=ClusterGroupSerializer, + nested=True, + required=False, + many=True + ) + clusters = SerializedPKRelatedField( + queryset=Cluster.objects.all(), + serializer=ClusterSerializer, + nested=True, + required=False, + many=True + ) + tenant_groups = SerializedPKRelatedField( + queryset=TenantGroup.objects.all(), + serializer=TenantGroupSerializer, + nested=True, + required=False, + many=True + ) + tenants = SerializedPKRelatedField( + queryset=Tenant.objects.all(), + serializer=TenantSerializer, + nested=True, + required=False, + many=True + ) + tags = serializers.SlugRelatedField( + queryset=Tag.objects.all(), + slug_field='slug', + required=False, + many=True + ) + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = ConfigContext + fields = [ + 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', + 'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', + 'tenant_groups', 'tenants', 'tags', 'data_source', 'data_path', 'data_file', 'data_synced', 'data', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/configtemplates.py b/netbox/extras/api/serializers_/configtemplates.py new file mode 100644 index 000000000..935214478 --- /dev/null +++ b/netbox/extras/api/serializers_/configtemplates.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from extras.models import ConfigTemplate +from netbox.api.serializers import ValidatedModelSerializer +from netbox.api.serializers.features import TaggableModelSerializer + +__all__ = ( + 'ConfigTemplateSerializer', +) + + +class ConfigTemplateSerializer(TaggableModelSerializer, ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + required=False + ) + + class Meta: + model = ConfigTemplate + fields = [ + 'id', 'url', 'display', 'name', 'description', 'environment_params', 'template_code', 'data_source', + 'data_path', 'data_file', 'data_synced', 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/contenttypes.py b/netbox/extras/api/serializers_/contenttypes.py new file mode 100644 index 000000000..cc11e88b6 --- /dev/null +++ b/netbox/extras/api/serializers_/contenttypes.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from core.models import ContentType +from netbox.api.serializers import BaseModelSerializer + +__all__ = ( + 'ContentTypeSerializer', +) + + +class ContentTypeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') + + class Meta: + model = ContentType + fields = ['id', 'url', 'display', 'app_label', 'model'] diff --git a/netbox/extras/api/serializers_/customfields.py b/netbox/extras/api/serializers_/customfields.py new file mode 100644 index 000000000..668e780d9 --- /dev/null +++ b/netbox/extras/api/serializers_/customfields.py @@ -0,0 +1,91 @@ +from django.utils.translation import gettext as _ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import CustomField, CustomFieldChoiceSet +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'CustomFieldChoiceSetSerializer', + 'CustomFieldSerializer', +) + + +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( + queryset=ContentType.objects.with_feature('custom_fields'), + many=True + ) + type = ChoiceField(choices=CustomFieldTypeChoices) + object_type = ContentTypeField( + queryset=ContentType.objects.all(), + required=False, + allow_null=True + ) + filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) + data_type = serializers.SerializerMethodField() + choice_set = CustomFieldChoiceSetSerializer( + nested=True, + required=False, + allow_null=True + ) + ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False) + ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False) + + class Meta: + model = CustomField + fields = [ + 'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name', + 'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable', + 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + def validate_type(self, value): + if self.instance and self.instance.type != value: + raise serializers.ValidationError(_('Changing the type of custom fields is not supported.')) + + return value + + @extend_schema_field(OpenApiTypes.STR) + def get_data_type(self, obj): + types = CustomFieldTypeChoices + if obj.type == types.TYPE_INTEGER: + return 'integer' + if obj.type == types.TYPE_DECIMAL: + return 'decimal' + if obj.type == types.TYPE_BOOLEAN: + return 'boolean' + if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT): + return 'object' + if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT): + return 'array' + return 'string' diff --git a/netbox/extras/api/serializers_/customlinks.py b/netbox/extras/api/serializers_/customlinks.py new file mode 100644 index 000000000..8f53db2ca --- /dev/null +++ b/netbox/extras/api/serializers_/customlinks.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import CustomLink +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'CustomLinkSerializer', +) + + +class CustomLinkSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('custom_links'), + many=True + ) + + class Meta: + model = CustomLink + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name', + 'button_class', 'new_window', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name') diff --git a/netbox/extras/api/serializers_/dashboard.py b/netbox/extras/api/serializers_/dashboard.py new file mode 100644 index 000000000..74d5daecf --- /dev/null +++ b/netbox/extras/api/serializers_/dashboard.py @@ -0,0 +1,13 @@ +from rest_framework import serializers + +from extras.models import Dashboard + +__all__ = ( + 'DashboardSerializer', +) + + +class DashboardSerializer(serializers.ModelSerializer): + class Meta: + model = Dashboard + fields = ('layout', 'config') diff --git a/netbox/extras/api/serializers_/events.py b/netbox/extras/api/serializers_/events.py new file mode 100644 index 000000000..6f369d63d --- /dev/null +++ b/netbox/extras/api/serializers_/events.py @@ -0,0 +1,71 @@ +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import EventRule, Webhook +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from utilities.api import get_serializer_for_model +from .scripts import ScriptSerializer + +__all__ = ( + 'EventRuleSerializer', + 'WebhookSerializer', +) + + +# +# Event Rules +# + +class EventRuleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('event_rules'), + many=True + ) + action_type = ChoiceField(choices=EventRuleActionChoices) + action_object_type = ContentTypeField( + queryset=ContentType.objects.with_feature('event_rules'), + ) + action_object = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = EventRule + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete', + 'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type', + 'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(OpenApiTypes.OBJECT) + def get_action_object(self, instance): + context = {'request': self.context['request']} + # We need to manually instantiate the serializer for scripts + if instance.action_type == EventRuleActionChoices.SCRIPT: + script = instance.action_object + instance = script.python_class() if script.python_class else None + return ScriptSerializer(instance, nested=True, context=context).data + else: + serializer = get_serializer_for_model(instance.action_object_type.model_class()) + return serializer(instance.action_object, nested=True, context=context).data + + +# +# Webhooks +# + +class WebhookSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail') + + class Meta: + model = Webhook + fields = [ + 'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type', + 'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields', + 'tags', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/exporttemplates.py b/netbox/extras/api/serializers_/exporttemplates.py new file mode 100644 index 000000000..37e36fd55 --- /dev/null +++ b/netbox/extras/api/serializers_/exporttemplates.py @@ -0,0 +1,36 @@ +from rest_framework import serializers + +from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer +from core.models import ContentType +from extras.models import ExportTemplate +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'ExportTemplateSerializer', +) + + +class ExportTemplateSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.with_feature('export_templates'), + many=True + ) + data_source = DataSourceSerializer( + nested=True, + required=False + ) + data_file = DataFileSerializer( + nested=True, + read_only=True + ) + + class Meta: + model = ExportTemplate + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type', + 'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/extras/api/serializers_/journaling.py b/netbox/extras/api/serializers_/journaling.py new file mode 100644 index 000000000..848b2842a --- /dev/null +++ b/netbox/extras/api/serializers_/journaling.py @@ -0,0 +1,63 @@ +from django.contrib.auth import get_user_model +from django.core.exceptions import ObjectDoesNotExist +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.models import ContentType +from extras.choices import * +from extras.models import JournalEntry +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from utilities.api import get_serializer_for_model + +__all__ = ( + 'JournalEntrySerializer', +) + + +class JournalEntrySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail') + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + created_by = serializers.PrimaryKeyRelatedField( + allow_null=True, + queryset=get_user_model().objects.all(), + required=False, + default=serializers.CurrentUserDefault() + ) + kind = ChoiceField( + choices=JournalEntryKindChoices, + required=False + ) + + class Meta: + model = JournalEntry + fields = [ + 'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created', + 'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'created') + + def validate(self, data): + + # Validate that the parent object exists + if 'assigned_object_type' in data and 'assigned_object_id' in data: + try: + data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}" + ) + + # Enforce model validation + super().validate(data) + + return data + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, instance): + serializer = get_serializer_for_model(instance.assigned_object_type.model_class()) + context = {'request': self.context['request']} + return serializer(instance.assigned_object, nested=True, context=context).data diff --git a/netbox/extras/api/serializers_/savedfilters.py b/netbox/extras/api/serializers_/savedfilters.py new file mode 100644 index 000000000..cb27c0b0d --- /dev/null +++ b/netbox/extras/api/serializers_/savedfilters.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import SavedFilter +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'SavedFilterSerializer', +) + + +class SavedFilterSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail') + content_types = ContentTypeField( + queryset=ContentType.objects.all(), + many=True + ) + + class Meta: + model = SavedFilter + fields = [ + 'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled', + 'shared', 'parameters', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/extras/api/serializers_/scripts.py b/netbox/extras/api/serializers_/scripts.py new file mode 100644 index 000000000..b2a8ef29d --- /dev/null +++ b/netbox/extras/api/serializers_/scripts.py @@ -0,0 +1,77 @@ +from django.utils.translation import gettext as _ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from core.api.serializers_.jobs import JobSerializer +from extras.models import Script +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'ScriptDetailSerializer', + 'ScriptInputSerializer', + 'ScriptSerializer', +) + + +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 = JobSerializer(nested=True, read_only=True) + + class Meta: + model = Script + fields = [ + 'id', 'url', 'module', 'name', 'description', 'vars', 'result', 'display', 'is_executable', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_vars(self, obj): + if obj.python_class: + return { + k: v.__class__.__name__ for k, v in obj.python_class()._get_vars().items() + } + else: + return {} + + @extend_schema_field(serializers.CharField()) + def get_display(self, obj): + return f'{obj.name} ({obj.module})' + + @extend_schema_field(serializers.CharField()) + def get_description(self, obj): + if obj.python_class: + return obj.python_class().description + else: + return None + + +class ScriptDetailSerializer(ScriptSerializer): + result = serializers.SerializerMethodField(read_only=True) + + @extend_schema_field(JobSerializer()) + def get_result(self, obj): + job = obj.jobs.all().order_by('-created').first() + context = { + 'request': self.context['request'] + } + data = JobSerializer(job, context=context).data + return data + + +class ScriptInputSerializer(serializers.Serializer): + data = serializers.JSONField() + commit = serializers.BooleanField() + schedule_at = serializers.DateTimeField(required=False, allow_null=True) + interval = serializers.IntegerField(required=False, allow_null=True) + + def validate_schedule_at(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) + return value + + def validate_interval(self, value): + if value and not self.context['script'].scheduling_enabled: + raise serializers.ValidationError(_("Scheduling is not enabled for this script.")) + return value diff --git a/netbox/extras/api/serializers_/tags.py b/netbox/extras/api/serializers_/tags.py new file mode 100644 index 000000000..28a021f29 --- /dev/null +++ b/netbox/extras/api/serializers_/tags.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.models import ContentType +from extras.models import Tag +from netbox.api.fields import ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import ValidatedModelSerializer + +__all__ = ( + 'TagSerializer', +) + + +class TagSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail') + object_types = ContentTypeField( + queryset=ContentType.objects.with_feature('tags'), + many=True, + required=False + ) + + # Related object counts + tagged_items = RelatedObjectCountField('extras_taggeditem_items') + + class Meta: + model = Tag + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description') diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index a9c0ce7cb..1f5f21028 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -1,510 +1,8 @@ -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 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 utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedVirtualMachineSerializer -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer -from .field_serializers import IPAddressField, IPNetworkField +from .serializers_.asns import * +from .serializers_.vrfs import * +from .serializers_.roles import * +from .serializers_.vlans import * +from .serializers_.ip import * +from .serializers_.fhrpgroups import * +from .serializers_.services import * from .nested_serializers import * - - -# -# ASN ranges -# - -class ASNRangeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') - rir = NestedRIRSerializer() - tenant = NestedTenantSerializer(required=False, allow_null=True) - asn_count = serializers.IntegerField(read_only=True) - - class Meta: - model = ASNRange - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags', - 'custom_fields', 'created', 'last_updated', 'asn_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# ASNs -# - -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) - - # Related object counts - site_count = RelatedObjectCountField('sites') - provider_count = RelatedObjectCountField('providers') - - class Meta: - model = ASN - fields = [ - 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'site_count', 'provider_count', - ] - brief_fields = ('id', 'url', 'display', 'asn', 'description') - - -class AvailableASNSerializer(serializers.Serializer): - """ - Representation of an ASN which does not exist in the database. - """ - asn = serializers.IntegerField(read_only=True) - description = serializers.CharField(required=False) - - def to_representation(self, asn): - rir = NestedRIRSerializer(self.context['range'].rir, context={ - 'request': self.context['request'] - }).data - return { - 'rir': rir, - 'asn': asn, - } - - -# -# VRFs -# - -class VRFSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') - tenant = NestedTenantSerializer(required=False, allow_null=True) - import_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - export_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - - # Related object counts - ipaddress_count = RelatedObjectCountField('ip_addresses') - prefix_count = RelatedObjectCountField('prefixes') - - class Meta: - model = VRF - fields = [ - 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', - 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', - 'prefix_count', - ] - 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) - - class Meta: - model = RouteTarget - fields = [ - 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# RIRs/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) - prefix = IPNetworkField() - - class Meta: - model = Aggregate - fields = [ - 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') - - -# -# FHRP Groups -# - -class FHRPGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') - ip_addresses = NestedIPAddressSerializer(many=True, read_only=True) - - class Meta: - model = FHRPGroup - fields = [ - 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', - ] - brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') - - -class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') - group = NestedFHRPGroupSerializer() - interface_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - interface = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = FHRPGroupAssignment - fields = [ - 'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created', - 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_interface(self, obj): - if obj.interface is None: - return None - serializer = get_serializer_for_model(obj.interface, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.interface, context=context).data - - -# -# VLANs -# - -class RoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') - - # Related object counts - prefix_count = RelatedObjectCountField('prefixes') - vlan_count = RelatedObjectCountField('vlans') - - class Meta: - model = Role - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'prefix_count', 'vlan_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') - - -class VLANGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') - scope_type = ContentTypeField( - queryset=ContentType.objects.filter( - model__in=VLANGROUP_SCOPE_TYPES - ), - allow_null=True, - required=False, - default=None - ) - scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) - scope = serializers.SerializerMethodField(read_only=True) - utilization = serializers.CharField(read_only=True) - - # Related object counts - vlan_count = RelatedObjectCountField('vlans') - - class Meta: - model = VLANGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', - 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') - validators = [] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_scope(self, obj): - if obj.scope_id is None: - return None - serializer = get_serializer_for_model(obj.scope, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - - return serializer(obj.scope, context=context).data - - -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) - status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) - l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True) - - # Related object counts - prefix_count = RelatedObjectCountField('prefixes') - - class Meta: - model = VLAN - fields = [ - 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', - 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', - ] - brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') - - -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) - - def to_representation(self, instance): - return { - 'vid': instance, - 'group': NestedVLANGroupSerializer( - self.context['group'], - context={'request': self.context['request']} - ).data, - } - - -class CreateAvailableVLANSerializer(NetBoxModelSerializer): - site = NestedSiteSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - status = ChoiceField(choices=VLANStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) - - class Meta: - model = VLAN - fields = [ - 'name', 'site', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields', - ] - - def validate(self, data): - # Bypass model validation since we don't have a VID yet - return data - - -# -# Prefixes -# - -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) - status = ChoiceField(choices=PrefixStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) - children = serializers.IntegerField(read_only=True) - _depth = serializers.IntegerField(read_only=True) - prefix = IPNetworkField() - - class Meta: - model = Prefix - fields = [ - 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', - 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', - '_depth', - ] - brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') - - -class PrefixLengthSerializer(serializers.Serializer): - - prefix_length = serializers.IntegerField() - - def to_internal_value(self, data): - requested_prefix = data.get('prefix_length') - if requested_prefix is None: - raise serializers.ValidationError({ - 'prefix_length': 'this field can not be missing' - }) - if not isinstance(requested_prefix, int): - raise serializers.ValidationError({ - 'prefix_length': 'this field must be int type' - }) - - prefix = self.context.get('prefix') - if prefix.family == 4 and requested_prefix > 32: - raise serializers.ValidationError({ - 'prefix_length': 'Invalid prefix length ({}) for IPv4'.format((requested_prefix)) - }) - elif prefix.family == 6 and requested_prefix > 128: - raise serializers.ValidationError({ - 'prefix_length': 'Invalid prefix length ({}) for IPv6'.format((requested_prefix)) - }) - return data - - -class AvailablePrefixSerializer(serializers.Serializer): - """ - Representation of a prefix which does not exist in the database. - """ - family = serializers.IntegerField(read_only=True) - prefix = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(read_only=True) - - def to_representation(self, instance): - if self.context.get('vrf'): - vrf = NestedVRFSerializer(self.context['vrf'], context={'request': self.context['request']}).data - else: - vrf = None - return { - 'family': instance.version, - 'prefix': str(instance), - 'vrf': vrf, - } - - -# -# IP ranges -# - -class IPRangeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail') - 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) - status = ChoiceField(choices=IPRangeStatusChoices, required=False) - role = NestedRoleSerializer(required=False, allow_null=True) - - class Meta: - model = IPRange - fields = [ - 'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') - - -# -# IP addresses -# - -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) - status = ChoiceField(choices=IPAddressStatusChoices, required=False) - role = ChoiceField(choices=IPAddressRoleChoices, allow_blank=True, required=False) - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS), - required=False, - allow_null=True - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) - nat_outside = NestedIPAddressSerializer(many=True, read_only=True) - - class Meta: - model = IPAddress - fields = [ - 'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', - 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, obj): - if obj.assigned_object is None: - return None - serializer = get_serializer_for_model(obj.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(obj.assigned_object, context=context).data - - -class AvailableIPSerializer(serializers.Serializer): - """ - Representation of an IP address which does not exist in the database. - """ - family = serializers.IntegerField(read_only=True) - address = serializers.CharField(read_only=True) - vrf = NestedVRFSerializer(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 - else: - vrf = None - return { - 'family': self.context['parent'].family, - 'address': f"{instance}/{self.context['parent'].mask_length}", - 'vrf': vrf, - } - - -# -# Services -# - -class ServiceTemplateSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail') - protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) - - class Meta: - model = ServiceTemplate - fields = [ - 'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') - - -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) - protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) - ipaddresses = SerializedPKRelatedField( - queryset=IPAddress.objects.all(), - serializer=NestedIPAddressSerializer, - required=False, - many=True - ) - - class Meta: - model = Service - fields = [ - 'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', - 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/api/serializers_/__init__.py b/netbox/ipam/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/ipam/api/serializers_/asns.py b/netbox/ipam/api/serializers_/asns.py new file mode 100644 index 000000000..9a8ab5b00 --- /dev/null +++ b/netbox/ipam/api/serializers_/asns.py @@ -0,0 +1,78 @@ +from rest_framework import serializers + +from ipam.models import ASN, ASNRange, RIR +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer + +__all__ = ( + 'ASNRangeSerializer', + 'ASNSerializer', + 'AvailableASNSerializer', + 'RIRSerializer', +) + + +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 ASNRangeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + asn_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ASNRange + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'rir', 'start', 'end', 'tenant', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'asn_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ASNSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') + 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') + provider_count = RelatedObjectCountField('providers') + + class Meta: + model = ASN + fields = [ + 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'site_count', 'provider_count', + ] + brief_fields = ('id', 'url', 'display', 'asn', 'description') + + +class AvailableASNSerializer(serializers.Serializer): + """ + Representation of an ASN which does not exist in the database. + """ + asn = serializers.IntegerField(read_only=True) + description = serializers.CharField(required=False) + + def to_representation(self, asn): + rir = RIRSerializer(self.context['range'].rir, nested=True, context={ + 'request': self.context['request'] + }).data + return { + 'rir': rir, + 'asn': asn, + } diff --git a/netbox/ipam/api/serializers_/fhrpgroups.py b/netbox/ipam/api/serializers_/fhrpgroups.py new file mode 100644 index 000000000..9bf1d4548 --- /dev/null +++ b/netbox/ipam/api/serializers_/fhrpgroups.py @@ -0,0 +1,52 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.models import FHRPGroup, FHRPGroupAssignment +from netbox.api.fields import ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from utilities.api import get_serializer_for_model +from .ip import IPAddressSerializer + +__all__ = ( + 'FHRPGroupAssignmentSerializer', + 'FHRPGroupSerializer', +) + + +class FHRPGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroup-detail') + ip_addresses = IPAddressSerializer(nested=True, many=True, read_only=True) + + class Meta: + model = FHRPGroup + fields = [ + 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'ip_addresses', + ] + brief_fields = ('id', 'url', 'display', 'protocol', 'group_id', 'description') + + +class FHRPGroupAssignmentSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:fhrpgroupassignment-detail') + group = FHRPGroupSerializer(nested=True) + interface_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + interface = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = FHRPGroupAssignment + fields = [ + 'id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'interface', 'priority', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'group', 'interface_type', 'interface_id', 'priority') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_interface(self, obj): + if obj.interface is None: + return None + serializer = get_serializer_for_model(obj.interface) + context = {'request': self.context['request']} + return serializer(obj.interface, nested=True, context=context).data diff --git a/netbox/ipam/api/serializers_/ip.py b/netbox/ipam/api/serializers_/ip.py new file mode 100644 index 000000000..e5fa81314 --- /dev/null +++ b/netbox/ipam/api/serializers_/ip.py @@ -0,0 +1,198 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from ipam.choices import * +from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS +from ipam.models import Aggregate, IPAddress, IPRange, Prefix +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from .asns import RIRSerializer +from .roles import RoleSerializer +from .vlans import VLANSerializer +from .vrfs import VRFSerializer +from ..field_serializers import IPAddressField, IPNetworkField +from ..nested_serializers import * + +__all__ = ( + 'AggregateSerializer', + 'AvailableIPSerializer', + 'AvailablePrefixSerializer', + 'IPAddressSerializer', + 'IPRangeSerializer', + 'PrefixLengthSerializer', + 'PrefixSerializer', +) + + +class AggregateSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:aggregate-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + rir = RIRSerializer(nested=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + prefix = IPNetworkField() + + class Meta: + model = Aggregate + fields = [ + 'id', 'url', 'display', 'family', 'prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description') + + +class PrefixSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:prefix-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=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 = RoleSerializer(nested=True, required=False, allow_null=True) + children = serializers.IntegerField(read_only=True) + _depth = serializers.IntegerField(read_only=True) + prefix = IPNetworkField() + + class Meta: + model = Prefix + fields = [ + 'id', 'url', 'display', 'family', 'prefix', 'site', 'vrf', 'tenant', 'vlan', 'status', 'role', 'is_pool', + 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'children', + '_depth', + ] + brief_fields = ('id', 'url', 'display', 'family', 'prefix', 'description', '_depth') + + +class PrefixLengthSerializer(serializers.Serializer): + + prefix_length = serializers.IntegerField() + + def to_internal_value(self, data): + requested_prefix = data.get('prefix_length') + if requested_prefix is None: + raise serializers.ValidationError({ + 'prefix_length': 'this field can not be missing' + }) + if not isinstance(requested_prefix, int): + raise serializers.ValidationError({ + 'prefix_length': 'this field must be int type' + }) + + prefix = self.context.get('prefix') + if prefix.family == 4 and requested_prefix > 32: + raise serializers.ValidationError({ + 'prefix_length': 'Invalid prefix length ({}) for IPv4'.format(requested_prefix) + }) + elif prefix.family == 6 and requested_prefix > 128: + raise serializers.ValidationError({ + 'prefix_length': 'Invalid prefix length ({}) for IPv6'.format(requested_prefix) + }) + return data + + +class AvailablePrefixSerializer(serializers.Serializer): + """ + Representation of a prefix which does not exist in the database. + """ + family = serializers.IntegerField(read_only=True) + prefix = serializers.CharField(read_only=True) + vrf = VRFSerializer(nested=True, read_only=True) + + def to_representation(self, instance): + if self.context.get('vrf'): + vrf = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data + else: + vrf = None + return { + 'family': instance.version, + 'prefix': str(instance), + 'vrf': vrf, + } + + +# +# IP ranges +# + +class IPRangeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:iprange-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + start_address = IPAddressField() + end_address = IPAddressField() + 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 = RoleSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = IPRange + fields = [ + 'id', 'url', 'display', 'family', 'start_address', 'end_address', 'size', 'vrf', 'tenant', 'status', 'role', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'mark_utilized', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'start_address', 'end_address', 'description') + + +# +# IP addresses +# + +class IPAddressSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:ipaddress-detail') + family = ChoiceField(choices=IPAddressFamilyChoices, read_only=True) + address = IPAddressField() + 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( + queryset=ContentType.objects.filter(IPADDRESS_ASSIGNMENT_MODELS), + required=False, + allow_null=True + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + nat_inside = NestedIPAddressSerializer(required=False, allow_null=True) + nat_outside = NestedIPAddressSerializer(many=True, read_only=True) + + class Meta: + model = IPAddress + fields = [ + 'id', 'url', 'display', 'family', 'address', 'vrf', 'tenant', 'status', 'role', 'assigned_object_type', + 'assigned_object_id', 'assigned_object', 'nat_inside', 'nat_outside', 'dns_name', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'family', 'address', 'description') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, obj): + if obj.assigned_object is None: + return None + serializer = get_serializer_for_model(obj.assigned_object) + context = {'request': self.context['request']} + return serializer(obj.assigned_object, nested=True, context=context).data + + +class AvailableIPSerializer(serializers.Serializer): + """ + Representation of an IP address which does not exist in the database. + """ + family = serializers.IntegerField(read_only=True) + address = serializers.CharField(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 = VRFSerializer(self.context['vrf'], nested=True, context={'request': self.context['request']}).data + else: + vrf = None + return { + 'family': self.context['parent'].family, + 'address': f"{instance}/{self.context['parent'].mask_length}", + 'vrf': vrf, + } diff --git a/netbox/ipam/api/serializers_/roles.py b/netbox/ipam/api/serializers_/roles.py new file mode 100644 index 000000000..8208b8074 --- /dev/null +++ b/netbox/ipam/api/serializers_/roles.py @@ -0,0 +1,25 @@ +from rest_framework import serializers + +from ipam.models import Role +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer + +__all__ = ( + 'RoleSerializer', +) + + +class RoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:role-detail') + + # Related object counts + prefix_count = RelatedObjectCountField('prefixes') + vlan_count = RelatedObjectCountField('vlans') + + class Meta: + model = Role + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'weight', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'prefix_count', 'vlan_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'prefix_count', 'vlan_count') diff --git a/netbox/ipam/api/serializers_/services.py b/netbox/ipam/api/serializers_/services.py new file mode 100644 index 000000000..407739667 --- /dev/null +++ b/netbox/ipam/api/serializers_/services.py @@ -0,0 +1,49 @@ +from rest_framework import serializers + +from dcim.api.serializers_.devices import DeviceSerializer +from ipam.choices import * +from ipam.models import IPAddress, Service, ServiceTemplate +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from virtualization.api.serializers_.virtualmachines import VirtualMachineSerializer +from .ip import IPAddressSerializer + +__all__ = ( + 'ServiceSerializer', + 'ServiceTemplateSerializer', +) + + +class ServiceTemplateSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:servicetemplate-detail') + protocol = ChoiceField(choices=ServiceProtocolChoices, required=False) + + class Meta: + model = ServiceTemplate + fields = [ + 'id', 'url', 'display', 'name', 'protocol', 'ports', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') + + +class ServiceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:service-detail') + 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(), + serializer=IPAddressSerializer, + nested=True, + required=False, + many=True + ) + + class Meta: + model = Service + fields = [ + 'id', 'url', 'display', 'device', 'virtual_machine', 'name', 'protocol', 'ports', 'ipaddresses', + 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'protocol', 'ports', 'description') diff --git a/netbox/ipam/api/serializers_/vlans.py b/netbox/ipam/api/serializers_/vlans.py new file mode 100644 index 000000000..a400f949b --- /dev/null +++ b/netbox/ipam/api/serializers_/vlans.py @@ -0,0 +1,112 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from ipam.choices import * +from ipam.constants import VLANGROUP_SCOPE_TYPES +from ipam.models import VLAN, VLANGroup +from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer +from .roles import RoleSerializer + +__all__ = ( + 'AvailableVLANSerializer', + 'CreateAvailableVLANSerializer', + 'VLANGroupSerializer', + 'VLANSerializer', +) + + +class VLANGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlangroup-detail') + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = serializers.IntegerField(allow_null=True, required=False, default=None) + scope = serializers.SerializerMethodField(read_only=True) + utilization = serializers.CharField(read_only=True) + + # Related object counts + vlan_count = RelatedObjectCountField('vlans') + + class Meta: + model = VLANGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'scope_type', 'scope_id', 'scope', 'min_vid', 'max_vid', + 'description', 'tags', 'custom_fields', 'created', 'last_updated', 'vlan_count', 'utilization' + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'vlan_count') + validators = [] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {'request': self.context['request']} + return serializer(obj.scope, nested=True, context=context).data + + +class VLANSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vlan-detail') + 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 = RoleSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + + # Related object counts + prefix_count = RelatedObjectCountField('prefixes') + + class Meta: + model = VLAN + fields = [ + 'id', 'url', 'display', 'site', 'group', 'vid', 'name', 'tenant', 'status', 'role', 'description', + 'comments', 'l2vpn_termination', 'tags', 'custom_fields', 'created', 'last_updated', 'prefix_count', + ] + brief_fields = ('id', 'url', 'display', 'vid', 'name', 'description') + + +class AvailableVLANSerializer(serializers.Serializer): + """ + Representation of a VLAN which does not exist in the database. + """ + vid = serializers.IntegerField(read_only=True) + group = VLANGroupSerializer(nested=True, read_only=True) + + def to_representation(self, instance): + return { + 'vid': instance, + 'group': VLANGroupSerializer( + self.context['group'], + nested=True, + context={'request': self.context['request']} + ).data, + } + + +class CreateAvailableVLANSerializer(NetBoxModelSerializer): + 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 = RoleSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = VLAN + fields = [ + 'name', 'site', 'tenant', 'status', 'role', 'description', 'tags', 'custom_fields', + ] + + def validate(self, data): + # Bypass model validation since we don't have a VID yet + return data diff --git a/netbox/ipam/api/serializers_/vrfs.py b/netbox/ipam/api/serializers_/vrfs.py new file mode 100644 index 000000000..fdb5f98ab --- /dev/null +++ b/netbox/ipam/api/serializers_/vrfs.py @@ -0,0 +1,54 @@ +from rest_framework import serializers + +from ipam.models import RouteTarget, VRF +from netbox.api.fields import RelatedObjectCountField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer + +__all__ = ( + 'RouteTargetSerializer', + 'VRFSerializer', +) + + +class RouteTargetSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:routetarget-detail') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = RouteTarget + fields = [ + 'id', 'url', 'display', 'name', 'tenant', 'description', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class VRFSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:vrf-detail') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + import_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=RouteTargetSerializer, + required=False, + many=True + ) + export_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=RouteTargetSerializer, + required=False, + many=True + ) + + # Related object counts + ipaddress_count = RelatedObjectCountField('ip_addresses') + prefix_count = RelatedObjectCountField('prefixes') + + class Meta: + model = VRF + fields = [ + 'id', 'url', 'display', 'name', 'rd', 'tenant', 'enforce_unique', 'description', 'comments', + 'import_targets', 'export_targets', 'tags', 'custom_fields', 'created', 'last_updated', 'ipaddress_count', + 'prefix_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'rd', 'description', 'prefix_count') diff --git a/netbox/netbox/api/fields.py b/netbox/netbox/api/fields.py index 241dce0a0..08ffd0bc4 100644 --- a/netbox/netbox/api/fields.py +++ b/netbox/netbox/api/fields.py @@ -132,13 +132,15 @@ class SerializedPKRelatedField(PrimaryKeyRelatedField): Extends PrimaryKeyRelatedField to return a serialized object on read. This is useful for representing related objects in a ManyToManyField while still allowing a set of primary keys to be written. """ - def __init__(self, serializer, **kwargs): + def __init__(self, serializer, nested=False, **kwargs): self.serializer = serializer + self.nested = nested self.pk_field = kwargs.pop('pk_field', None) + super().__init__(**kwargs) def to_representation(self, value): - return self.serializer(value, context={'request': self.context['request']}).data + return self.serializer(value, nested=self.nested, context={'request': self.context['request']}).data @extend_schema_field(OpenApiTypes.INT64) diff --git a/netbox/netbox/api/serializers/base.py b/netbox/netbox/api/serializers/base.py index c715b2d26..4445f62da 100644 --- a/netbox/netbox/api/serializers/base.py +++ b/netbox/netbox/api/serializers/base.py @@ -1,8 +1,12 @@ -from django.db.models import ManyToManyField +from functools import cached_property + from rest_framework import serializers +from rest_framework.utils.serializer_helpers import BindingDict 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,14 +16,48 @@ __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, fields=None, **kwargs): + """ + Extends the base __init__() method to support dynamic fields. + + :param nested: Set to True if this serializer is being employed within a parent serializer + :param fields: An iterable of fields to include when rendering the serialized object, If nested is + True but no fields are specified, Meta.brief_fields will be used. + """ + self.nested = nested + self._requested_fields = fields + + # If this serializer is nested but no fields have been specified, + # default to using Meta.brief_fields (if set) + if nested and not fields: + self._requested_fields = getattr(self.Meta, 'brief_fields', None) + super().__init__(*args, **kwargs) - # 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) + + @cached_property + def fields(self): + """ + Override the fields property to check for requested fields. If defined, + return only the applicable fields. + """ + if not self._requested_fields: + return super().fields + + fields = BindingDict(self) + for key, value in self.get_fields().items(): + if key in self._requested_fields: + fields[key] = value + return fields @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): @@ -32,6 +70,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/generic.py b/netbox/netbox/api/serializers/generic.py index 545ebb936..fb4fab8b0 100644 --- a/netbox/netbox/api/serializers/generic.py +++ b/netbox/netbox/api/serializers/generic.py @@ -3,7 +3,6 @@ from drf_spectacular.utils import extend_schema_field from rest_framework import serializers from netbox.api.fields import ContentTypeField -from netbox.constants import NESTED_SERIALIZER_PREFIX from utilities.api import get_serializer_for_model from utilities.utils import content_type_identifier @@ -40,6 +39,5 @@ class GenericObjectSerializer(serializers.Serializer): @extend_schema_field(serializers.JSONField(allow_null=True)) def get_object(self, obj): - serializer = get_serializer_for_model(obj, prefix=NESTED_SERIALIZER_PREFIX) - # context = {'request': self.context['request']} - return serializer(obj, context=self.context).data + serializer = get_serializer_for_model(obj) + return serializer(obj, nested=True, context=self.context).data 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/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index 2f0431efa..d72507e8a 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -69,7 +69,7 @@ class BaseViewSet(GenericViewSet): # If specific fields have been requested, pass them to the serializer if self.requested_fields: - kwargs['requested_fields'] = self.requested_fields + kwargs['fields'] = self.requested_fields return super().get_serializer(*args, **kwargs) diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 547e2079b..6a6928021 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -1,4 +1,5 @@ # Prefix for nested serializers +# TODO: Remove in v4.1 NESTED_SERIALIZER_PREFIX = 'Nested' # RQ queue names diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 325d3b439..642397733 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -1,123 +1,3 @@ -from django.contrib.auth.models import ContentType -from drf_spectacular.types import OpenApiTypes -from drf_spectacular.utils import extend_schema_field -from rest_framework import serializers - -from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from netbox.constants import NESTED_SERIALIZER_PREFIX -from tenancy.choices import ContactPriorityChoices -from tenancy.models import * -from utilities.api import get_serializer_for_model +from .serializers_.tenants import * +from .serializers_.contacts import * from .nested_serializers import * - - -# -# Tenants -# - -class TenantGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') - parent = NestedTenantGroupSerializer(required=False, allow_null=True) - tenant_count = serializers.IntegerField(read_only=True) - - class Meta: - model = TenantGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'tenant_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth') - - -class TenantSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') - group = NestedTenantGroupSerializer(required=False, allow_null=True) - - # Related object counts - circuit_count = RelatedObjectCountField('circuits') - device_count = RelatedObjectCountField('devices') - rack_count = RelatedObjectCountField('racks') - site_count = RelatedObjectCountField('sites') - ipaddress_count = RelatedObjectCountField('ip_addresses') - prefix_count = RelatedObjectCountField('prefixes') - vlan_count = RelatedObjectCountField('vlans') - vrf_count = RelatedObjectCountField('vrfs') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - cluster_count = RelatedObjectCountField('clusters') - - class Meta: - model = Tenant - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', - 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', - 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -# -# Contacts -# - -class ContactGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail') - parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None) - contact_count = serializers.IntegerField(read_only=True) - - class Meta: - model = ContactGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'contact_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth') - - -class ContactRoleSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') - - class Meta: - model = ContactRole - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') - - -class ContactSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') - group = NestedContactGroupSerializer(required=False, allow_null=True, default=None) - - class Meta: - model = Contact - fields = [ - 'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class ContactAssignmentSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') - content_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - object = serializers.SerializerMethodField(read_only=True) - contact = NestedContactSerializer() - role = NestedContactRoleSerializer(required=False, allow_null=True) - priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '') - - class Meta: - model = ContactAssignment - fields = [ - 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', - 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') - - @extend_schema_field(OpenApiTypes.OBJECT) - def get_object(self, instance): - serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.object, context=context).data diff --git a/netbox/tenancy/api/serializers_/__init__.py b/netbox/tenancy/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/tenancy/api/serializers_/contacts.py b/netbox/tenancy/api/serializers_/contacts.py new file mode 100644 index 000000000..925e04cfd --- /dev/null +++ b/netbox/tenancy/api/serializers_/contacts.py @@ -0,0 +1,81 @@ +from django.contrib.auth.models import ContentType +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from netbox.api.fields import ChoiceField, ContentTypeField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.choices import ContactPriorityChoices +from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole +from utilities.api import get_serializer_for_model +from ..nested_serializers import * + +__all__ = ( + 'ContactAssignmentSerializer', + 'ContactGroupSerializer', + 'ContactRoleSerializer', + 'ContactSerializer', +) + + +class ContactGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail') + parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None) + contact_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ContactGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'contact_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth') + + +class ContactRoleSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail') + + class Meta: + model = ContactRole + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') + + +class ContactSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail') + group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None) + + class Meta: + model = Contact + fields = [ + 'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description', + 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class ContactAssignmentSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail') + content_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + object = serializers.SerializerMethodField(read_only=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: + model = ContactAssignment + fields = [ + 'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags', + 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority') + + @extend_schema_field(OpenApiTypes.OBJECT) + def get_object(self, instance): + serializer = get_serializer_for_model(instance.content_type.model_class()) + context = {'request': self.context['request']} + return serializer(instance.object, nested=True, context=context).data diff --git a/netbox/tenancy/api/serializers_/tenants.py b/netbox/tenancy/api/serializers_/tenants.py new file mode 100644 index 000000000..3ee238c90 --- /dev/null +++ b/netbox/tenancy/api/serializers_/tenants.py @@ -0,0 +1,51 @@ +from rest_framework import serializers + +from netbox.api.fields import RelatedObjectCountField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.models import Tenant, TenantGroup +from ..nested_serializers import * + +__all__ = ( + 'TenantGroupSerializer', + 'TenantSerializer', +) + + +class TenantGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail') + parent = NestedTenantGroupSerializer(required=False, allow_null=True) + tenant_count = serializers.IntegerField(read_only=True) + + class Meta: + model = TenantGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'tenant_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth') + + +class TenantSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail') + group = TenantGroupSerializer(nested=True, required=False, allow_null=True) + + # Related object counts + circuit_count = RelatedObjectCountField('circuits') + device_count = RelatedObjectCountField('devices') + rack_count = RelatedObjectCountField('racks') + site_count = RelatedObjectCountField('sites') + ipaddress_count = RelatedObjectCountField('ip_addresses') + prefix_count = RelatedObjectCountField('prefixes') + vlan_count = RelatedObjectCountField('vlans') + vrf_count = RelatedObjectCountField('vrfs') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + cluster_count = RelatedObjectCountField('clusters') + + class Meta: + model = Tenant + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', + 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', + 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description') diff --git a/netbox/users/api/serializers.py b/netbox/users/api/serializers.py index b9bd55e75..ef3f66a7d 100644 --- a/netbox/users/api/serializers.py +++ b/netbox/users/api/serializers.py @@ -1,188 +1,4 @@ -from django.conf import settings -from django.contrib.auth import authenticate -from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType -from drf_spectacular.utils import extend_schema_field -from drf_spectacular.types import OpenApiTypes -from rest_framework import serializers -from rest_framework.exceptions import AuthenticationFailed, PermissionDenied - -from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField -from netbox.api.serializers import ValidatedModelSerializer -from users.models import Group, ObjectPermission, Token +from .serializers_.users import * +from .serializers_.permissions import * +from .serializers_.tokens import * from .nested_serializers import * - - -__all__ = ( - 'GroupSerializer', - 'ObjectPermissionSerializer', - 'TokenSerializer', - 'UserSerializer', -) - - -class UserSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail') - groups = SerializedPKRelatedField( - queryset=Group.objects.all(), - serializer=NestedGroupSerializer, - required=False, - many=True - ) - - class Meta: - model = get_user_model() - fields = ( - 'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', - 'date_joined', 'last_login', 'groups', - ) - brief_fields = ('id', 'url', 'display', 'username') - extra_kwargs = { - 'password': {'write_only': True} - } - - def create(self, validated_data): - """ - Extract the password from validated data and set it separately to ensure proper hash generation. - """ - password = validated_data.pop('password') - user = super().create(validated_data) - user.set_password(password) - user.save() - - return user - - def update(self, instance, validated_data): - """ - Ensure proper updated password hash generation. - """ - password = validated_data.pop('password', None) - if password is not None: - instance.set_password(password) - - return super().update(instance, validated_data) - - @extend_schema_field(OpenApiTypes.STR) - def get_display(self, obj): - if full_name := obj.get_full_name(): - return f"{obj.username} ({full_name})" - return obj.username - - -class GroupSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') - user_count = serializers.IntegerField(read_only=True) - - class Meta: - model = Group - fields = ('id', 'url', 'display', 'name', 'user_count') - brief_fields = ('id', 'url', 'display', 'name') - - -class TokenSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail') - key = serializers.CharField( - min_length=40, - max_length=40, - allow_blank=True, - required=False, - write_only=not settings.ALLOW_TOKEN_RETRIEVAL - ) - user = NestedUserSerializer() - allowed_ips = serializers.ListField( - child=IPNetworkSerializer(), - required=False, - allow_empty=True, - default=[] - ) - - class Meta: - model = Token - fields = ( - 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', - 'allowed_ips', - ) - brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description') - - def to_internal_value(self, data): - if 'key' not in data: - data['key'] = Token.generate_key() - return super().to_internal_value(data) - - def validate(self, data): - - # If the Token is being created on behalf of another user, enforce the grant_token permission. - request = self.context.get('request') - token_user = data.get('user') - if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'): - raise PermissionDenied("This user does not have permission to create tokens for other users.") - - return super().validate(data) - - -class TokenProvisionSerializer(TokenSerializer): - user = NestedUserSerializer( - read_only=True - ) - username = serializers.CharField( - write_only=True - ) - password = serializers.CharField( - write_only=True - ) - last_used = serializers.DateTimeField( - read_only=True - ) - key = serializers.CharField( - read_only=True - ) - - class Meta: - model = Token - fields = ( - 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', - 'allowed_ips', 'username', 'password', - ) - - def validate(self, data): - # Validate the username and password - username = data.pop('username') - password = data.pop('password') - user = authenticate(request=self.context.get('request'), username=username, password=password) - if user is None: - raise AuthenticationFailed("Invalid username/password") - - # Inject the user into the validated data - data['user'] = user - - return data - - -class ObjectPermissionSerializer(ValidatedModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') - object_types = ContentTypeField( - queryset=ContentType.objects.all(), - many=True - ) - groups = SerializedPKRelatedField( - queryset=Group.objects.all(), - serializer=NestedGroupSerializer, - required=False, - many=True - ) - users = SerializedPKRelatedField( - queryset=get_user_model().objects.all(), - serializer=NestedUserSerializer, - required=False, - many=True - ) - - class Meta: - model = ObjectPermission - fields = ( - 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', - 'constraints', - ) - brief_fields = ( - 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', - ) diff --git a/netbox/users/api/serializers_/__init__.py b/netbox/users/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/users/api/serializers_/permissions.py b/netbox/users/api/serializers_/permissions.py new file mode 100644 index 000000000..6b0a6f6d9 --- /dev/null +++ b/netbox/users/api/serializers_/permissions.py @@ -0,0 +1,44 @@ +from django.contrib.auth import get_user_model +from django.contrib.contenttypes.models import ContentType +from rest_framework import serializers + +from netbox.api.fields import ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from users.models import Group, ObjectPermission +from .users import GroupSerializer, UserSerializer + +__all__ = ( + 'ObjectPermissionSerializer', +) + + +class ObjectPermissionSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail') + object_types = ContentTypeField( + queryset=ContentType.objects.all(), + many=True + ) + groups = SerializedPKRelatedField( + queryset=Group.objects.all(), + serializer=GroupSerializer, + nested=True, + required=False, + many=True + ) + users = SerializedPKRelatedField( + queryset=get_user_model().objects.all(), + serializer=UserSerializer, + nested=True, + required=False, + many=True + ) + + class Meta: + model = ObjectPermission + fields = ( + 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', + 'constraints', + ) + brief_fields = ( + 'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions', + ) diff --git a/netbox/users/api/serializers_/tokens.py b/netbox/users/api/serializers_/tokens.py new file mode 100644 index 000000000..65e1e7111 --- /dev/null +++ b/netbox/users/api/serializers_/tokens.py @@ -0,0 +1,94 @@ +from django.conf import settings +from django.contrib.auth import authenticate +from rest_framework import serializers +from rest_framework.exceptions import AuthenticationFailed, PermissionDenied + +from netbox.api.fields import IPNetworkSerializer +from netbox.api.serializers import ValidatedModelSerializer +from users.models import Token +from .users import * + +__all__ = ( + 'TokenProvisionSerializer', + 'TokenSerializer', +) + + +class TokenSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail') + key = serializers.CharField( + min_length=40, + max_length=40, + allow_blank=True, + required=False, + write_only=not settings.ALLOW_TOKEN_RETRIEVAL + ) + user = UserSerializer(nested=True) + allowed_ips = serializers.ListField( + child=IPNetworkSerializer(), + required=False, + allow_empty=True, + default=[] + ) + + class Meta: + model = Token + fields = ( + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', + 'allowed_ips', + ) + brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description') + + def to_internal_value(self, data): + if 'key' not in data: + data['key'] = Token.generate_key() + return super().to_internal_value(data) + + def validate(self, data): + + # If the Token is being created on behalf of another user, enforce the grant_token permission. + request = self.context.get('request') + token_user = data.get('user') + if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'): + raise PermissionDenied("This user does not have permission to create tokens for other users.") + + return super().validate(data) + + +class TokenProvisionSerializer(TokenSerializer): + user = UserSerializer( + nested=True, + read_only=True + ) + username = serializers.CharField( + write_only=True + ) + password = serializers.CharField( + write_only=True + ) + last_used = serializers.DateTimeField( + read_only=True + ) + key = serializers.CharField( + read_only=True + ) + + class Meta: + model = Token + fields = ( + 'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description', + 'allowed_ips', 'username', 'password', + ) + + def validate(self, data): + # Validate the username and password + username = data.pop('username') + password = data.pop('password') + user = authenticate(request=self.context.get('request'), username=username, password=password) + if user is None: + raise AuthenticationFailed("Invalid username/password") + + # Inject the user into the validated data + data['user'] = user + + return data diff --git a/netbox/users/api/serializers_/users.py b/netbox/users/api/serializers_/users.py new file mode 100644 index 000000000..4aa2b5a8c --- /dev/null +++ b/netbox/users/api/serializers_/users.py @@ -0,0 +1,72 @@ +from django.contrib.auth import get_user_model +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from netbox.api.fields import SerializedPKRelatedField +from netbox.api.serializers import ValidatedModelSerializer +from users.models import Group + +__all__ = ( + 'GroupSerializer', + 'UserSerializer', +) + + +class GroupSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail') + user_count = serializers.IntegerField(read_only=True) + + class Meta: + model = Group + fields = ('id', 'url', 'display', 'name', 'user_count') + brief_fields = ('id', 'url', 'display', 'name') + + +class UserSerializer(ValidatedModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail') + groups = SerializedPKRelatedField( + queryset=Group.objects.all(), + serializer=GroupSerializer, + nested=True, + required=False, + many=True + ) + + class Meta: + model = get_user_model() + fields = ( + 'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active', + 'date_joined', 'last_login', 'groups', + ) + brief_fields = ('id', 'url', 'display', 'username') + extra_kwargs = { + 'password': {'write_only': True} + } + + def create(self, validated_data): + """ + Extract the password from validated data and set it separately to ensure proper hash generation. + """ + password = validated_data.pop('password') + user = super().create(validated_data) + user.set_password(password) + user.save() + + return user + + def update(self, instance, validated_data): + """ + Ensure proper updated password hash generation. + """ + password = validated_data.pop('password', None) + if password is not None: + instance.set_password(password) + + return super().update(instance, validated_data) + + @extend_schema_field(OpenApiTypes.STR) + def get_display(self, obj): + if full_name := obj.get_full_name(): + return f"{obj.username} ({full_name})" + return obj.username diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index 25a350c81..cc25db77f 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', @@ -93,7 +96,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 @@ -118,7 +121,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 @@ -144,6 +149,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..ad698fe2f 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -1,189 +1,3 @@ -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.choices import InterfaceModeChoices -from extras.api.nested_serializers import NestedConfigTemplateSerializer -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 virtualization.choices import * -from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface -from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer +from .serializers_.clusters import * +from .serializers_.virtualmachines import * from .nested_serializers import * - - -# -# Clusters -# - -class ClusterTypeSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') - - # Related object counts - cluster_count = RelatedObjectCountField('clusters') - - class Meta: - model = ClusterType - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') - - -class ClusterGroupSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') - - # Related object counts - cluster_count = RelatedObjectCountField('clusters') - - class Meta: - model = ClusterGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', - 'cluster_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') - - -class ClusterSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') - type = NestedClusterTypeSerializer() - group = NestedClusterGroupSerializer(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) - - # Related object counts - device_count = RelatedObjectCountField('devices') - virtualmachine_count = RelatedObjectCountField('virtual_machines') - - class Meta: - model = Cluster - fields = [ - 'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') - - -# -# Virtual machines -# - -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) - 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) - - # Counter fields - interface_count = serializers.IntegerField(read_only=True) - virtual_disk_count = serializers.IntegerField(read_only=True) - - class Meta: - model = VirtualMachine - fields = [ - 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', - 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', - 'interface_count', 'virtual_disk_count', - ] - brief_fields = ('id', 'url', 'display', 'name', 'description') - validators = [] - - -class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): - config_context = serializers.SerializerMethodField() - - class Meta(VirtualMachineSerializer.Meta): - fields = [ - 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', - 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', - 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', - 'last_updated', 'interface_count', 'virtual_disk_count', - ] - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_config_context(self, obj): - return obj.get_config_context() - - -# -# VM interfaces -# - -class VMInterfaceSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') - virtual_machine = NestedVirtualMachineSerializer() - parent = NestedVMInterfaceSerializer(required=False, allow_null=True) - bridge = NestedVMInterfaceSerializer(required=False, allow_null=True) - mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False) - 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) - count_ipaddresses = serializers.IntegerField(read_only=True) - count_fhrp_groups = serializers.IntegerField(read_only=True) - mac_address = serializers.CharField( - required=False, - default=None, - allow_null=True - ) - - class Meta: - model = VMInterface - fields = [ - 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', - 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', - 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', - ] - brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') - - def validate(self, data): - - # Validate many-to-many VLAN assignments - virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') - for vlan in data.get('tagged_vlans', []): - if vlan.site not in [virtual_machine.site, None]: - raise serializers.ValidationError({ - 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " - f"machine, or it must be global." - }) - - return super().validate(data) - - -# -# Virtual Disk -# - -class VirtualDiskSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') - virtual_machine = NestedVirtualMachineSerializer() - - class Meta: - model = VirtualDisk - fields = [ - 'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields', - 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size') diff --git a/netbox/virtualization/api/serializers_/__init__.py b/netbox/virtualization/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/virtualization/api/serializers_/clusters.py b/netbox/virtualization/api/serializers_/clusters.py new file mode 100644 index 000000000..5765ff614 --- /dev/null +++ b/netbox/virtualization/api/serializers_/clusters.py @@ -0,0 +1,65 @@ +from rest_framework import serializers + +from dcim.api.serializers_.sites import SiteSerializer +from netbox.api.fields import ChoiceField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.choices import * +from virtualization.models import Cluster, ClusterGroup, ClusterType + +__all__ = ( + 'ClusterGroupSerializer', + 'ClusterSerializer', + 'ClusterTypeSerializer', +) + + +class ClusterTypeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail') + + # Related object counts + cluster_count = RelatedObjectCountField('clusters') + + class Meta: + model = ClusterType + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') + + +class ClusterGroupSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail') + + # Related object counts + cluster_count = RelatedObjectCountField('clusters') + + class Meta: + model = ClusterGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated', + 'cluster_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count') + + +class ClusterSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') + type = ClusterTypeSerializer(nested=True) + group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None) + status = ChoiceField(choices=ClusterStatusChoices, required=False) + 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') + virtualmachine_count = RelatedObjectCountField('virtual_machines') + + class Meta: + model = Cluster + fields = [ + 'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count') diff --git a/netbox/virtualization/api/serializers_/virtualmachines.py b/netbox/virtualization/api/serializers_/virtualmachines.py new file mode 100644 index 000000000..689ed3179 --- /dev/null +++ b/netbox/virtualization/api/serializers_/virtualmachines.py @@ -0,0 +1,142 @@ +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from dcim.api.serializers_.devices import DeviceSerializer +from dcim.api.serializers_.platforms import PlatformSerializer +from dcim.api.serializers_.roles import DeviceRoleSerializer +from dcim.api.serializers_.sites import SiteSerializer +from dcim.choices import InterfaceModeChoices +from extras.api.serializers_.configtemplates import ConfigTemplateSerializer +from ipam.api.serializers_.ip import IPAddressSerializer +from ipam.api.serializers_.vlans import VLANSerializer +from ipam.api.serializers_.vrfs import VRFSerializer +from ipam.models import VLAN +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from virtualization.choices import * +from virtualization.models import VirtualDisk, VirtualMachine, VMInterface +from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer +from .clusters import ClusterSerializer +from ..nested_serializers import * + +__all__ = ( + 'VMInterfaceSerializer', + 'VirtualDiskSerializer', + 'VirtualMachineSerializer', + 'VirtualMachineWithConfigContextSerializer', +) + + +class VirtualMachineSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail') + status = ChoiceField(choices=VirtualMachineStatusChoices, required=False) + 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 = IPAddressSerializer(nested=True, read_only=True) + primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True) + primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True) + config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None) + + # Counter fields + interface_count = serializers.IntegerField(read_only=True) + virtual_disk_count = serializers.IntegerField(read_only=True) + + class Meta: + model = VirtualMachine + fields = [ + 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', + 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'interface_count', 'virtual_disk_count', + ] + brief_fields = ('id', 'url', 'display', 'name', 'description') + validators = [] + + +class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): + config_context = serializers.SerializerMethodField() + + class Meta(VirtualMachineSerializer.Meta): + fields = [ + 'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform', + 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', + 'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created', + 'last_updated', 'interface_count', 'virtual_disk_count', + ] + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_config_context(self, obj): + return obj.get_config_context() + + +# +# VM interfaces +# + +class VMInterfaceSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail') + 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) + untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True) + tagged_vlans = SerializedPKRelatedField( + queryset=VLAN.objects.all(), + serializer=VLANSerializer, + nested=True, + required=False, + many=True + ) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True) + count_ipaddresses = serializers.IntegerField(read_only=True) + count_fhrp_groups = serializers.IntegerField(read_only=True) + mac_address = serializers.CharField( + required=False, + default=None, + allow_null=True + ) + + class Meta: + model = VMInterface + fields = [ + 'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address', + 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields', + 'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups', + ] + brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description') + + def validate(self, data): + + # Validate many-to-many VLAN assignments + virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine') + for vlan in data.get('tagged_vlans', []): + if vlan.site not in [virtual_machine.site, None]: + raise serializers.ValidationError({ + 'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual " + f"machine, or it must be global." + }) + + return super().validate(data) + + +# +# Virtual Disk +# + +class VirtualDiskSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail') + virtual_machine = VirtualMachineSerializer(nested=True) + + class Meta: + model = VirtualDisk + fields = [ + 'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields', + 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size') diff --git a/netbox/vpn/api/serializers.py b/netbox/vpn/api/serializers.py index 53c1f898c..8a7e21e63 100644 --- a/netbox/vpn/api/serializers.py +++ b/netbox/vpn/api/serializers.py @@ -1,280 +1,4 @@ -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.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 utilities.api import get_serializer_for_model -from vpn.choices import * -from vpn.models import * +from .serializers_.crypto import * +from .serializers_.tunnels import * +from .serializers_.l2vpn import * from .nested_serializers import * - -__all__ = ( - 'IKEPolicySerializer', - 'IKEProposalSerializer', - 'IPSecPolicySerializer', - 'IPSecProfileSerializer', - 'IPSecProposalSerializer', - 'L2VPNSerializer', - 'L2VPNTerminationSerializer', - 'TunnelGroupSerializer', - 'TunnelSerializer', - 'TunnelTerminationSerializer', -) - - -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' - ) - authentication_method = ChoiceField( - choices=AuthenticationMethodChoices - ) - encryption_algorithm = ChoiceField( - choices=EncryptionAlgorithmChoices - ) - authentication_algorithm = ChoiceField( - choices=AuthenticationAlgorithmChoices - ) - group = ChoiceField( - choices=DHGroupChoices - ) - - class Meta: - model = IKEProposal - fields = ( - 'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm', - 'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IKEPolicySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ikepolicy-detail' - ) - version = ChoiceField( - choices=IKEVersionChoices - ) - mode = ChoiceField( - choices=IKEModeChoices - ) - proposals = SerializedPKRelatedField( - queryset=IKEProposal.objects.all(), - serializer=NestedIKEProposalSerializer, - required=False, - many=True - ) - - class Meta: - model = IKEPolicy - fields = ( - 'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecProposalSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecproposal-detail' - ) - encryption_algorithm = ChoiceField( - choices=EncryptionAlgorithmChoices - ) - authentication_algorithm = ChoiceField( - choices=AuthenticationAlgorithmChoices - ) - - class Meta: - model = IPSecProposal - fields = ( - 'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', - 'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecPolicySerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecpolicy-detail' - ) - proposals = SerializedPKRelatedField( - queryset=IPSecProposal.objects.all(), - serializer=NestedIPSecProposalSerializer, - required=False, - many=True - ) - pfs_group = ChoiceField( - choices=DHGroupChoices, - required=False - ) - - class Meta: - model = IPSecPolicy - fields = ( - 'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -class IPSecProfileSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField( - view_name='vpn-api:ipsecprofile-detail' - ) - mode = ChoiceField( - choices=IPSecModeChoices - ) - ike_policy = NestedIKEPolicySerializer() - ipsec_policy = NestedIPSecPolicySerializer() - - class Meta: - model = IPSecProfile - fields = ( - 'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags', - 'custom_fields', 'created', 'last_updated', - ) - brief_fields = ('id', 'url', 'display', 'name', 'description') - - -# -# L2VPN -# - -class L2VPNSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail') - type = ChoiceField(choices=L2VPNTypeChoices, required=False) - import_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - export_targets = SerializedPKRelatedField( - queryset=RouteTarget.objects.all(), - serializer=NestedRouteTargetSerializer, - required=False, - many=True - ) - tenant = NestedTenantSerializer(required=False, allow_null=True) - - class Meta: - model = L2VPN - fields = [ - 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets', - 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' - ] - brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description') - - -class L2VPNTerminationSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') - l2vpn = NestedL2VPNSerializer() - assigned_object_type = ContentTypeField( - queryset=ContentType.objects.all() - ) - assigned_object = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = L2VPNTermination - fields = [ - 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id', - 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated' - ] - brief_fields = ('id', 'url', 'display', 'l2vpn') - - @extend_schema_field(serializers.JSONField(allow_null=True)) - def get_assigned_object(self, instance): - serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX) - context = {'request': self.context['request']} - return serializer(instance.assigned_object, context=context).data diff --git a/netbox/vpn/api/serializers_/__init__.py b/netbox/vpn/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/vpn/api/serializers_/crypto.py b/netbox/vpn/api/serializers_/crypto.py new file mode 100644 index 000000000..3ee30b754 --- /dev/null +++ b/netbox/vpn/api/serializers_/crypto.py @@ -0,0 +1,136 @@ +from rest_framework import serializers + +from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from vpn.choices import * +from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal + +__all__ = ( + 'IKEPolicySerializer', + 'IKEProposalSerializer', + 'IPSecPolicySerializer', + 'IPSecProfileSerializer', + 'IPSecProposalSerializer', +) + + +class IKEProposalSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ikeproposal-detail' + ) + authentication_method = ChoiceField( + choices=AuthenticationMethodChoices + ) + encryption_algorithm = ChoiceField( + choices=EncryptionAlgorithmChoices + ) + authentication_algorithm = ChoiceField( + choices=AuthenticationAlgorithmChoices + ) + group = ChoiceField( + choices=DHGroupChoices + ) + + class Meta: + model = IKEProposal + fields = ( + 'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm', + 'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created', + 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IKEPolicySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ikepolicy-detail' + ) + version = ChoiceField( + choices=IKEVersionChoices + ) + mode = ChoiceField( + choices=IKEModeChoices + ) + proposals = SerializedPKRelatedField( + queryset=IKEProposal.objects.all(), + serializer=IKEProposalSerializer, + nested=True, + required=False, + many=True + ) + + class Meta: + model = IKEPolicy + fields = ( + 'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments', + 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecProposalSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecproposal-detail' + ) + encryption_algorithm = ChoiceField( + choices=EncryptionAlgorithmChoices + ) + authentication_algorithm = ChoiceField( + choices=AuthenticationAlgorithmChoices + ) + + class Meta: + model = IPSecProposal + fields = ( + 'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm', + 'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecPolicySerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecpolicy-detail' + ) + proposals = SerializedPKRelatedField( + queryset=IPSecProposal.objects.all(), + serializer=IPSecProposalSerializer, + nested=True, + required=False, + many=True + ) + pfs_group = ChoiceField( + choices=DHGroupChoices, + required=False + ) + + class Meta: + model = IPSecPolicy + fields = ( + 'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') + + +class IPSecProfileSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField( + view_name='vpn-api:ipsecprofile-detail' + ) + mode = ChoiceField( + choices=IPSecModeChoices + ) + ike_policy = IKEPolicySerializer( + nested=True + ) + ipsec_policy = IPSecPolicySerializer( + nested=True + ) + + class Meta: + model = IPSecProfile + fields = ( + 'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags', + 'custom_fields', 'created', 'last_updated', + ) + brief_fields = ('id', 'url', 'display', 'name', 'description') diff --git a/netbox/vpn/api/serializers_/l2vpn.py b/netbox/vpn/api/serializers_/l2vpn.py new file mode 100644 index 000000000..3b78d50c9 --- /dev/null +++ b/netbox/vpn/api/serializers_/l2vpn.py @@ -0,0 +1,70 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.api.serializers_.vrfs import RouteTargetSerializer +from ipam.models import RouteTarget +from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.choices import * +from vpn.models import L2VPN, L2VPNTermination + +__all__ = ( + 'L2VPNSerializer', + 'L2VPNTerminationSerializer', +) + + +class L2VPNSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail') + type = ChoiceField(choices=L2VPNTypeChoices, required=False) + import_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=RouteTargetSerializer, + nested=True, + required=False, + many=True + ) + export_targets = SerializedPKRelatedField( + queryset=RouteTarget.objects.all(), + serializer=RouteTargetSerializer, + nested=True, + required=False, + many=True + ) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + + class Meta: + model = L2VPN + fields = [ + 'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets', + 'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated' + ] + brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description') + + +class L2VPNTerminationSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail') + l2vpn = L2VPNSerializer( + nested=True + ) + assigned_object_type = ContentTypeField( + queryset=ContentType.objects.all() + ) + assigned_object = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = L2VPNTermination + fields = [ + 'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id', + 'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated' + ] + brief_fields = ('id', 'url', 'display', 'l2vpn') + + @extend_schema_field(serializers.JSONField(allow_null=True)) + def get_assigned_object(self, instance): + serializer = get_serializer_for_model(instance.assigned_object) + context = {'request': self.context['request']} + return serializer(instance.assigned_object, nested=True, context=context).data diff --git a/netbox/vpn/api/serializers_/tunnels.py b/netbox/vpn/api/serializers_/tunnels.py new file mode 100644 index 000000000..3e131b1c1 --- /dev/null +++ b/netbox/vpn/api/serializers_/tunnels.py @@ -0,0 +1,112 @@ +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from rest_framework import serializers + +from ipam.api.serializers_.ip import IPAddressSerializer +from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from utilities.api import get_serializer_for_model +from vpn.choices import * +from vpn.models import Tunnel, TunnelGroup, TunnelTermination +from .crypto import IPSecProfileSerializer + +__all__ = ( + 'TunnelGroupSerializer', + 'TunnelSerializer', + 'TunnelTerminationSerializer', +) + + +# +# 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) + context = {'request': self.context['request']} + return serializer(obj.termination, nested=True, context=context).data diff --git a/netbox/wireless/api/serializers.py b/netbox/wireless/api/serializers.py index 0aec97fd2..8c864d059 100644 --- a/netbox/wireless/api/serializers.py +++ b/netbox/wireless/api/serializers.py @@ -1,67 +1,3 @@ -from rest_framework import serializers - -from dcim.choices import LinkStatusChoices -from dcim.api.serializers import NestedInterfaceSerializer -from ipam.api.serializers import NestedVLANSerializer -from netbox.api.fields import ChoiceField -from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer -from wireless.choices import * -from wireless.models import * +from .serializers_.wirelesslans import * +from .serializers_.wirelesslinks import * from .nested_serializers import * - -__all__ = ( - 'WirelessLANGroupSerializer', - 'WirelessLANSerializer', - 'WirelessLinkSerializer', -) - - -class WirelessLANGroupSerializer(NestedGroupModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') - parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None) - wirelesslan_count = serializers.IntegerField(read_only=True) - - class Meta: - model = WirelessLANGroup - fields = [ - 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', - 'last_updated', 'wirelesslan_count', '_depth', - ] - brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth') - - -class WirelessLANSerializer(NetBoxModelSerializer): - url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') - group = NestedWirelessLANGroupSerializer(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) - auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) - auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) - - class Meta: - model = WirelessLAN - fields = [ - 'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type', - 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'ssid', 'description') - - -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) - auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True) - auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True) - - class Meta: - model = WirelessLink - fields = [ - 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', - 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - ] - brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/api/serializers_/__init__.py b/netbox/wireless/api/serializers_/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/wireless/api/serializers_/wirelesslans.py b/netbox/wireless/api/serializers_/wirelesslans.py new file mode 100644 index 000000000..d4356c881 --- /dev/null +++ b/netbox/wireless/api/serializers_/wirelesslans.py @@ -0,0 +1,46 @@ +from rest_framework import serializers + +from ipam.api.serializers_.vlans import VLANSerializer +from netbox.api.fields import ChoiceField +from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from wireless.choices import * +from wireless.models import WirelessLAN, WirelessLANGroup +from ..nested_serializers import * + +__all__ = ( + 'WirelessLANGroupSerializer', + 'WirelessLANSerializer', +) + + +class WirelessLANGroupSerializer(NestedGroupModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail') + parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None) + wirelesslan_count = serializers.IntegerField(read_only=True) + + class Meta: + model = WirelessLANGroup + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created', + 'last_updated', 'wirelesslan_count', '_depth', + ] + brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth') + + +class WirelessLANSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail') + group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True) + status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=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) + + class Meta: + model = WirelessLAN + fields = [ + 'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'ssid', 'description') diff --git a/netbox/wireless/api/serializers_/wirelesslinks.py b/netbox/wireless/api/serializers_/wirelesslinks.py new file mode 100644 index 000000000..3a7f88856 --- /dev/null +++ b/netbox/wireless/api/serializers_/wirelesslinks.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from dcim.api.serializers_.device_components import InterfaceSerializer +from dcim.choices import LinkStatusChoices +from netbox.api.fields import ChoiceField +from netbox.api.serializers import NetBoxModelSerializer +from tenancy.api.serializers_.tenants import TenantSerializer +from wireless.choices import * +from wireless.models import WirelessLink + +__all__ = ( + 'WirelessLinkSerializer', +) + + +class WirelessLinkSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail') + status = ChoiceField(choices=LinkStatusChoices, required=False) + 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) + + class Meta: + model = WirelessLink + fields = [ + 'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type', + 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + ] + brief_fields = ('id', 'url', 'display', 'ssid', 'description')