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 7ecee01f8..81535a147 100644 --- a/netbox/extras/api/customfields.py +++ b/netbox/extras/api/customfields.py @@ -1,13 +1,12 @@ 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 core.models import ObjectType 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 46189cf4e..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 ObjectType -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') - object_types = ContentTypeField( - queryset=ObjectType.objects.with_feature('event_rules'), - many=True - ) - action_type = ChoiceField(choices=EventRuleActionChoices) - action_object_type = ContentTypeField( - queryset=ObjectType.objects.with_feature('event_rules'), - ) - action_object = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = EventRule - fields = [ - 'id', 'url', 'display', 'object_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') - object_types = ContentTypeField( - queryset=ObjectType.objects.with_feature('custom_fields'), - many=True - ) - type = ChoiceField(choices=CustomFieldTypeChoices) - object_type = ContentTypeField( - queryset=ObjectType.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', 'object_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') - object_types = ContentTypeField( - queryset=ObjectType.objects.with_feature('custom_links'), - many=True - ) - - class Meta: - model = CustomLink - fields = [ - 'id', 'url', 'display', 'object_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') - object_types = ContentTypeField( - queryset=ObjectType.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', 'object_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') - object_types = ContentTypeField( - queryset=ObjectType.objects.all(), - many=True - ) - - class Meta: - model = SavedFilter - fields = [ - 'id', 'url', 'display', 'object_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=ObjectType.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=ObjectType.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') - object_type = ContentTypeField( - queryset=ObjectType.objects.all() - ) - parent = serializers.SerializerMethodField(read_only=True) - - class Meta: - model = ImageAttachment - fields = [ - 'id', 'url', 'display', 'object_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['object_type'].get_object_for_this_type(id=data['object_id']) - except ObjectDoesNotExist: - raise serializers.ValidationError( - "Invalid parent object: {} ID {}".format(data['object_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=ObjectType.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 = ObjectType - 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..bcf3a24ec --- /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 ObjectType +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') + object_type = ContentTypeField( + queryset=ObjectType.objects.all() + ) + parent = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = ImageAttachment + fields = [ + 'id', 'url', 'display', 'object_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['object_type'].get_object_for_this_type(id=data['object_id']) + except ObjectDoesNotExist: + raise serializers.ValidationError( + "Invalid parent object: {} ID {}".format(data['object_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..7a2d4d6aa --- /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 ObjectType +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=ObjectType.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..75d25a5bf --- /dev/null +++ b/netbox/extras/api/serializers_/contenttypes.py @@ -0,0 +1,16 @@ +from rest_framework import serializers + +from core.models import ObjectType +from netbox.api.serializers import BaseModelSerializer + +__all__ = ( + 'ContentTypeSerializer', +) + + +class ContentTypeSerializer(BaseModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail') + + class Meta: + model = ObjectType + 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..efd6db063 --- /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 ObjectType +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') + object_types = ContentTypeField( + queryset=ObjectType.objects.with_feature('custom_fields'), + many=True + ) + type = ChoiceField(choices=CustomFieldTypeChoices) + object_type = ContentTypeField( + queryset=ObjectType.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', 'object_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..8635ea2a0 --- /dev/null +++ b/netbox/extras/api/serializers_/customlinks.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ObjectType +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') + object_types = ContentTypeField( + queryset=ObjectType.objects.with_feature('custom_links'), + many=True + ) + + class Meta: + model = CustomLink + fields = [ + 'id', 'url', 'display', 'object_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..4285b12e6 --- /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 ObjectType +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') + object_types = ContentTypeField( + queryset=ObjectType.objects.with_feature('event_rules'), + many=True + ) + action_type = ChoiceField(choices=EventRuleActionChoices) + action_object_type = ContentTypeField( + queryset=ObjectType.objects.with_feature('event_rules'), + ) + action_object = serializers.SerializerMethodField(read_only=True) + + class Meta: + model = EventRule + fields = [ + 'id', 'url', 'display', 'object_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..43cc061a7 --- /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 ObjectType +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') + object_types = ContentTypeField( + queryset=ObjectType.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', 'object_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..46ab0477b --- /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 ObjectType +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=ObjectType.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..9e26f0c30 --- /dev/null +++ b/netbox/extras/api/serializers_/savedfilters.py @@ -0,0 +1,26 @@ +from rest_framework import serializers + +from core.models import ObjectType +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') + object_types = ContentTypeField( + queryset=ObjectType.objects.all(), + many=True + ) + + class Meta: + model = SavedFilter + fields = [ + 'id', 'url', 'display', 'object_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..9d91ba5e1 --- /dev/null +++ b/netbox/extras/api/serializers_/tags.py @@ -0,0 +1,30 @@ +from rest_framework import serializers + +from core.models import ObjectType +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=ObjectType.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/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css index e016f0923..95ac3c1b1 100644 Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ diff --git a/netbox/project-static/dist/netbox.js b/netbox/project-static/dist/netbox.js index 30c0d1674..07e7d30b4 100644 Binary files a/netbox/project-static/dist/netbox.js and b/netbox/project-static/dist/netbox.js differ diff --git a/netbox/project-static/dist/netbox.js.map b/netbox/project-static/dist/netbox.js.map index bd26af093..04cebd4c2 100644 Binary files a/netbox/project-static/dist/netbox.js.map and b/netbox/project-static/dist/netbox.js.map differ diff --git a/netbox/project-static/img/tint_20.png b/netbox/project-static/img/tint_20.png deleted file mode 100644 index a03a1f9ac..000000000 Binary files a/netbox/project-static/img/tint_20.png and /dev/null differ diff --git a/netbox/project-static/src/objectSelector.ts b/netbox/project-static/src/objectSelector.ts index 9de6c1750..633f5038a 100644 --- a/netbox/project-static/src/objectSelector.ts +++ b/netbox/project-static/src/objectSelector.ts @@ -18,11 +18,12 @@ function handleSelection(link: HTMLAnchorElement): void { const value = link.getAttribute('data-value'); //@ts-ignore - target.slim.setData([ - {text: label, value: value} - ]); - const change = new Event('change'); - target.dispatchEvent(change); + target.tomselect.addOption({ + id: value, + display: label, + }); + //@ts-ignore + target.tomselect.addItem(value); } diff --git a/netbox/project-static/src/select/dynamic.ts b/netbox/project-static/src/select/dynamic.ts index 20912140b..10ce955c2 100644 --- a/netbox/project-static/src/select/dynamic.ts +++ b/netbox/project-static/src/select/dynamic.ts @@ -42,7 +42,7 @@ function renderItem(data: TomOption, escape: typeof escape_html) { // Initialize elements with statically-defined options export function initStaticSelects(): void { for (const select of getElements( - 'select:not(.api-select):not(.color-select)', + 'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)', )) { new TomSelect(select, { ...config, @@ -24,7 +24,7 @@ export function initColorSelects(): void { )}"> ${escape(item.text)}`; } - for (const select of getElements('select.color-select')) { + for (const select of getElements('select.color-select:not(.tomselected)')) { new TomSelect(select, { ...config, maxOptions: undefined, diff --git a/netbox/project-static/src/util.ts b/netbox/project-static/src/util.ts index e1ada2e19..3aa8b6676 100644 --- a/netbox/project-static/src/util.ts +++ b/netbox/project-static/src/util.ts @@ -244,29 +244,6 @@ export function getSelectedOptions( return selected; } -/** - * Get data that can only be accessed via Django context, and is thus already rendered in the HTML - * template. - * - * @see Templates requiring Django context data have a `{% block data %}` block. - * - * @param key Property name, which must exist on the HTML element. If not already prefixed with - * `data-`, `data-` will be prepended to the property. - * @returns Value if it exists, `null` if not. - */ -export function getNetboxData(key: string): string | null { - if (!key.startsWith('data-')) { - key = `data-${key}`; - } - for (const element of getElements('body > div#netbox-data > *')) { - const value = element.getAttribute(key); - if (isTruthy(value)) { - return value; - } - } - return null; -} - /** * Toggle visibility of an element. */ diff --git a/netbox/project-static/styles/custom/_markdown.scss b/netbox/project-static/styles/custom/_markdown.scss index 08de23581..cb4527f37 100644 --- a/netbox/project-static/styles/custom/_markdown.scss +++ b/netbox/project-static/styles/custom/_markdown.scss @@ -28,6 +28,13 @@ } +// Remove the bottom margin of

elements inside a table cell +td > .rendered-markdown { + p:last-of-type { + margin-bottom: 0; + } +} + // Markdown preview .markdown-widget { .preview { diff --git a/netbox/project-static/styles/custom/_misc.scss b/netbox/project-static/styles/custom/_misc.scss index ebf66d547..9779bf583 100644 --- a/netbox/project-static/styles/custom/_misc.scss +++ b/netbox/project-static/styles/custom/_misc.scss @@ -2,7 +2,7 @@ // Color labels span.color-label { - display: block; + display: inline-block; width: 5rem; height: 1rem; padding: $badge-padding-y $badge-padding-x; diff --git a/netbox/project-static/styles/overrides/_tabler.scss b/netbox/project-static/styles/overrides/_tabler.scss index a5ae3c647..f855daf0c 100644 --- a/netbox/project-static/styles/overrides/_tabler.scss +++ b/netbox/project-static/styles/overrides/_tabler.scss @@ -9,6 +9,10 @@ pre { // Tabler sets display: flex display: inline-block; } +.btn-sm { + // $border-radius-sm (2px) is too small + border-radius: $border-radius; +} // Tabs .nav-tabs { diff --git a/netbox/project-static/styles/transitional/_tables.scss b/netbox/project-static/styles/transitional/_tables.scss index 6ac17c59c..0af11f9cd 100644 --- a/netbox/project-static/styles/transitional/_tables.scss +++ b/netbox/project-static/styles/transitional/_tables.scss @@ -23,7 +23,6 @@ table.attr-table { // Restyle row header th { - color: $gray-700; font-weight: normal; width: min-content; } diff --git a/netbox/templates/base/base.html b/netbox/templates/base/base.html index b7d4f6fc6..1c58047ef 100644 --- a/netbox/templates/base/base.html +++ b/netbox/templates/base/base.html @@ -70,10 +70,5 @@ {# User messages #} {% include 'inc/messages.html' %} - {# Data container #} -

- {% block data %}{% endblock %} -
- diff --git a/netbox/templates/dcim/rack.html b/netbox/templates/dcim/rack.html index 97be5e839..b28ee6e31 100644 --- a/netbox/templates/dcim/rack.html +++ b/netbox/templates/dcim/rack.html @@ -163,7 +163,7 @@
- diff --git a/netbox/templates/dcim/rack_elevation_list.html b/netbox/templates/dcim/rack_elevation_list.html index 752fe6913..e50380ba0 100644 --- a/netbox/templates/dcim/rack_elevation_list.html +++ b/netbox/templates/dcim/rack_elevation_list.html @@ -11,13 +11,11 @@ {% trans "View List" %} -
- -
+
{% trans "Front" %} {% trans "Rear" %} diff --git a/netbox/templates/inc/toast.html b/netbox/templates/inc/toast.html index 85eff2d7a..0cf04b93b 100644 --- a/netbox/templates/inc/toast.html +++ b/netbox/templates/inc/toast.html @@ -1,6 +1,6 @@ {% load helpers %} -