Selaa lähdekoodia

Refactor REST API serializers to avoid circular imports

Jeremy Stretch 1 vuosi sitten
vanhempi
commit
c382ba0ae0
71 muutettua tiedostoa jossa 4096 lisäystä ja 3616 poistoa
  1. 2 143
      netbox/circuits/api/serializers.py
  2. 0 0
      netbox/circuits/api/serializers_/__init__.py
  3. 81 0
      netbox/circuits/api/serializers_/circuits.py
  4. 67 0
      netbox/circuits/api/serializers_/providers.py
  5. 3 75
      netbox/core/api/serializers.py
  6. 0 0
      netbox/core/api/serializers_/__init__.py
  7. 53 0
      netbox/core/api/serializers_/data.py
  8. 31 0
      netbox/core/api/serializers_/jobs.py
  9. 13 1364
      netbox/dcim/api/serializers.py
  10. 0 0
      netbox/dcim/api/serializers_/__init__.py
  11. 38 0
      netbox/dcim/api/serializers_/base.py
  12. 127 0
      netbox/dcim/api/serializers_/cables.py
  13. 366 0
      netbox/dcim/api/serializers_/device_components.py
  14. 165 0
      netbox/dcim/api/serializers_/devices.py
  15. 328 0
      netbox/dcim/api/serializers_/devicetype_components.py
  16. 74 0
      netbox/dcim/api/serializers_/devicetypes.py
  17. 26 0
      netbox/dcim/api/serializers_/manufacturers.py
  18. 29 0
      netbox/dcim/api/serializers_/platforms.py
  19. 80 0
      netbox/dcim/api/serializers_/power.py
  20. 117 0
      netbox/dcim/api/serializers_/racks.py
  21. 31 0
      netbox/dcim/api/serializers_/rackunits.py
  22. 43 0
      netbox/dcim/api/serializers_/roles.py
  23. 97 0
      netbox/dcim/api/serializers_/sites.py
  24. 25 0
      netbox/dcim/api/serializers_/virtualchassis.py
  25. 14 665
      netbox/extras/api/serializers.py
  26. 0 0
      netbox/extras/api/serializers_/__init__.py
  27. 50 0
      netbox/extras/api/serializers_/attachments.py
  28. 35 0
      netbox/extras/api/serializers_/bookmarks.py
  29. 59 0
      netbox/extras/api/serializers_/change_logging.py
  30. 16 0
      netbox/extras/api/serializers_/contenttypes.py
  31. 91 0
      netbox/extras/api/serializers_/customfields.py
  32. 26 0
      netbox/extras/api/serializers_/customlinks.py
  33. 13 0
      netbox/extras/api/serializers_/dashboard.py
  34. 75 0
      netbox/extras/api/serializers_/events.py
  35. 36 0
      netbox/extras/api/serializers_/exporttemplates.py
  36. 67 0
      netbox/extras/api/serializers_/journaling.py
  37. 147 0
      netbox/extras/api/serializers_/provisioning.py
  38. 26 0
      netbox/extras/api/serializers_/savedfilters.py
  39. 77 0
      netbox/extras/api/serializers_/scripts.py
  40. 30 0
      netbox/extras/api/serializers_/tags.py
  41. 7 510
      netbox/ipam/api/serializers.py
  42. 0 0
      netbox/ipam/api/serializers_/__init__.py
  43. 78 0
      netbox/ipam/api/serializers_/asns.py
  44. 53 0
      netbox/ipam/api/serializers_/fhrpgroups.py
  45. 200 0
      netbox/ipam/api/serializers_/ip.py
  46. 25 0
      netbox/ipam/api/serializers_/roles.py
  47. 48 0
      netbox/ipam/api/serializers_/services.py
  48. 115 0
      netbox/ipam/api/serializers_/vlans.py
  49. 55 0
      netbox/ipam/api/serializers_/vrfs.py
  50. 2 122
      netbox/tenancy/api/serializers.py
  51. 0 0
      netbox/tenancy/api/serializers_/__init__.py
  52. 82 0
      netbox/tenancy/api/serializers_/contacts.py
  53. 51 0
      netbox/tenancy/api/serializers_/tenants.py
  54. 3 189
      netbox/users/api/serializers.py
  55. 0 0
      netbox/users/api/serializers_/__init__.py
  56. 43 0
      netbox/users/api/serializers_/permissions.py
  57. 94 0
      netbox/users/api/serializers_/tokens.py
  58. 72 0
      netbox/users/api/serializers_/users.py
  59. 2 186
      netbox/virtualization/api/serializers.py
  60. 0 0
      netbox/virtualization/api/serializers_/__init__.py
  61. 65 0
      netbox/virtualization/api/serializers_/clusters.py
  62. 143 0
      netbox/virtualization/api/serializers_/virtualmachines.py
  63. 3 296
      netbox/vpn/api/serializers.py
  64. 0 0
      netbox/vpn/api/serializers_/__init__.py
  65. 135 0
      netbox/vpn/api/serializers_/crypto.py
  66. 69 0
      netbox/vpn/api/serializers_/l2vpn.py
  67. 114 0
      netbox/vpn/api/serializers_/tunnels.py
  68. 2 66
      netbox/wireless/api/serializers.py
  69. 0 0
      netbox/wireless/api/serializers_/__init__.py
  70. 46 0
      netbox/wireless/api/serializers_/wirelesslans.py
  71. 31 0
      netbox/wireless/api/serializers_/wirelesslinks.py

+ 2 - 143
netbox/circuits/api/serializers.py

@@ -1,144 +1,3 @@
-from rest_framework import serializers
-
-from circuits.choices import CircuitStatusChoices
-from circuits.models import *
-from dcim.api.serializers import CabledObjectSerializer, SiteSerializer
-from ipam.api.nested_serializers import NestedASNSerializer
-from ipam.models import ASN
-from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
-from tenancy.api.serializers import TenantSerializer
+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 = 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')
-
-
-#
-# Provider networks
-#
-
-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')
-
-
-#
-# 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 = 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')

+ 0 - 0
netbox/circuits/api/serializers_/__init__.py


+ 81 - 0
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')

+ 67 - 0
netbox/circuits/api/serializers_/providers.py

@@ -0,0 +1,67 @@
+from rest_framework import serializers
+
+from circuits.models import Provider, ProviderAccount, ProviderNetwork
+from ipam.api.nested_serializers import NestedASNSerializer
+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=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')
+
+
+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')

+ 3 - 75
netbox/core/api/serializers.py

@@ -1,75 +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.serializers import UserSerializer
-
-__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 = 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')
-
-
-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')
+from .serializers_.data import *
+from .serializers_.jobs import *
+from .nested_serializers import *

+ 0 - 0
netbox/core/api/serializers_/__init__.py


+ 53 - 0
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')

+ 31 - 0
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')

+ 13 - 1364
netbox/dcim/api/serializers.py

@@ -1,1365 +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.serializers import ConfigTemplateSerializer
-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.serializers import TenantSerializer
-from users.api.serializers import UserSerializer
-from utilities.api import get_serializer_for_model
-from virtualization.api.nested_serializers import NestedClusterSerializer
-from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
-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 *
-
-
-#
-# 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 = 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, 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
-
-
-class CabledObjectSerializer(serializers.ModelSerializer):
-    cable = CableSerializer(nested=True, read_only=True, allow_null=True)
-    cable_end = serializers.CharField(read_only=True)
-    link_peers_type = serializers.SerializerMethodField(read_only=True)
-    link_peers = serializers.SerializerMethodField(read_only=True)
-    _occupied = serializers.SerializerMethodField(read_only=True)
-
-    @extend_schema_field(OpenApiTypes.STR)
-    def get_link_peers_type(self, obj):
-        """
-        Return the type of the peer link terminations, or None.
-        """
-        if not obj.cable:
-            return None
-
-        if obj.link_peers:
-            return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}'
-
-        return None
-
-    @extend_schema_field(serializers.ListField)
-    def get_link_peers(self, obj):
-        """
-        Return the appropriate serializer for the link termination model.
-        """
-        if not obj.link_peers:
-            return []
-
-        # Return serialized peer termination objects
-        serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(obj.link_peers, context=context, many=True).data
-
-    @extend_schema_field(serializers.BooleanField)
-    def get__occupied(self, obj):
-        return obj._occupied
-
-
-#
-# Regions/sites
-#
-
-class RegionSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:region-detail')
-    parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
-    site_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = Region
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'site_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
-
-
-class SiteGroupSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:sitegroup-detail')
-    parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
-    site_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = SiteGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'site_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'site_count', '_depth')
-
-
-class SiteSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:site-detail')
-    status = ChoiceField(choices=SiteStatusChoices, required=False)
-    region = RegionSerializer(nested=True, required=False, allow_null=True)
-    group = SiteGroupSerializer(nested=True, required=False, allow_null=True)
-    tenant = TenantSerializer(required=False, allow_null=True)
-    time_zone = TimeZoneSerializerField(required=False, allow_null=True)
-    asns = SerializedPKRelatedField(
-        queryset=ASN.objects.all(),
-        serializer=NestedASNSerializer,
-        required=False,
-        many=True
-    )
-
-    # Related object counts
-    circuit_count = RelatedObjectCountField('circuit_terminations')
-    device_count = RelatedObjectCountField('devices')
-    prefix_count = RelatedObjectCountField('prefixes')
-    rack_count = RelatedObjectCountField('racks')
-    vlan_count = RelatedObjectCountField('vlans')
-    virtualmachine_count = RelatedObjectCountField('virtual_machines')
-
-    class Meta:
-        model = Site
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
-            'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
-            'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
-            'virtualmachine_count', 'vlan_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', 'slug')
-
-
-#
-# Racks
-#
-
-class LocationSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:location-detail')
-    site = SiteSerializer(nested=True)
-    parent = NestedLocationSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=LocationStatusChoices, required=False)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    rack_count = serializers.IntegerField(read_only=True)
-    device_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = Location
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'site', 'parent', 'status', 'tenant', 'description', 'tags',
-            'custom_fields', 'created', 'last_updated', 'rack_count', 'device_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count', '_depth')
-
-
-class RackRoleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackrole-detail')
-
-    # Related object counts
-    rack_count = RelatedObjectCountField('racks')
-
-    class Meta:
-        model = RackRole
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'rack_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'rack_count')
-
-
-class RackSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rack-detail')
-    site = SiteSerializer(nested=True)
-    location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    status = ChoiceField(choices=RackStatusChoices, required=False)
-    role = RackRoleSerializer(nested=True, required=False, allow_null=True)
-    type = ChoiceField(choices=RackTypeChoices, allow_blank=True, required=False, allow_null=True)
-    facility_id = serializers.CharField(max_length=50, allow_blank=True, allow_null=True, label=_('Facility ID'),
-                                        default=None)
-    width = ChoiceField(choices=RackWidthChoices, required=False)
-    outer_unit = ChoiceField(choices=RackDimensionUnitChoices, allow_blank=True, required=False, allow_null=True)
-    weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
-
-    # Related object counts
-    device_count = RelatedObjectCountField('devices')
-    powerfeed_count = RelatedObjectCountField('powerfeeds')
-
-    class Meta:
-        model = Rack
-        fields = [
-            'id', 'url', 'display', 'name', 'facility_id', 'site', 'location', 'tenant', 'status', 'role', 'serial',
-            'asset_tag', 'type', 'width', 'u_height', 'starting_unit', 'weight', 'max_weight', 'weight_unit',
-            'desc_units', 'outer_width', 'outer_depth', 'outer_unit', 'mounting_depth', 'description', 'comments',
-            'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'powerfeed_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', 'device_count')
-
-
-class RackReservationSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
-    rack = RackSerializer(nested=True)
-    user = UserSerializer(nested=True)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-
-    class Meta:
-        model = RackReservation
-        fields = [
-            'id', 'url', 'display', 'rack', 'units', 'created', 'last_updated', 'user', 'tenant', 'description',
-            'comments', 'tags', 'custom_fields',
-        ]
-        brief_fields = ('id', 'url', 'display', 'user', 'description', 'units')
-
-
-class RackElevationDetailFilterSerializer(serializers.Serializer):
-    q = serializers.CharField(
-        required=False,
-        default=None
-    )
-    face = serializers.ChoiceField(
-        choices=DeviceFaceChoices,
-        default=DeviceFaceChoices.FACE_FRONT
-    )
-    render = serializers.ChoiceField(
-        choices=RackElevationDetailRenderChoices,
-        default=RackElevationDetailRenderChoices.RENDER_JSON
-    )
-    unit_width = serializers.IntegerField(
-        default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_WIDTH')
-    )
-    unit_height = serializers.IntegerField(
-        default=ConfigItem('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT')
-    )
-    legend_width = serializers.IntegerField(
-        default=RACK_ELEVATION_DEFAULT_LEGEND_WIDTH
-    )
-    margin_width = serializers.IntegerField(
-        default=RACK_ELEVATION_DEFAULT_MARGIN_WIDTH
-    )
-    exclude = serializers.IntegerField(
-        required=False,
-        default=None
-    )
-    expand_devices = serializers.BooleanField(
-        required=False,
-        default=True
-    )
-    include_images = serializers.BooleanField(
-        required=False,
-        default=True
-    )
-
-
-class ManufacturerSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:manufacturer-detail')
-
-    # Related object counts
-    devicetype_count = RelatedObjectCountField('device_types')
-    inventoryitem_count = RelatedObjectCountField('inventory_items')
-    platform_count = RelatedObjectCountField('platforms')
-
-    class Meta:
-        model = Manufacturer
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'devicetype_count', 'inventoryitem_count', 'platform_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'devicetype_count')
-
-
-class DeviceRoleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicerole-detail')
-    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    # Related object counts
-    device_count = RelatedObjectCountField('devices')
-    virtualmachine_count = RelatedObjectCountField('virtual_machines')
-
-    class Meta:
-        model = DeviceRole
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
-            'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
-
-
-class InventoryItemRoleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemrole-detail')
-
-    # Related object counts
-    inventoryitem_count = RelatedObjectCountField('inventory_items')
-
-    class Meta:
-        model = InventoryItemRole
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'inventoryitem_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'inventoryitem_count')
-
-
-class PlatformSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:platform-detail')
-    manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True)
-    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    # Related object counts
-    device_count = RelatedObjectCountField('devices')
-    virtualmachine_count = RelatedObjectCountField('virtual_machines')
-
-    class Meta:
-        model = Platform
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'description', 'tags',
-            'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'device_count', 'virtualmachine_count')
-
-
-#
-# Device/module types
-#
-
-class DeviceTypeSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicetype-detail')
-    manufacturer = ManufacturerSerializer(nested=True)
-    default_platform = PlatformSerializer(nested=True, required=False, allow_null=True)
-    u_height = serializers.DecimalField(
-        max_digits=4,
-        decimal_places=1,
-        label=_('Position (U)'),
-        min_value=0,
-        default=1.0
-    )
-    subdevice_role = ChoiceField(choices=SubdeviceRoleChoices, allow_blank=True, required=False, allow_null=True)
-    airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False, allow_null=True)
-    weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
-    front_image = serializers.URLField(allow_null=True, required=False)
-    rear_image = serializers.URLField(allow_null=True, required=False)
-
-    # Counter fields
-    console_port_template_count = serializers.IntegerField(read_only=True)
-    console_server_port_template_count = serializers.IntegerField(read_only=True)
-    power_port_template_count = serializers.IntegerField(read_only=True)
-    power_outlet_template_count = serializers.IntegerField(read_only=True)
-    interface_template_count = serializers.IntegerField(read_only=True)
-    front_port_template_count = serializers.IntegerField(read_only=True)
-    rear_port_template_count = serializers.IntegerField(read_only=True)
-    device_bay_template_count = serializers.IntegerField(read_only=True)
-    module_bay_template_count = serializers.IntegerField(read_only=True)
-    inventory_item_template_count = serializers.IntegerField(read_only=True)
-
-    # Related object counts
-    device_count = RelatedObjectCountField('instances')
-
-    class Meta:
-        model = DeviceType
-        fields = [
-            'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
-            'exclude_from_utilization', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
-            'front_image', 'rear_image', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-            'device_count', 'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
-            'power_outlet_template_count', 'interface_template_count', 'front_port_template_count',
-            'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count',
-            'inventory_item_template_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'slug', 'description', 'device_count')
-
-
-class ModuleTypeSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:moduletype-detail')
-    manufacturer = ManufacturerSerializer(nested=True)
-    weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
-
-    class Meta:
-        model = ModuleType
-        fields = [
-            'id', 'url', 'display', 'manufacturer', 'model', 'part_number', 'weight', 'weight_unit', 'description',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'manufacturer', 'model', 'description')
-
-
-#
-# Component templates
-#
-
-class ConsolePortTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleporttemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(
-        choices=ConsolePortTypeChoices,
-        allow_blank=True,
-        required=False
-    )
-
-    class Meta:
-        model = ConsolePortTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
-            'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class ConsoleServerPortTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverporttemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(
-        choices=ConsolePortTypeChoices,
-        allow_blank=True,
-        required=False
-    )
-
-    class Meta:
-        model = ConsoleServerPortTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'description', 'created',
-            'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class PowerPortTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerporttemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(
-        choices=PowerPortTypeChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = PowerPortTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'maximum_draw',
-            'allocated_draw', 'description', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class PowerOutletTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlettemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(
-        choices=PowerOutletTypeChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-    power_port = PowerPortTemplateSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-    feed_leg = ChoiceField(
-        choices=PowerOutletFeedLegChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = PowerOutletTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'power_port', 'feed_leg',
-            'description', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class InterfaceTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interfacetemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(choices=InterfaceTypeChoices)
-    bridge = NestedInterfaceTemplateSerializer(
-        required=False,
-        allow_null=True
-    )
-    poe_mode = ChoiceField(
-        choices=InterfacePoEModeChoices,
-        required=False,
-        allow_blank=True,
-        allow_null=True
-    )
-    poe_type = ChoiceField(
-        choices=InterfacePoETypeChoices,
-        required=False,
-        allow_blank=True,
-        allow_null=True
-    )
-    rf_role = ChoiceField(
-        choices=WirelessRoleChoices,
-        required=False,
-        allow_blank=True,
-        allow_null=True
-    )
-
-    class Meta:
-        model = InterfaceTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
-            'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class RearPortTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearporttemplate-detail')
-    device_type = DeviceTypeSerializer(
-        required=False,
-        nested=True,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(choices=PortTypeChoices)
-
-    class Meta:
-        model = RearPortTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'positions',
-            'description', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class FrontPortTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontporttemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    module_type = ModuleTypeSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    type = ChoiceField(choices=PortTypeChoices)
-    rear_port = RearPortTemplateSerializer(nested=True)
-
-    class Meta:
-        model = FrontPortTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'color', 'rear_port',
-            'rear_port_position', 'description', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class ModuleBayTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebaytemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True
-    )
-
-    class Meta:
-        model = ModuleBayTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'name', 'label', 'position', 'description', 'created',
-            'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class DeviceBayTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebaytemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True
-    )
-
-    class Meta:
-        model = DeviceBayTemplate
-        fields = ['id', 'url', 'display', 'device_type', 'name', 'label', 'description', 'created', 'last_updated']
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class InventoryItemTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitemtemplate-detail')
-    device_type = DeviceTypeSerializer(
-        nested=True
-    )
-    parent = serializers.PrimaryKeyRelatedField(
-        queryset=InventoryItemTemplate.objects.all(),
-        allow_null=True,
-        default=None
-    )
-    role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
-    manufacturer = ManufacturerSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    component_type = ContentTypeField(
-        queryset=ContentType.objects.filter(MODULAR_COMPONENT_TEMPLATE_MODELS),
-        required=False,
-        allow_null=True
-    )
-    component = serializers.SerializerMethodField(read_only=True)
-    _depth = serializers.IntegerField(source='level', read_only=True)
-
-    class Meta:
-        model = InventoryItemTemplate
-        fields = [
-            'id', 'url', 'display', 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id',
-            'description', 'component_type', 'component_id', 'component', 'created', 'last_updated', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', '_depth')
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_component(self, obj):
-        if obj.component is None:
-            return None
-        serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(obj.component, context=context).data
-
-
-#
-# Virtual chassis
-#
-
-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')
-
-
-#
-# Devices
-#
-
-class DeviceSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:device-detail')
-    device_type = DeviceTypeSerializer(nested=True)
-    role = DeviceRoleSerializer(nested=True)
-    device_role = DeviceRoleSerializer(
-        nested=True,
-        read_only=True,
-        help_text='Deprecated in v3.6 in favor of `role`.'
-    )
-    tenant = TenantSerializer(
-        nested=True,
-        required=False,
-        allow_null=True,
-        default=None
-    )
-    platform = PlatformSerializer(nested=True, required=False, allow_null=True)
-    site = SiteSerializer(nested=True)
-    location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
-    rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
-    face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
-    position = serializers.DecimalField(
-        max_digits=4,
-        decimal_places=1,
-        allow_null=True,
-        label=_('Position (U)'),
-        min_value=decimal.Decimal(0.5),
-        default=None
-    )
-    status = ChoiceField(choices=DeviceStatusChoices, required=False)
-    airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
-    primary_ip = NestedIPAddressSerializer(read_only=True)
-    primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
-    primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
-    oob_ip = NestedIPAddressSerializer(required=False, allow_null=True)
-    parent_device = serializers.SerializerMethodField()
-    cluster = NestedClusterSerializer(required=False, allow_null=True)
-    virtual_chassis = VirtualChassisSerializer(nested=True, required=False, allow_null=True, default=None)
-    vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
-    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    # Counter fields
-    console_port_count = serializers.IntegerField(read_only=True)
-    console_server_port_count = serializers.IntegerField(read_only=True)
-    power_port_count = serializers.IntegerField(read_only=True)
-    power_outlet_count = serializers.IntegerField(read_only=True)
-    interface_count = serializers.IntegerField(read_only=True)
-    front_port_count = serializers.IntegerField(read_only=True)
-    rear_port_count = serializers.IntegerField(read_only=True)
-    device_bay_count = serializers.IntegerField(read_only=True)
-    module_bay_count = serializers.IntegerField(read_only=True)
-    inventory_item_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = Device
-        fields = [
-            'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
-            'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
-            'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
-            'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
-            'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
-            'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
-            'device_bay_count', 'module_bay_count', 'inventory_item_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-    @extend_schema_field(NestedDeviceSerializer)
-    def get_parent_device(self, obj):
-        try:
-            device_bay = obj.parent_bay
-        except DeviceBay.DoesNotExist:
-            return None
-        context = {'request': self.context['request']}
-        data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
-        data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
-        return data
-
-    def get_device_role(self, obj):
-        return obj.role
-
-
-class DeviceWithConfigContextSerializer(DeviceSerializer):
-    config_context = serializers.SerializerMethodField(read_only=True)
-
-    class Meta(DeviceSerializer.Meta):
-        fields = [
-            'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
-            'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
-            'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
-            'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
-            'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
-            'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
-            'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
-        ]
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_config_context(self, obj):
-        return obj.get_config_context()
-
-
-class VirtualDeviceContextSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
-    device = DeviceSerializer(nested=True)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
-    primary_ip = NestedIPAddressSerializer(read_only=True, allow_null=True)
-    primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
-    primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
-    status = ChoiceField(choices=VirtualDeviceContextStatusChoices)
-
-    # Related object counts
-    interface_count = RelatedObjectCountField('interfaces')
-
-    class Meta:
-        model = VirtualDeviceContext
-        fields = [
-            'id', 'url', 'display', 'name', 'device', 'identifier', 'tenant', 'primary_ip', 'primary_ip4',
-            'primary_ip6', 'status', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-            'interface_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'identifier', 'device', 'description')
-
-
-class ModuleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:module-detail')
-    device = DeviceSerializer(nested=True)
-    module_bay = NestedModuleBaySerializer()
-    module_type = ModuleTypeSerializer(nested=True)
-    status = ChoiceField(choices=ModuleStatusChoices, required=False)
-
-    class Meta:
-        model = Module
-        fields = [
-            'id', 'url', 'display', 'device', 'module_bay', 'module_type', 'status', 'serial', 'asset_tag',
-            'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'module_bay', 'module_type', 'description')
-
-
-#
-# Device components
-#
-
-class ConnectedEndpointsSerializer(serializers.ModelSerializer):
-    """
-    Legacy serializer for pre-v3.3 connections
-    """
-    connected_endpoints_type = serializers.SerializerMethodField(read_only=True)
-    connected_endpoints = serializers.SerializerMethodField(read_only=True)
-    connected_endpoints_reachable = serializers.SerializerMethodField(read_only=True)
-
-    @extend_schema_field(OpenApiTypes.STR)
-    def get_connected_endpoints_type(self, obj):
-        if endpoints := obj.connected_endpoints:
-            return f'{endpoints[0]._meta.app_label}.{endpoints[0]._meta.model_name}'
-
-    @extend_schema_field(serializers.ListField)
-    def get_connected_endpoints(self, obj):
-        """
-        Return the appropriate serializer for the type of connected object.
-        """
-        if endpoints := obj.connected_endpoints:
-            serializer = get_serializer_for_model(endpoints[0], prefix=NESTED_SERIALIZER_PREFIX)
-            context = {'request': self.context['request']}
-            return serializer(endpoints, many=True, context=context).data
-
-    @extend_schema_field(serializers.BooleanField)
-    def get_connected_endpoints_reachable(self, obj):
-        return obj._path and obj._path.is_complete and obj._path.is_active
-
-
-class ConsoleServerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleserverport-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(
-        choices=ConsolePortTypeChoices,
-        allow_blank=True,
-        required=False
-    )
-    speed = ChoiceField(
-        choices=ConsolePortSpeedChoices,
-        allow_null=True,
-        required=False
-    )
-
-    class Meta:
-        model = ConsoleServerPort
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
-            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
-            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
-            'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(
-        choices=ConsolePortTypeChoices,
-        allow_blank=True,
-        required=False
-    )
-    speed = ChoiceField(
-        choices=ConsolePortSpeedChoices,
-        allow_null=True,
-        required=False
-    )
-
-    class Meta:
-        model = ConsolePort
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
-            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
-            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
-            'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(
-        choices=PowerPortTypeChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = PowerPort
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
-            'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
-            'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
-            'created', 'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(
-        choices=PowerOutletTypeChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-    power_port = PowerPortSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-    feed_leg = ChoiceField(
-        choices=PowerOutletFeedLegChoices,
-        allow_blank=True,
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = PowerOutlet
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
-            'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
-            'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
-            'created', 'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
-    device = DeviceSerializer(nested=True)
-    vdcs = SerializedPKRelatedField(
-        queryset=VirtualDeviceContext.objects.all(),
-        serializer=NestedVirtualDeviceContextSerializer,
-        required=False,
-        many=True
-    )
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(choices=InterfaceTypeChoices)
-    parent = NestedInterfaceSerializer(required=False, allow_null=True)
-    bridge = NestedInterfaceSerializer(required=False, allow_null=True)
-    lag = NestedInterfaceSerializer(required=False, allow_null=True)
-    mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
-    duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
-    rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
-    rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
-    poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
-    poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
-    untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
-    tagged_vlans = SerializedPKRelatedField(
-        queryset=VLAN.objects.all(),
-        serializer=NestedVLANSerializer,
-        required=False,
-        many=True
-    )
-    vrf = NestedVRFSerializer(required=False, allow_null=True)
-    l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
-    wireless_link = NestedWirelessLinkSerializer(read_only=True, allow_null=True)
-    wireless_lans = SerializedPKRelatedField(
-        queryset=WirelessLAN.objects.all(),
-        serializer=NestedWirelessLANSerializer,
-        required=False,
-        many=True
-    )
-    count_ipaddresses = serializers.IntegerField(read_only=True)
-    count_fhrp_groups = serializers.IntegerField(read_only=True)
-    mac_address = serializers.CharField(
-        required=False,
-        default=None,
-        allow_blank=True,
-        allow_null=True
-    )
-    wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
-
-    class Meta:
-        model = Interface
-        fields = [
-            'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
-            'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
-            'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
-            'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
-            'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
-            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
-            'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-    def validate(self, data):
-
-        # Validate many-to-many VLAN assignments
-        if not self.nested:
-            device = self.instance.device if self.instance else data.get('device')
-            for vlan in data.get('tagged_vlans', []):
-                if vlan.site not in [device.site, None]:
-                    raise serializers.ValidationError({
-                        'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
-                                        f"or it must be global."
-                    })
-
-        return super().validate(data)
-
-
-class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(choices=PortTypeChoices)
-
-    class Meta:
-        model = RearPort
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
-            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
-            'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class FrontPortRearPortSerializer(WritableNestedSerializer):
-    """
-    NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
-    """
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
-
-    class Meta:
-        model = RearPort
-        fields = ['id', 'url', 'display', 'name', 'label', 'description']
-
-
-class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
-    device = DeviceSerializer(nested=True)
-    module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
-        required=False,
-        allow_null=True
-    )
-    type = ChoiceField(choices=PortTypeChoices)
-    rear_port = FrontPortRearPortSerializer()
-
-    class Meta:
-        model = FrontPort
-        fields = [
-            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
-            'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
-            'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
-
-
-class ModuleBaySerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
-    device = DeviceSerializer(nested=True)
-    installed_module = ModuleSerializer(
-        nested=True,
-        requested_fields=('id', 'url', 'display', 'serial', 'description'),
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = ModuleBay
-        fields = [
-            'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
-            'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
-
-
-class DeviceBaySerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
-    device = DeviceSerializer(nested=True)
-    installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
-
-    class Meta:
-        model = DeviceBay
-        fields = [
-            'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
-            'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
-
-
-class InventoryItemSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
-    device = DeviceSerializer(nested=True)
-    parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
-    role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
-    manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
-    component_type = ContentTypeField(
-        queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
-        required=False,
-        allow_null=True
-    )
-    component = serializers.SerializerMethodField(read_only=True)
-    _depth = serializers.IntegerField(source='level', read_only=True)
-
-    class Meta:
-        model = InventoryItem
-        fields = [
-            'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
-            'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
-            'custom_fields', 'created', 'last_updated', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_component(self, obj):
-        if obj.component is None:
-            return None
-        serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(obj.component, context=context).data
-
-
-#
-# Power panels
-#
-
-class PowerPanelSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
-    site = 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')
-
-
-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']

+ 0 - 0
netbox/dcim/api/serializers_/__init__.py


+ 38 - 0
netbox/dcim/api/serializers_/base.py

@@ -0,0 +1,38 @@
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+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], 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

+ 127 - 0
netbox/dcim/api/serializers_/cables.py

@@ -0,0 +1,127 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+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, 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
+
+
+class CabledObjectSerializer(serializers.ModelSerializer):
+    cable = CableSerializer(nested=True, read_only=True, allow_null=True)
+    cable_end = serializers.CharField(read_only=True)
+    link_peers_type = serializers.SerializerMethodField(read_only=True)
+    link_peers = serializers.SerializerMethodField(read_only=True)
+    _occupied = serializers.SerializerMethodField(read_only=True)
+
+    @extend_schema_field(OpenApiTypes.STR)
+    def get_link_peers_type(self, obj):
+        """
+        Return the type of the peer link terminations, or None.
+        """
+        if not obj.cable:
+            return None
+
+        if obj.link_peers:
+            return f'{obj.link_peers[0]._meta.app_label}.{obj.link_peers[0]._meta.model_name}'
+
+        return None
+
+    @extend_schema_field(serializers.ListField)
+    def get_link_peers(self, obj):
+        """
+        Return the appropriate serializer for the link termination model.
+        """
+        if not obj.link_peers:
+            return []
+
+        # Return serialized peer termination objects
+        serializer = get_serializer_for_model(obj.link_peers[0], prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.link_peers, context=context, many=True).data
+
+    @extend_schema_field(serializers.BooleanField)
+    def get__occupied(self, obj):
+        return obj._occupied

+ 366 - 0
netbox/dcim/api/serializers_/device_components.py

@@ -0,0 +1,366 @@
+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.api.nested_serializers import NestedVLANSerializer
+from ipam.models import VLAN
+from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.serializers import NetBoxModelSerializer, WritableNestedSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from utilities.api import get_serializer_for_model
+from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
+from wireless.api.nested_serializers import NestedWirelessLANSerializer, NestedWirelessLinkSerializer
+from wireless.choices import *
+from wireless.models import WirelessLAN
+from .base import ConnectedEndpointsSerializer
+from .cables import CabledObjectSerializer
+from .devices import DeviceSerializer, ModuleSerializer
+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,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        allow_blank=True,
+        required=False
+    )
+    speed = ChoiceField(
+        choices=ConsolePortSpeedChoices,
+        allow_null=True,
+        required=False
+    )
+
+    class Meta:
+        model = ConsoleServerPort
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
+            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
+            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
+            'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class ConsolePortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:consoleport-detail')
+    device = DeviceSerializer(nested=True)
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(
+        choices=ConsolePortTypeChoices,
+        allow_blank=True,
+        required=False
+    )
+    speed = ChoiceField(
+        choices=ConsolePortSpeedChoices,
+        allow_null=True,
+        required=False
+    )
+
+    class Meta:
+        model = ConsolePort
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'speed', 'description',
+            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'connected_endpoints',
+            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
+            'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class PowerPortSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerport-detail')
+    device = DeviceSerializer(nested=True)
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(
+        choices=PowerPortTypeChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
+
+    class Meta:
+        model = PowerPort
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw',
+            'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
+            'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
+            'created', 'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class PowerOutletSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:poweroutlet-detail')
+    device = DeviceSerializer(nested=True)
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(
+        choices=PowerOutletTypeChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
+    power_port = PowerPortSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    feed_leg = ChoiceField(
+        choices=PowerOutletFeedLegChoices,
+        allow_blank=True,
+        required=False,
+        allow_null=True
+    )
+
+    class Meta:
+        model = PowerOutlet
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'power_port', 'feed_leg',
+            'description', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
+            'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields',
+            'created', 'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class InterfaceSerializer(NetBoxModelSerializer, CabledObjectSerializer, ConnectedEndpointsSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:interface-detail')
+    device = DeviceSerializer(nested=True)
+    vdcs = SerializedPKRelatedField(
+        queryset=VirtualDeviceContext.objects.all(),
+        serializer=NestedVirtualDeviceContextSerializer,
+        required=False,
+        many=True
+    )
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(choices=InterfaceTypeChoices)
+    parent = NestedInterfaceSerializer(required=False, allow_null=True)
+    bridge = NestedInterfaceSerializer(required=False, allow_null=True)
+    lag = NestedInterfaceSerializer(required=False, allow_null=True)
+    mode = ChoiceField(choices=InterfaceModeChoices, required=False, allow_blank=True)
+    duplex = ChoiceField(choices=InterfaceDuplexChoices, required=False, allow_blank=True, allow_null=True)
+    rf_role = ChoiceField(choices=WirelessRoleChoices, required=False, allow_blank=True)
+    rf_channel = ChoiceField(choices=WirelessChannelChoices, required=False, allow_blank=True)
+    poe_mode = ChoiceField(choices=InterfacePoEModeChoices, required=False, allow_blank=True)
+    poe_type = ChoiceField(choices=InterfacePoETypeChoices, required=False, allow_blank=True)
+    untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
+    tagged_vlans = SerializedPKRelatedField(
+        queryset=VLAN.objects.all(),
+        serializer=NestedVLANSerializer,
+        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=NestedWirelessLANSerializer,
+        required=False,
+        many=True
+    )
+    count_ipaddresses = serializers.IntegerField(read_only=True)
+    count_fhrp_groups = serializers.IntegerField(read_only=True)
+    mac_address = serializers.CharField(
+        required=False,
+        default=None,
+        allow_blank=True,
+        allow_null=True
+    )
+    wwn = serializers.CharField(required=False, default=None, allow_blank=True, allow_null=True)
+
+    class Meta:
+        model = Interface
+        fields = [
+            'id', 'url', 'display', 'device', 'vdcs', 'module', 'name', 'label', 'type', 'enabled', 'parent', 'bridge',
+            'lag', 'mtu', 'mac_address', 'speed', 'duplex', 'wwn', 'mgmt_only', 'description', 'mode', 'rf_role',
+            'rf_channel', 'poe_mode', 'poe_type', 'rf_channel_frequency', 'rf_channel_width', 'tx_power',
+            'untagged_vlan', 'tagged_vlans', 'mark_connected', 'cable', 'cable_end', 'wireless_link', 'link_peers',
+            'link_peers_type', 'wireless_lans', 'vrf', 'l2vpn_termination', 'connected_endpoints',
+            'connected_endpoints_type', 'connected_endpoints_reachable', 'tags', 'custom_fields', 'created',
+            'last_updated', 'count_ipaddresses', 'count_fhrp_groups', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+    def validate(self, data):
+
+        # Validate many-to-many VLAN assignments
+        if not self.nested:
+            device = self.instance.device if self.instance else data.get('device')
+            for vlan in data.get('tagged_vlans', []):
+                if vlan.site not in [device.site, None]:
+                    raise serializers.ValidationError({
+                        'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent device, "
+                                        f"or it must be global."
+                    })
+
+        return super().validate(data)
+
+
+class RearPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
+    device = DeviceSerializer(nested=True)
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(choices=PortTypeChoices)
+
+    class Meta:
+        model = RearPort
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'positions', 'description',
+            'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type', 'tags', 'custom_fields', 'created',
+            'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class FrontPortRearPortSerializer(WritableNestedSerializer):
+    """
+    NestedRearPortSerializer but with parent device omitted (since front and rear ports must belong to same device)
+    """
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail')
+
+    class Meta:
+        model = RearPort
+        fields = ['id', 'url', 'display', 'name', 'label', 'description']
+
+
+class FrontPortSerializer(NetBoxModelSerializer, CabledObjectSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail')
+    device = DeviceSerializer(nested=True)
+    module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'device', 'module_bay'),
+        required=False,
+        allow_null=True
+    )
+    type = ChoiceField(choices=PortTypeChoices)
+    rear_port = FrontPortRearPortSerializer()
+
+    class Meta:
+        model = FrontPort
+        fields = [
+            'id', 'url', 'display', 'device', 'module', 'name', 'label', 'type', 'color', 'rear_port',
+            'rear_port_position', 'description', 'mark_connected', 'cable', 'cable_end', 'link_peers',
+            'link_peers_type', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', 'cable', '_occupied')
+
+
+class ModuleBaySerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:modulebay-detail')
+    device = DeviceSerializer(nested=True)
+    installed_module = ModuleSerializer(
+        nested=True,
+        requested_fields=('id', 'url', 'display', 'serial', 'description'),
+        required=False,
+        allow_null=True
+    )
+
+    class Meta:
+        model = ModuleBay
+        fields = [
+            'id', 'url', 'display', 'device', 'name', 'installed_module', 'label', 'position', 'description', 'tags',
+            'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'installed_module', 'name', 'description')
+
+
+class DeviceBaySerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail')
+    device = DeviceSerializer(nested=True)
+    installed_device = DeviceSerializer(nested=True, required=False, allow_null=True)
+
+    class Meta:
+        model = DeviceBay
+        fields = [
+            'id', 'url', 'display', 'device', 'name', 'label', 'description', 'installed_device', 'tags',
+            'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description')
+
+
+class InventoryItemSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail')
+    device = DeviceSerializer(nested=True)
+    parent = serializers.PrimaryKeyRelatedField(queryset=InventoryItem.objects.all(), allow_null=True, default=None)
+    role = InventoryItemRoleSerializer(nested=True, required=False, allow_null=True)
+    manufacturer = ManufacturerSerializer(nested=True, required=False, allow_null=True, default=None)
+    component_type = ContentTypeField(
+        queryset=ContentType.objects.filter(MODULAR_COMPONENT_MODELS),
+        required=False,
+        allow_null=True
+    )
+    component = serializers.SerializerMethodField(read_only=True)
+    _depth = serializers.IntegerField(source='level', read_only=True)
+
+    class Meta:
+        model = InventoryItem
+        fields = [
+            'id', 'url', 'display', 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial',
+            'asset_tag', 'discovered', 'description', 'component_type', 'component_id', 'component', 'tags',
+            'custom_fields', 'created', 'last_updated', '_depth',
+        ]
+        brief_fields = ('id', 'url', 'display', 'device', 'name', 'description', '_depth')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_component(self, obj):
+        if obj.component is None:
+            return None
+        serializer = get_serializer_for_model(obj.component, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.component, context=context).data

+ 165 - 0
netbox/dcim/api/serializers_/devices.py

@@ -0,0 +1,165 @@
+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_.provisioning 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)
+    device_role = DeviceRoleSerializer(
+        nested=True,
+        read_only=True,
+        help_text='Deprecated in v3.6 in favor of `role`.'
+    )
+    tenant = TenantSerializer(
+        nested=True,
+        required=False,
+        allow_null=True,
+        default=None
+    )
+    platform = PlatformSerializer(nested=True, required=False, allow_null=True)
+    site = SiteSerializer(nested=True)
+    location = LocationSerializer(nested=True, required=False, allow_null=True, default=None)
+    rack = RackSerializer(nested=True, required=False, allow_null=True, default=None)
+    face = ChoiceField(choices=DeviceFaceChoices, allow_blank=True, default=lambda: '')
+    position = serializers.DecimalField(
+        max_digits=4,
+        decimal_places=1,
+        allow_null=True,
+        label=_('Position (U)'),
+        min_value=decimal.Decimal(0.5),
+        default=None
+    )
+    status = ChoiceField(choices=DeviceStatusChoices, required=False)
+    airflow = ChoiceField(choices=DeviceAirflowChoices, allow_blank=True, required=False)
+    primary_ip = 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', 'device_role', 'tenant', 'platform', 'serial',
+            'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
+            'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
+            'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
+            'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
+            'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
+            'device_bay_count', 'module_bay_count', 'inventory_item_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+    @extend_schema_field(NestedDeviceSerializer)
+    def get_parent_device(self, obj):
+        try:
+            device_bay = obj.parent_bay
+        except DeviceBay.DoesNotExist:
+            return None
+        context = {'request': self.context['request']}
+        data = NestedDeviceSerializer(instance=device_bay.device, context=context).data
+        data['device_bay'] = NestedDeviceBaySerializer(instance=device_bay, context=context).data
+        return data
+
+    def get_device_role(self, obj):
+        return obj.role
+
+
+class DeviceWithConfigContextSerializer(DeviceSerializer):
+    config_context = serializers.SerializerMethodField(read_only=True)
+
+    class Meta(DeviceSerializer.Meta):
+        fields = [
+            'id', 'url', 'display', 'name', 'device_type', 'role', 'device_role', 'tenant', 'platform', 'serial',
+            'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device',
+            'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
+            'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'config_context',
+            'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'console_port_count',
+            'console_server_port_count', 'power_port_count', 'power_outlet_count', 'interface_count',
+            'front_port_count', 'rear_port_count', 'device_bay_count', 'module_bay_count', 'inventory_item_count',
+        ]
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_config_context(self, obj):
+        return obj.get_config_context()
+
+
+class VirtualDeviceContextSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualdevicecontext-detail')
+    device = DeviceSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True, default=None)
+    primary_ip = 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')

+ 328 - 0
netbox/dcim/api/serializers_/devicetype_components.py

@@ -0,0 +1,328 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+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, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.component, context=context).data

+ 74 - 0
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')

+ 26 - 0
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')

+ 29 - 0
netbox/dcim/api/serializers_/platforms.py

@@ -0,0 +1,29 @@
+from rest_framework import serializers
+
+from dcim.models import Platform
+from extras.api.serializers_.provisioning 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')

+ 80 - 0
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')

+ 117 - 0
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
+    )

+ 31 - 0
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']

+ 43 - 0
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_.provisioning 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')

+ 97 - 0
netbox/dcim/api/serializers_/sites.py

@@ -0,0 +1,97 @@
+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.nested_serializers import NestedASNSerializer
+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=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')
+
+
+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')

+ 25 - 0
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')

+ 14 - 665
netbox/extras/api/serializers.py

@@ -1,666 +1,15 @@
-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.serializers import DataFileSerializer, DataSourceSerializer, JobSerializer
-from core.models import ContentType
-from dcim.api.nested_serializers import (
-    NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
-    NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
-)
-from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
-from extras.choices import *
-from extras.models import *
-from netbox.api.exceptions import SerializerNotFound
-from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import BaseModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer
-from netbox.api.serializers.features import TaggableModelSerializer
-from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
-from tenancy.models import Tenant, TenantGroup
-from users.api.serializers import UserSerializer
-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_.provisioning import *
+from .serializers_.savedfilters import *
+from .serializers_.scripts import *
+from .serializers_.tags import *
 from .nested_serializers import *
-
-__all__ = (
-    'BookmarkSerializer',
-    'ConfigContextSerializer',
-    'ConfigTemplateSerializer',
-    'ContentTypeSerializer',
-    'CustomFieldChoiceSetSerializer',
-    'CustomFieldSerializer',
-    'CustomLinkSerializer',
-    'DashboardSerializer',
-    'EventRuleSerializer',
-    'ExportTemplateSerializer',
-    'ImageAttachmentSerializer',
-    'JournalEntrySerializer',
-    'ObjectChangeSerializer',
-    'SavedFilterSerializer',
-    'ScriptDetailSerializer',
-    'ScriptInputSerializer',
-    'ScriptSerializer',
-    'TagSerializer',
-    'WebhookSerializer',
-)
-
-
-#
-# Event Rules
-#
-
-class EventRuleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
-    content_types = ContentTypeField(
-        queryset=ContentType.objects.with_feature('event_rules'),
-        many=True
-    )
-    action_type = ChoiceField(choices=EventRuleActionChoices)
-    action_object_type = ContentTypeField(
-        queryset=ContentType.objects.with_feature('event_rules'),
-    )
-    action_object = serializers.SerializerMethodField(read_only=True)
-
-    class Meta:
-        model = EventRule
-        fields = [
-            'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
-            'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
-            'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-    @extend_schema_field(OpenApiTypes.OBJECT)
-    def get_action_object(self, instance):
-        context = {'request': self.context['request']}
-        # We need to manually instantiate the serializer for scripts
-        if instance.action_type == EventRuleActionChoices.SCRIPT:
-            script = instance.action_object
-            instance = script.python_class() if script.python_class else None
-            return NestedScriptSerializer(instance, context=context).data
-        else:
-            serializer = get_serializer_for_model(
-                model=instance.action_object_type.model_class(),
-                prefix=NESTED_SERIALIZER_PREFIX
-            )
-            return serializer(instance.action_object, context=context).data
-
-
-#
-# Webhooks
-#
-
-class WebhookSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:webhook-detail')
-
-    class Meta:
-        model = Webhook
-        fields = [
-            'id', 'url', 'display', 'name', 'description', 'payload_url', 'http_method', 'http_content_type',
-            'additional_headers', 'body_template', 'secret', 'ssl_verification', 'ca_file_path', 'custom_fields',
-            'tags', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-#
-# Custom fields
-#
-
-class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
-    base_choices = ChoiceField(
-        choices=CustomFieldChoiceSetBaseChoices,
-        required=False
-    )
-    extra_choices = serializers.ListField(
-        child=serializers.ListField(
-            min_length=2,
-            max_length=2
-        )
-    )
-
-    class Meta:
-        model = CustomFieldChoiceSet
-        fields = [
-            'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
-            'choices_count', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
-
-
-class CustomFieldSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
-    content_types = ContentTypeField(
-        queryset=ContentType.objects.with_feature('custom_fields'),
-        many=True
-    )
-    type = ChoiceField(choices=CustomFieldTypeChoices)
-    object_type = ContentTypeField(
-        queryset=ContentType.objects.all(),
-        required=False,
-        allow_null=True
-    )
-    filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
-    data_type = serializers.SerializerMethodField()
-    choice_set = CustomFieldChoiceSetSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-    ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
-    ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
-
-    class Meta:
-        model = CustomField
-        fields = [
-            'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
-            'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
-            'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
-            'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-    def validate_type(self, value):
-        if self.instance and self.instance.type != value:
-            raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
-
-        return value
-
-    @extend_schema_field(OpenApiTypes.STR)
-    def get_data_type(self, obj):
-        types = CustomFieldTypeChoices
-        if obj.type == types.TYPE_INTEGER:
-            return 'integer'
-        if obj.type == types.TYPE_DECIMAL:
-            return 'decimal'
-        if obj.type == types.TYPE_BOOLEAN:
-            return 'boolean'
-        if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
-            return 'object'
-        if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
-            return 'array'
-        return 'string'
-
-
-#
-# Custom links
-#
-
-class CustomLinkSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
-    content_types = ContentTypeField(
-        queryset=ContentType.objects.with_feature('custom_links'),
-        many=True
-    )
-
-    class Meta:
-        model = CustomLink
-        fields = [
-            'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
-            'button_class', 'new_window', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name')
-
-
-#
-# Export templates
-#
-
-class ExportTemplateSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
-    content_types = ContentTypeField(
-        queryset=ContentType.objects.with_feature('export_templates'),
-        many=True
-    )
-    data_source = DataSourceSerializer(
-        nested=True,
-        required=False
-    )
-    data_file = DataFileSerializer(
-        nested=True,
-        read_only=True
-    )
-
-    class Meta:
-        model = ExportTemplate
-        fields = [
-            'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
-            'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
-            'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-#
-# Saved filters
-#
-
-class SavedFilterSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
-    content_types = ContentTypeField(
-        queryset=ContentType.objects.all(),
-        many=True
-    )
-
-    class Meta:
-        model = SavedFilter
-        fields = [
-            'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
-            'shared', 'parameters', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
-
-
-#
-# Bookmarks
-#
-
-class BookmarkSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
-    object_type = ContentTypeField(
-        queryset=ContentType.objects.with_feature('bookmarks'),
-    )
-    object = serializers.SerializerMethodField(read_only=True)
-    user = 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, prefix=NESTED_SERIALIZER_PREFIX)
-        return serializer(instance.object, context={'request': self.context['request']}).data
-
-
-#
-# Tags
-#
-
-class TagSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
-    object_types = ContentTypeField(
-        queryset=ContentType.objects.with_feature('tags'),
-        many=True,
-        required=False
-    )
-
-    # Related object counts
-    tagged_items = RelatedObjectCountField('extras_taggeditem_items')
-
-    class Meta:
-        model = Tag
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
-            'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')
-
-
-#
-# Image attachments
-#
-
-class ImageAttachmentSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
-    content_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    parent = serializers.SerializerMethodField(read_only=True)
-
-    class Meta:
-        model = ImageAttachment
-        fields = [
-            'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
-            'image_width', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'image')
-
-    def validate(self, data):
-
-        # Validate that the parent object exists
-        try:
-            data['content_type'].get_object_for_this_type(id=data['object_id'])
-        except ObjectDoesNotExist:
-            raise serializers.ValidationError(
-                "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
-            )
-
-        # Enforce model validation
-        super().validate(data)
-
-        return data
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_parent(self, obj):
-        serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
-        return serializer(obj.parent, context={'request': self.context['request']}).data
-
-
-#
-# Journal entries
-#
-
-class JournalEntrySerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
-    assigned_object_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    assigned_object = serializers.SerializerMethodField(read_only=True)
-    created_by = serializers.PrimaryKeyRelatedField(
-        allow_null=True,
-        queryset=get_user_model().objects.all(),
-        required=False,
-        default=serializers.CurrentUserDefault()
-    )
-    kind = ChoiceField(
-        choices=JournalEntryKindChoices,
-        required=False
-    )
-
-    class Meta:
-        model = JournalEntry
-        fields = [
-            'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
-            'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'created')
-
-    def validate(self, data):
-
-        # Validate that the parent object exists
-        if 'assigned_object_type' in data and 'assigned_object_id' in data:
-            try:
-                data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
-            except ObjectDoesNotExist:
-                raise serializers.ValidationError(
-                    f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
-                )
-
-        # Enforce model validation
-        super().validate(data)
-
-        return data
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_assigned_object(self, instance):
-        serializer = get_serializer_for_model(instance.assigned_object_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(instance.assigned_object, context=context).data
-
-
-#
-# Config contexts
-#
-
-class ConfigContextSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:configcontext-detail')
-    regions = SerializedPKRelatedField(
-        queryset=Region.objects.all(),
-        serializer=NestedRegionSerializer,
-        required=False,
-        many=True
-    )
-    site_groups = SerializedPKRelatedField(
-        queryset=SiteGroup.objects.all(),
-        serializer=NestedSiteGroupSerializer,
-        required=False,
-        many=True
-    )
-    sites = SerializedPKRelatedField(
-        queryset=Site.objects.all(),
-        serializer=NestedSiteSerializer,
-        required=False,
-        many=True
-    )
-    locations = SerializedPKRelatedField(
-        queryset=Location.objects.all(),
-        serializer=NestedLocationSerializer,
-        required=False,
-        many=True
-    )
-    device_types = SerializedPKRelatedField(
-        queryset=DeviceType.objects.all(),
-        serializer=NestedDeviceTypeSerializer,
-        required=False,
-        many=True
-    )
-    roles = SerializedPKRelatedField(
-        queryset=DeviceRole.objects.all(),
-        serializer=NestedDeviceRoleSerializer,
-        required=False,
-        many=True
-    )
-    platforms = SerializedPKRelatedField(
-        queryset=Platform.objects.all(),
-        serializer=NestedPlatformSerializer,
-        required=False,
-        many=True
-    )
-    cluster_types = SerializedPKRelatedField(
-        queryset=ClusterType.objects.all(),
-        serializer=NestedClusterTypeSerializer,
-        required=False,
-        many=True
-    )
-    cluster_groups = SerializedPKRelatedField(
-        queryset=ClusterGroup.objects.all(),
-        serializer=NestedClusterGroupSerializer,
-        required=False,
-        many=True
-    )
-    clusters = SerializedPKRelatedField(
-        queryset=Cluster.objects.all(),
-        serializer=NestedClusterSerializer,
-        required=False,
-        many=True
-    )
-    tenant_groups = SerializedPKRelatedField(
-        queryset=TenantGroup.objects.all(),
-        serializer=NestedTenantGroupSerializer,
-        required=False,
-        many=True
-    )
-    tenants = SerializedPKRelatedField(
-        queryset=Tenant.objects.all(),
-        serializer=NestedTenantSerializer,
-        required=False,
-        many=True
-    )
-    tags = serializers.SlugRelatedField(
-        queryset=Tag.objects.all(),
-        slug_field='slug',
-        required=False,
-        many=True
-    )
-    data_source = 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')
-
-
-#
-# Config templates
-#
-
-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')
-
-
-#
-# 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 = 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
-
-
-#
-# Change logging
-#
-
-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, prefix=NESTED_SERIALIZER_PREFIX)
-        except SerializerNotFound:
-            return obj.object_repr
-        context = {
-            'request': self.context['request']
-        }
-        data = serializer(obj.changed_object, context=context).data
-
-        return data
-
-
-#
-# ContentTypes
-#
-
-class ContentTypeSerializer(BaseModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
-
-    class Meta:
-        model = ContentType
-        fields = ['id', 'url', 'display', 'app_label', 'model']
-
-
-#
-# User dashboard
-#
-
-class DashboardSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = Dashboard
-        fields = ('layout', 'config')

+ 0 - 0
netbox/extras/api/serializers_/__init__.py


+ 50 - 0
netbox/extras/api/serializers_/attachments.py

@@ -0,0 +1,50 @@
+from django.core.exceptions import ObjectDoesNotExist
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.models import ImageAttachment
+from netbox.api.fields import ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from utilities.api import get_serializer_for_model
+
+__all__ = (
+    'ImageAttachmentSerializer',
+)
+
+
+class ImageAttachmentSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:imageattachment-detail')
+    content_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    parent = serializers.SerializerMethodField(read_only=True)
+
+    class Meta:
+        model = ImageAttachment
+        fields = [
+            'id', 'url', 'display', 'content_type', 'object_id', 'parent', 'name', 'image', 'image_height',
+            'image_width', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'image')
+
+    def validate(self, data):
+
+        # Validate that the parent object exists
+        try:
+            data['content_type'].get_object_for_this_type(id=data['object_id'])
+        except ObjectDoesNotExist:
+            raise serializers.ValidationError(
+                "Invalid parent object: {} ID {}".format(data['content_type'], data['object_id'])
+            )
+
+        # Enforce model validation
+        super().validate(data)
+
+        return data
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_parent(self, obj):
+        serializer = get_serializer_for_model(obj.parent, prefix=NESTED_SERIALIZER_PREFIX)
+        return serializer(obj.parent, context={'request': self.context['request']}).data

+ 35 - 0
netbox/extras/api/serializers_/bookmarks.py

@@ -0,0 +1,35 @@
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.models import Bookmark
+from netbox.api.fields import ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from users.api.serializers_.users import UserSerializer
+from utilities.api import get_serializer_for_model
+
+__all__ = (
+    'BookmarkSerializer',
+)
+
+
+class BookmarkSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:bookmark-detail')
+    object_type = ContentTypeField(
+        queryset=ContentType.objects.with_feature('bookmarks'),
+    )
+    object = serializers.SerializerMethodField(read_only=True)
+    user = UserSerializer(nested=True)
+
+    class Meta:
+        model = Bookmark
+        fields = [
+            'id', 'url', 'display', 'object_type', 'object_id', 'object', 'user', 'created',
+        ]
+        brief_fields = ('id', 'url', 'display', 'object_id', 'object_type')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_object(self, instance):
+        serializer = get_serializer_for_model(instance.object, prefix=NESTED_SERIALIZER_PREFIX)
+        return serializer(instance.object, context={'request': self.context['request']}).data

+ 59 - 0
netbox/extras/api/serializers_/change_logging.py

@@ -0,0 +1,59 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+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, 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

+ 16 - 0
netbox/extras/api/serializers_/contenttypes.py

@@ -0,0 +1,16 @@
+from rest_framework import serializers
+
+from core.models import ContentType
+from netbox.api.serializers import BaseModelSerializer
+
+__all__ = (
+    'ContentTypeSerializer',
+)
+
+
+class ContentTypeSerializer(BaseModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:contenttype-detail')
+
+    class Meta:
+        model = ContentType
+        fields = ['id', 'url', 'display', 'app_label', 'model']

+ 91 - 0
netbox/extras/api/serializers_/customfields.py

@@ -0,0 +1,91 @@
+from django.utils.translation import gettext as _
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.choices import *
+from extras.models import CustomField, CustomFieldChoiceSet
+from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+
+__all__ = (
+    'CustomFieldChoiceSetSerializer',
+    'CustomFieldSerializer',
+)
+
+
+class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
+    base_choices = ChoiceField(
+        choices=CustomFieldChoiceSetBaseChoices,
+        required=False
+    )
+    extra_choices = serializers.ListField(
+        child=serializers.ListField(
+            min_length=2,
+            max_length=2
+        )
+    )
+
+    class Meta:
+        model = CustomFieldChoiceSet
+        fields = [
+            'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
+            'choices_count', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description', 'choices_count')
+
+
+class CustomFieldSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfield-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.with_feature('custom_fields'),
+        many=True
+    )
+    type = ChoiceField(choices=CustomFieldTypeChoices)
+    object_type = ContentTypeField(
+        queryset=ContentType.objects.all(),
+        required=False,
+        allow_null=True
+    )
+    filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
+    data_type = serializers.SerializerMethodField()
+    choice_set = CustomFieldChoiceSetSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    ui_visible = ChoiceField(choices=CustomFieldUIVisibleChoices, required=False)
+    ui_editable = ChoiceField(choices=CustomFieldUIEditableChoices, required=False)
+
+    class Meta:
+        model = CustomField
+        fields = [
+            'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
+            'description', 'required', 'search_weight', 'filter_logic', 'ui_visible', 'ui_editable', 'is_cloneable',
+            'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set',
+            'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+    def validate_type(self, value):
+        if self.instance and self.instance.type != value:
+            raise serializers.ValidationError(_('Changing the type of custom fields is not supported.'))
+
+        return value
+
+    @extend_schema_field(OpenApiTypes.STR)
+    def get_data_type(self, obj):
+        types = CustomFieldTypeChoices
+        if obj.type == types.TYPE_INTEGER:
+            return 'integer'
+        if obj.type == types.TYPE_DECIMAL:
+            return 'decimal'
+        if obj.type == types.TYPE_BOOLEAN:
+            return 'boolean'
+        if obj.type in (types.TYPE_JSON, types.TYPE_OBJECT):
+            return 'object'
+        if obj.type in (types.TYPE_MULTISELECT, types.TYPE_MULTIOBJECT):
+            return 'array'
+        return 'string'

+ 26 - 0
netbox/extras/api/serializers_/customlinks.py

@@ -0,0 +1,26 @@
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.models import CustomLink
+from netbox.api.fields import ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+
+__all__ = (
+    'CustomLinkSerializer',
+)
+
+
+class CustomLinkSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.with_feature('custom_links'),
+        many=True
+    )
+
+    class Meta:
+        model = CustomLink
+        fields = [
+            'id', 'url', 'display', 'content_types', 'name', 'enabled', 'link_text', 'link_url', 'weight', 'group_name',
+            'button_class', 'new_window', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name')

+ 13 - 0
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')

+ 75 - 0
netbox/extras/api/serializers_/events.py

@@ -0,0 +1,75 @@
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.choices import *
+from extras.models import EventRule, Webhook
+from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from utilities.api import get_serializer_for_model
+from ..nested_serializers import *
+
+__all__ = (
+    'EventRuleSerializer',
+    'WebhookSerializer',
+)
+
+
+#
+# Event Rules
+#
+
+class EventRuleSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:eventrule-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.with_feature('event_rules'),
+        many=True
+    )
+    action_type = ChoiceField(choices=EventRuleActionChoices)
+    action_object_type = ContentTypeField(
+        queryset=ContentType.objects.with_feature('event_rules'),
+    )
+    action_object = serializers.SerializerMethodField(read_only=True)
+
+    class Meta:
+        model = EventRule
+        fields = [
+            'id', 'url', 'display', 'content_types', 'name', 'type_create', 'type_update', 'type_delete',
+            'type_job_start', 'type_job_end', 'enabled', 'conditions', 'action_type', 'action_object_type',
+            'action_object_id', 'action_object', 'description', 'custom_fields', 'tags', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+    @extend_schema_field(OpenApiTypes.OBJECT)
+    def get_action_object(self, instance):
+        context = {'request': self.context['request']}
+        # We need to manually instantiate the serializer for scripts
+        if instance.action_type == EventRuleActionChoices.SCRIPT:
+            script = instance.action_object
+            instance = script.python_class() if script.python_class else None
+            return 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')

+ 36 - 0
netbox/extras/api/serializers_/exporttemplates.py

@@ -0,0 +1,36 @@
+from rest_framework import serializers
+
+from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
+from core.models import ContentType
+from extras.models import ExportTemplate
+from netbox.api.fields import ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+
+__all__ = (
+    'ExportTemplateSerializer',
+)
+
+
+class ExportTemplateSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.with_feature('export_templates'),
+        many=True
+    )
+    data_source = DataSourceSerializer(
+        nested=True,
+        required=False
+    )
+    data_file = DataFileSerializer(
+        nested=True,
+        read_only=True
+    )
+
+    class Meta:
+        model = ExportTemplate
+        fields = [
+            'id', 'url', 'display', 'content_types', 'name', 'description', 'template_code', 'mime_type',
+            'file_extension', 'as_attachment', 'data_source', 'data_path', 'data_file', 'data_synced', 'created',
+            'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 67 - 0
netbox/extras/api/serializers_/journaling.py

@@ -0,0 +1,67 @@
+from django.contrib.auth import get_user_model
+from django.core.exceptions import ObjectDoesNotExist
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.choices import *
+from extras.models import JournalEntry
+from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from utilities.api import get_serializer_for_model
+
+__all__ = (
+    'JournalEntrySerializer',
+)
+
+
+class JournalEntrySerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:journalentry-detail')
+    assigned_object_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    assigned_object = serializers.SerializerMethodField(read_only=True)
+    created_by = serializers.PrimaryKeyRelatedField(
+        allow_null=True,
+        queryset=get_user_model().objects.all(),
+        required=False,
+        default=serializers.CurrentUserDefault()
+    )
+    kind = ChoiceField(
+        choices=JournalEntryKindChoices,
+        required=False
+    )
+
+    class Meta:
+        model = JournalEntry
+        fields = [
+            'id', 'url', 'display', 'assigned_object_type', 'assigned_object_id', 'assigned_object', 'created',
+            'created_by', 'kind', 'comments', 'tags', 'custom_fields', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'created')
+
+    def validate(self, data):
+
+        # Validate that the parent object exists
+        if 'assigned_object_type' in data and 'assigned_object_id' in data:
+            try:
+                data['assigned_object_type'].get_object_for_this_type(id=data['assigned_object_id'])
+            except ObjectDoesNotExist:
+                raise serializers.ValidationError(
+                    f"Invalid assigned_object: {data['assigned_object_type']} ID {data['assigned_object_id']}"
+                )
+
+        # Enforce model validation
+        super().validate(data)
+
+        return data
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_assigned_object(self, instance):
+        serializer = get_serializer_for_model(
+            instance.assigned_object_type.model_class(),
+            prefix=NESTED_SERIALIZER_PREFIX
+        )
+        context = {'request': self.context['request']}
+        return serializer(instance.assigned_object, context=context).data

+ 147 - 0
netbox/extras/api/serializers_/provisioning.py

@@ -0,0 +1,147 @@
+from rest_framework import serializers
+
+from core.api.serializers_.data import DataFileSerializer, DataSourceSerializer
+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.models import ConfigContext, ConfigTemplate, Tag
+from netbox.api.fields import SerializedPKRelatedField
+from netbox.api.serializers import ValidatedModelSerializer
+from netbox.api.serializers.features import TaggableModelSerializer
+from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
+from tenancy.models import Tenant, TenantGroup
+from virtualization.api.nested_serializers import (
+    NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer,
+)
+from virtualization.models import Cluster, ClusterGroup, ClusterType
+
+__all__ = (
+    'ConfigContextSerializer',
+    'ConfigTemplateSerializer',
+)
+
+
+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 = 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')
+
+
+#
+# Config templates
+#
+
+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')

+ 26 - 0
netbox/extras/api/serializers_/savedfilters.py

@@ -0,0 +1,26 @@
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.models import SavedFilter
+from netbox.api.fields import ContentTypeField
+from netbox.api.serializers import ValidatedModelSerializer
+
+__all__ = (
+    'SavedFilterSerializer',
+)
+
+
+class SavedFilterSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:savedfilter-detail')
+    content_types = ContentTypeField(
+        queryset=ContentType.objects.all(),
+        many=True
+    )
+
+    class Meta:
+        model = SavedFilter
+        fields = [
+            'id', 'url', 'display', 'content_types', 'name', 'slug', 'description', 'user', 'weight', 'enabled',
+            'shared', 'parameters', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

+ 77 - 0
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

+ 30 - 0
netbox/extras/api/serializers_/tags.py

@@ -0,0 +1,30 @@
+from rest_framework import serializers
+
+from core.models import ContentType
+from extras.models import Tag
+from netbox.api.fields import ContentTypeField, RelatedObjectCountField
+from netbox.api.serializers import ValidatedModelSerializer
+
+__all__ = (
+    'TagSerializer',
+)
+
+
+class TagSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='extras-api:tag-detail')
+    object_types = ContentTypeField(
+        queryset=ContentType.objects.with_feature('tags'),
+        many=True,
+        required=False
+    )
+
+    # Related object counts
+    tagged_items = RelatedObjectCountField('extras_taggeditem_items')
+
+    class Meta:
+        model = Tag
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'color', 'description', 'object_types', 'tagged_items', 'created',
+            'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'color', 'description')

+ 7 - 510
netbox/ipam/api/serializers.py

@@ -1,511 +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.serializers import DeviceSerializer, SiteSerializer
-from ipam.choices import *
-from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, VLANGROUP_SCOPE_TYPES
-from ipam.models import *
-from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import NetBoxModelSerializer
-from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.serializers import TenantSerializer
-from utilities.api import get_serializer_for_model
-from virtualization.api.serializers import VirtualMachineSerializer
-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 *
-
-
-#
-# RIRs
-#
-
-class RIRSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:rir-detail')
-
-    # Related object counts
-    aggregate_count = RelatedObjectCountField('aggregates')
-
-    class Meta:
-        model = RIR
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'is_private', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'aggregate_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'aggregate_count')
-
-
-#
-# ASN ranges
-#
-
-class ASNRangeSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
-    rir = 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')
-
-
-#
-# ASNs
-#
-
-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,
-        }
-
-
-#
-# VRFs
-#
-
-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=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')
-
-
-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')
-
-
-#
-# Aggregates
-#
-
-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')
-
-
-#
-# 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 = 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, 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 = 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 = 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 = 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
-
-
-#
-# Prefixes
-#
-
-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, 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 = 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,
-        }
-
-
-#
-# 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 = 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=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')

+ 0 - 0
netbox/ipam/api/serializers_/__init__.py


+ 78 - 0
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,
+        }

+ 53 - 0
netbox/ipam/api/serializers_/fhrpgroups.py

@@ -0,0 +1,53 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+from utilities.api import get_serializer_for_model
+from ..nested_serializers import *
+
+__all__ = (
+    'FHRPGroupAssignmentSerializer',
+    'FHRPGroupSerializer',
+)
+
+
+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 = 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, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.interface, context=context).data

+ 200 - 0
netbox/ipam/api/serializers_/ip.py

@@ -0,0 +1,200 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+from tenancy.api.serializers_.tenants import TenantSerializer
+from utilities.api import get_serializer_for_model
+from ..field_serializers import IPAddressField, IPNetworkField
+from ..nested_serializers import *
+
+from .asns import RIRSerializer
+from .vrfs import VRFSerializer
+from .roles import RoleSerializer
+from .vlans import VLANSerializer
+
+__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, 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 = 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,
+        }

+ 25 - 0
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')

+ 48 - 0
netbox/ipam/api/serializers_/services.py

@@ -0,0 +1,48 @@
+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 ..nested_serializers import *
+
+__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=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')

+ 115 - 0
netbox/ipam/api/serializers_/vlans.py

@@ -0,0 +1,115 @@
+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 netbox.constants import NESTED_SERIALIZER_PREFIX
+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, 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 = 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

+ 55 - 0
netbox/ipam/api/serializers_/vrfs.py

@@ -0,0 +1,55 @@
+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
+from ..nested_serializers import *
+
+__all__ = (
+    'RouteTargetSerializer',
+    'VRFSerializer',
+)
+
+
+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=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')
+
+
+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')

+ 2 - 122
netbox/tenancy/api/serializers.py

@@ -1,123 +1,3 @@
-from django.contrib.auth.models import ContentType
-from drf_spectacular.types import OpenApiTypes
-from drf_spectacular.utils import extend_schema_field
-from rest_framework import serializers
-
-from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
-from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
-from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.choices import ContactPriorityChoices
-from tenancy.models import *
-from utilities.api import get_serializer_for_model
+from .serializers_.tenants import *
+from .serializers_.contacts import *
 from .nested_serializers import *
-
-
-#
-# Tenants
-#
-
-class TenantGroupSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
-    parent = NestedTenantGroupSerializer(required=False, allow_null=True)
-    tenant_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = TenantGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'tenant_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth')
-
-
-class TenantSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
-    group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
-
-    # Related object counts
-    circuit_count = RelatedObjectCountField('circuits')
-    device_count = RelatedObjectCountField('devices')
-    rack_count = RelatedObjectCountField('racks')
-    site_count = RelatedObjectCountField('sites')
-    ipaddress_count = RelatedObjectCountField('ip_addresses')
-    prefix_count = RelatedObjectCountField('prefixes')
-    vlan_count = RelatedObjectCountField('vlans')
-    vrf_count = RelatedObjectCountField('vrfs')
-    virtualmachine_count = RelatedObjectCountField('virtual_machines')
-    cluster_count = RelatedObjectCountField('clusters')
-
-    class Meta:
-        model = Tenant
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields',
-            'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count',
-            'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
-
-
-#
-# Contacts
-#
-
-class ContactGroupSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
-    parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
-    contact_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = ContactGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'contact_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth')
-
-
-class ContactRoleSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
-
-    class Meta:
-        model = ContactRole
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
-
-
-class ContactSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
-    group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    class Meta:
-        model = Contact
-        fields = [
-            'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description',
-            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class ContactAssignmentSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
-    content_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    object = serializers.SerializerMethodField(read_only=True)
-    contact = ContactSerializer(nested=True)
-    role = ContactRoleSerializer(nested=True, required=False, allow_null=True)
-    priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
-
-    class Meta:
-        model = ContactAssignment
-        fields = [
-            'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags',
-            'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority')
-
-    @extend_schema_field(OpenApiTypes.OBJECT)
-    def get_object(self, instance):
-        serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(instance.object, context=context).data

+ 0 - 0
netbox/tenancy/api/serializers_/__init__.py


+ 82 - 0
netbox/tenancy/api/serializers_/contacts.py

@@ -0,0 +1,82 @@
+from django.contrib.auth.models import ContentType
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from netbox.api.fields import ChoiceField, ContentTypeField
+from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from tenancy.choices import ContactPriorityChoices
+from tenancy.models import ContactAssignment, Contact, ContactGroup, ContactRole
+from utilities.api import get_serializer_for_model
+from ..nested_serializers import *
+
+__all__ = (
+    'ContactAssignmentSerializer',
+    'ContactGroupSerializer',
+    'ContactRoleSerializer',
+    'ContactSerializer',
+)
+
+
+class ContactGroupSerializer(NestedGroupModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactgroup-detail')
+    parent = NestedContactGroupSerializer(required=False, allow_null=True, default=None)
+    contact_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = ContactGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated', 'contact_count', '_depth',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'contact_count', '_depth')
+
+
+class ContactRoleSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactrole-detail')
+
+    class Meta:
+        model = ContactRole
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')
+
+
+class ContactSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contact-detail')
+    group = ContactGroupSerializer(nested=True, required=False, allow_null=True, default=None)
+
+    class Meta:
+        model = Contact
+        fields = [
+            'id', 'url', 'display', 'group', 'name', 'title', 'phone', 'email', 'address', 'link', 'description',
+            'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class ContactAssignmentSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:contactassignment-detail')
+    content_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    object = serializers.SerializerMethodField(read_only=True)
+    contact = ContactSerializer(nested=True)
+    role = ContactRoleSerializer(nested=True, required=False, allow_null=True)
+    priority = ChoiceField(choices=ContactPriorityChoices, allow_blank=True, required=False, default=lambda: '')
+
+    class Meta:
+        model = ContactAssignment
+        fields = [
+            'id', 'url', 'display', 'content_type', 'object_id', 'object', 'contact', 'role', 'priority', 'tags',
+            'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'contact', 'role', 'priority')
+
+    @extend_schema_field(OpenApiTypes.OBJECT)
+    def get_object(self, instance):
+        serializer = get_serializer_for_model(instance.content_type.model_class(), prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(instance.object, context=context).data

+ 51 - 0
netbox/tenancy/api/serializers_/tenants.py

@@ -0,0 +1,51 @@
+from rest_framework import serializers
+
+from netbox.api.fields import RelatedObjectCountField
+from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
+from tenancy.models import Tenant, TenantGroup
+from ..nested_serializers import *
+
+__all__ = (
+    'TenantGroupSerializer',
+    'TenantSerializer',
+)
+
+
+class TenantGroupSerializer(NestedGroupModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenantgroup-detail')
+    parent = NestedTenantGroupSerializer(required=False, allow_null=True)
+    tenant_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = TenantGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated', 'tenant_count', '_depth',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tenant_count', '_depth')
+
+
+class TenantSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='tenancy-api:tenant-detail')
+    group = TenantGroupSerializer(nested=True, required=False, allow_null=True)
+
+    # Related object counts
+    circuit_count = RelatedObjectCountField('circuits')
+    device_count = RelatedObjectCountField('devices')
+    rack_count = RelatedObjectCountField('racks')
+    site_count = RelatedObjectCountField('sites')
+    ipaddress_count = RelatedObjectCountField('ip_addresses')
+    prefix_count = RelatedObjectCountField('prefixes')
+    vlan_count = RelatedObjectCountField('vlans')
+    vrf_count = RelatedObjectCountField('vrfs')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
+    cluster_count = RelatedObjectCountField('clusters')
+
+    class Meta:
+        model = Tenant
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields',
+            'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count',
+            'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description')

+ 3 - 189
netbox/users/api/serializers.py

@@ -1,190 +1,4 @@
-from django.conf import settings
-from django.contrib.auth import authenticate
-from django.contrib.auth import get_user_model
-from django.contrib.auth.models import Group
-from django.contrib.contenttypes.models import ContentType
-from drf_spectacular.utils import extend_schema_field
-from drf_spectacular.types import OpenApiTypes
-from rest_framework import serializers
-from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
-
-from netbox.api.fields import ContentTypeField, IPNetworkSerializer, SerializedPKRelatedField
-from netbox.api.serializers import ValidatedModelSerializer
-from users.models import ObjectPermission, Token
+from .serializers_.users import *
+from .serializers_.permissions import *
+from .serializers_.tokens import *
 from .nested_serializers import *
-
-
-__all__ = (
-    'GroupSerializer',
-    'ObjectPermissionSerializer',
-    'TokenSerializer',
-    'UserSerializer',
-)
-
-
-class UserSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail')
-    groups = SerializedPKRelatedField(
-        queryset=Group.objects.all(),
-        serializer=NestedGroupSerializer,
-        required=False,
-        many=True
-    )
-
-    class Meta:
-        model = get_user_model()
-        fields = (
-            'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
-            'date_joined', 'last_login', 'groups',
-        )
-        brief_fields = ('id', 'url', 'display', 'username')
-        extra_kwargs = {
-            'password': {'write_only': True}
-        }
-
-    def create(self, validated_data):
-        """
-        Extract the password from validated data and set it separately to ensure proper hash generation.
-        """
-        password = validated_data.pop('password')
-        user = super().create(validated_data)
-        user.set_password(password)
-        user.save()
-
-        return user
-
-    def update(self, instance, validated_data):
-        """
-        Ensure proper updated password hash generation.
-        """
-        password = validated_data.pop('password', None)
-        if password is not None:
-            instance.set_password(password)
-
-        return super().update(instance, validated_data)
-
-    @extend_schema_field(OpenApiTypes.STR)
-    def get_display(self, obj):
-        if full_name := obj.get_full_name():
-            return f"{obj.username} ({full_name})"
-        return obj.username
-
-
-class GroupSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail')
-    user_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = Group
-        fields = ('id', 'url', 'display', 'name', 'user_count')
-        brief_fields = ('id', 'url', 'display', 'name')
-
-
-class TokenSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
-    key = serializers.CharField(
-        min_length=40,
-        max_length=40,
-        allow_blank=True,
-        required=False,
-        write_only=not settings.ALLOW_TOKEN_RETRIEVAL
-    )
-    user = UserSerializer(nested=True)
-    allowed_ips = serializers.ListField(
-        child=IPNetworkSerializer(),
-        required=False,
-        allow_empty=True,
-        default=[]
-    )
-
-    class Meta:
-        model = Token
-        fields = (
-            'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description',
-            'allowed_ips',
-        )
-        brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description')
-
-    def to_internal_value(self, data):
-        if 'key' not in data:
-            data['key'] = Token.generate_key()
-        return super().to_internal_value(data)
-
-    def validate(self, data):
-
-        # If the Token is being created on behalf of another user, enforce the grant_token permission.
-        request = self.context.get('request')
-        token_user = data.get('user')
-        if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'):
-            raise PermissionDenied("This user does not have permission to create tokens for other users.")
-
-        return super().validate(data)
-
-
-class TokenProvisionSerializer(TokenSerializer):
-    user = UserSerializer(
-        nested=True,
-        read_only=True
-    )
-    username = serializers.CharField(
-        write_only=True
-    )
-    password = serializers.CharField(
-        write_only=True
-    )
-    last_used = serializers.DateTimeField(
-        read_only=True
-    )
-    key = serializers.CharField(
-        read_only=True
-    )
-
-    class Meta:
-        model = Token
-        fields = (
-            'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description',
-            'allowed_ips', 'username', 'password',
-        )
-
-    def validate(self, data):
-        # Validate the username and password
-        username = data.pop('username')
-        password = data.pop('password')
-        user = authenticate(request=self.context.get('request'), username=username, password=password)
-        if user is None:
-            raise AuthenticationFailed("Invalid username/password")
-
-        # Inject the user into the validated data
-        data['user'] = user
-
-        return data
-
-
-class ObjectPermissionSerializer(ValidatedModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
-    object_types = ContentTypeField(
-        queryset=ContentType.objects.all(),
-        many=True
-    )
-    groups = SerializedPKRelatedField(
-        queryset=Group.objects.all(),
-        serializer=NestedGroupSerializer,
-        required=False,
-        many=True
-    )
-    users = SerializedPKRelatedField(
-        queryset=get_user_model().objects.all(),
-        serializer=NestedUserSerializer,
-        required=False,
-        many=True
-    )
-
-    class Meta:
-        model = ObjectPermission
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
-            'constraints',
-        )
-        brief_fields = (
-            'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
-        )

+ 0 - 0
netbox/users/api/serializers_/__init__.py


+ 43 - 0
netbox/users/api/serializers_/permissions.py

@@ -0,0 +1,43 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
+from django.contrib.contenttypes.models import ContentType
+from rest_framework import serializers
+
+from netbox.api.fields import ContentTypeField, SerializedPKRelatedField
+from netbox.api.serializers import ValidatedModelSerializer
+from users.models import ObjectPermission
+from ..nested_serializers import *
+
+__all__ = (
+    'ObjectPermissionSerializer',
+)
+
+
+class ObjectPermissionSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:objectpermission-detail')
+    object_types = ContentTypeField(
+        queryset=ContentType.objects.all(),
+        many=True
+    )
+    groups = SerializedPKRelatedField(
+        queryset=Group.objects.all(),
+        serializer=NestedGroupSerializer,
+        required=False,
+        many=True
+    )
+    users = SerializedPKRelatedField(
+        queryset=get_user_model().objects.all(),
+        serializer=NestedUserSerializer,
+        required=False,
+        many=True
+    )
+
+    class Meta:
+        model = ObjectPermission
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
+            'constraints',
+        )
+        brief_fields = (
+            'id', 'url', 'display', 'name', 'description', 'enabled', 'object_types', 'groups', 'users', 'actions',
+        )

+ 94 - 0
netbox/users/api/serializers_/tokens.py

@@ -0,0 +1,94 @@
+from django.conf import settings
+from django.contrib.auth import authenticate
+from rest_framework import serializers
+from rest_framework.exceptions import AuthenticationFailed, PermissionDenied
+
+from netbox.api.fields import IPNetworkSerializer
+from netbox.api.serializers import ValidatedModelSerializer
+from users.models import Token
+from .users import *
+
+__all__ = (
+    'TokenProvisionSerializer',
+    'TokenSerializer',
+)
+
+
+class TokenSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:token-detail')
+    key = serializers.CharField(
+        min_length=40,
+        max_length=40,
+        allow_blank=True,
+        required=False,
+        write_only=not settings.ALLOW_TOKEN_RETRIEVAL
+    )
+    user = UserSerializer(nested=True)
+    allowed_ips = serializers.ListField(
+        child=IPNetworkSerializer(),
+        required=False,
+        allow_empty=True,
+        default=[]
+    )
+
+    class Meta:
+        model = Token
+        fields = (
+            'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description',
+            'allowed_ips',
+        )
+        brief_fields = ('id', 'url', 'display', 'key', 'write_enabled', 'description')
+
+    def to_internal_value(self, data):
+        if 'key' not in data:
+            data['key'] = Token.generate_key()
+        return super().to_internal_value(data)
+
+    def validate(self, data):
+
+        # If the Token is being created on behalf of another user, enforce the grant_token permission.
+        request = self.context.get('request')
+        token_user = data.get('user')
+        if token_user and token_user != request.user and not request.user.has_perm('users.grant_token'):
+            raise PermissionDenied("This user does not have permission to create tokens for other users.")
+
+        return super().validate(data)
+
+
+class TokenProvisionSerializer(TokenSerializer):
+    user = UserSerializer(
+        nested=True,
+        read_only=True
+    )
+    username = serializers.CharField(
+        write_only=True
+    )
+    password = serializers.CharField(
+        write_only=True
+    )
+    last_used = serializers.DateTimeField(
+        read_only=True
+    )
+    key = serializers.CharField(
+        read_only=True
+    )
+
+    class Meta:
+        model = Token
+        fields = (
+            'id', 'url', 'display', 'user', 'created', 'expires', 'last_used', 'key', 'write_enabled', 'description',
+            'allowed_ips', 'username', 'password',
+        )
+
+    def validate(self, data):
+        # Validate the username and password
+        username = data.pop('username')
+        password = data.pop('password')
+        user = authenticate(request=self.context.get('request'), username=username, password=password)
+        if user is None:
+            raise AuthenticationFailed("Invalid username/password")
+
+        # Inject the user into the validated data
+        data['user'] = user
+
+        return data

+ 72 - 0
netbox/users/api/serializers_/users.py

@@ -0,0 +1,72 @@
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
+from drf_spectacular.types import OpenApiTypes
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from netbox.api.fields import SerializedPKRelatedField
+from netbox.api.serializers import ValidatedModelSerializer
+from ..nested_serializers import *
+
+__all__ = (
+    'GroupSerializer',
+    'UserSerializer',
+)
+
+
+class UserSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:user-detail')
+    groups = SerializedPKRelatedField(
+        queryset=Group.objects.all(),
+        serializer=NestedGroupSerializer,
+        required=False,
+        many=True
+    )
+
+    class Meta:
+        model = get_user_model()
+        fields = (
+            'id', 'url', 'display', 'username', 'password', 'first_name', 'last_name', 'email', 'is_staff', 'is_active',
+            'date_joined', 'last_login', 'groups',
+        )
+        brief_fields = ('id', 'url', 'display', 'username')
+        extra_kwargs = {
+            'password': {'write_only': True}
+        }
+
+    def create(self, validated_data):
+        """
+        Extract the password from validated data and set it separately to ensure proper hash generation.
+        """
+        password = validated_data.pop('password')
+        user = super().create(validated_data)
+        user.set_password(password)
+        user.save()
+
+        return user
+
+    def update(self, instance, validated_data):
+        """
+        Ensure proper updated password hash generation.
+        """
+        password = validated_data.pop('password', None)
+        if password is not None:
+            instance.set_password(password)
+
+        return super().update(instance, validated_data)
+
+    @extend_schema_field(OpenApiTypes.STR)
+    def get_display(self, obj):
+        if full_name := obj.get_full_name():
+            return f"{obj.username} ({full_name})"
+        return obj.username
+
+
+class GroupSerializer(ValidatedModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='users-api:group-detail')
+    user_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = Group
+        fields = ('id', 'url', 'display', 'name', 'user_count')
+        brief_fields = ('id', 'url', 'display', 'name')

+ 2 - 186
netbox/virtualization/api/serializers.py

@@ -1,187 +1,3 @@
-from drf_spectacular.utils import extend_schema_field
-from rest_framework import serializers
-
-from dcim.api.serializers import DeviceSerializer, DeviceRoleSerializer, PlatformSerializer, SiteSerializer
-from dcim.choices import InterfaceModeChoices
-from extras.api.serializers import ConfigTemplateSerializer
-from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedVRFSerializer
-from ipam.models import VLAN
-from netbox.api.fields import ChoiceField, RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import NetBoxModelSerializer
-from tenancy.api.serializers import TenantSerializer
-from virtualization.choices import *
-from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualDisk, VirtualMachine, VMInterface
-from vpn.api.nested_serializers import NestedL2VPNTerminationSerializer
+from .serializers_.clusters import *
+from .serializers_.virtualmachines import *
 from .nested_serializers import *
-
-
-#
-# Clusters
-#
-
-class ClusterTypeSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
-
-    # Related object counts
-    cluster_count = RelatedObjectCountField('clusters')
-
-    class Meta:
-        model = ClusterType
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'cluster_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
-
-
-class ClusterGroupSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
-
-    # Related object counts
-    cluster_count = RelatedObjectCountField('clusters')
-
-    class Meta:
-        model = ClusterGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'cluster_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
-
-
-class ClusterSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
-    type = ClusterTypeSerializer(nested=True)
-    group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None)
-    status = ChoiceField(choices=ClusterStatusChoices, required=False)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    # Related object counts
-    device_count = RelatedObjectCountField('devices')
-    virtualmachine_count = RelatedObjectCountField('virtual_machines')
-
-    class Meta:
-        model = Cluster
-        fields = [
-            'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments',
-            'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count')
-
-
-#
-# Virtual machines
-#
-
-class VirtualMachineSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
-    status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
-    site = SiteSerializer(nested=True, required=False, allow_null=True)
-    cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
-    device = DeviceSerializer(nested=True, required=False, allow_null=True)
-    role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    platform = PlatformSerializer(nested=True, required=False, allow_null=True)
-    primary_ip = NestedIPAddressSerializer(read_only=True)
-    primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
-    primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
-    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
-
-    # Counter fields
-    interface_count = serializers.IntegerField(read_only=True)
-    virtual_disk_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = VirtualMachine
-        fields = [
-            'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
-            'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
-            'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
-            'interface_count', 'virtual_disk_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-        validators = []
-
-
-class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
-    config_context = serializers.SerializerMethodField()
-
-    class Meta(VirtualMachineSerializer.Meta):
-        fields = [
-            'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
-            'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
-            'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created',
-            'last_updated', 'interface_count', 'virtual_disk_count',
-        ]
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_config_context(self, obj):
-        return obj.get_config_context()
-
-
-#
-# VM interfaces
-#
-
-class VMInterfaceSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
-    virtual_machine = VirtualMachineSerializer(nested=True)
-    parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
-    bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
-    mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
-    untagged_vlan = NestedVLANSerializer(required=False, allow_null=True)
-    tagged_vlans = SerializedPKRelatedField(
-        queryset=VLAN.objects.all(),
-        serializer=NestedVLANSerializer,
-        required=False,
-        many=True
-    )
-    vrf = NestedVRFSerializer(required=False, allow_null=True)
-    l2vpn_termination = NestedL2VPNTerminationSerializer(read_only=True, allow_null=True)
-    count_ipaddresses = serializers.IntegerField(read_only=True)
-    count_fhrp_groups = serializers.IntegerField(read_only=True)
-    mac_address = serializers.CharField(
-        required=False,
-        default=None,
-        allow_null=True
-    )
-
-    class Meta:
-        model = VMInterface
-        fields = [
-            'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address',
-            'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields',
-            'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
-        ]
-        brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
-
-    def validate(self, data):
-
-        # Validate many-to-many VLAN assignments
-        virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
-        for vlan in data.get('tagged_vlans', []):
-            if vlan.site not in [virtual_machine.site, None]:
-                raise serializers.ValidationError({
-                    'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "
-                                    f"machine, or it must be global."
-                })
-
-        return super().validate(data)
-
-
-#
-# Virtual Disk
-#
-
-class VirtualDiskSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail')
-    virtual_machine = VirtualMachineSerializer(nested=True)
-
-    class Meta:
-        model = VirtualDisk
-        fields = [
-            'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields',
-            'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size')

+ 0 - 0
netbox/virtualization/api/serializers_/__init__.py


+ 65 - 0
netbox/virtualization/api/serializers_/clusters.py

@@ -0,0 +1,65 @@
+from rest_framework import serializers
+
+from dcim.api.serializers_.sites import SiteSerializer
+from netbox.api.fields import ChoiceField, RelatedObjectCountField
+from netbox.api.serializers import NetBoxModelSerializer
+from tenancy.api.serializers_.tenants import TenantSerializer
+from virtualization.choices import *
+from virtualization.models import Cluster, ClusterGroup, ClusterType
+
+__all__ = (
+    'ClusterGroupSerializer',
+    'ClusterSerializer',
+    'ClusterTypeSerializer',
+)
+
+
+class ClusterTypeSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustertype-detail')
+
+    # Related object counts
+    cluster_count = RelatedObjectCountField('clusters')
+
+    class Meta:
+        model = ClusterType
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+            'cluster_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
+
+
+class ClusterGroupSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:clustergroup-detail')
+
+    # Related object counts
+    cluster_count = RelatedObjectCountField('clusters')
+
+    class Meta:
+        model = ClusterGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+            'cluster_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'cluster_count')
+
+
+class ClusterSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
+    type = ClusterTypeSerializer(nested=True)
+    group = ClusterGroupSerializer(nested=True, required=False, allow_null=True, default=None)
+    status = ChoiceField(choices=ClusterStatusChoices, required=False)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    site = SiteSerializer(nested=True, required=False, allow_null=True, default=None)
+
+    # Related object counts
+    device_count = RelatedObjectCountField('devices')
+    virtualmachine_count = RelatedObjectCountField('virtual_machines')
+
+    class Meta:
+        model = Cluster
+        fields = [
+            'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'description', 'comments',
+            'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count')

+ 143 - 0
netbox/virtualization/api/serializers_/virtualmachines.py

@@ -0,0 +1,143 @@
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from dcim.api.serializers_.devices import DeviceSerializer
+from dcim.api.serializers_.platforms import PlatformSerializer
+from dcim.api.serializers_.roles import DeviceRoleSerializer
+from dcim.api.serializers_.sites import SiteSerializer
+from dcim.choices import InterfaceModeChoices
+from extras.api.serializers_.provisioning import ConfigTemplateSerializer
+from ipam.api.nested_serializers import NestedVLANSerializer
+from ipam.api.serializers_.ip import IPAddressSerializer
+from ipam.api.serializers_.vlans import VLANSerializer
+from ipam.api.serializers_.vrfs import VRFSerializer
+from ipam.models import VLAN
+from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from netbox.api.serializers import NetBoxModelSerializer
+from tenancy.api.serializers_.tenants import TenantSerializer
+from virtualization.choices import *
+from virtualization.models import VirtualDisk, VirtualMachine, VMInterface
+from vpn.api.serializers_.l2vpn import L2VPNTerminationSerializer
+from ..nested_serializers import *
+
+from .clusters import ClusterSerializer
+
+__all__ = (
+    'VMInterfaceSerializer',
+    'VirtualDiskSerializer',
+    'VirtualMachineSerializer',
+    'VirtualMachineWithConfigContextSerializer',
+)
+
+
+class VirtualMachineSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualmachine-detail')
+    status = ChoiceField(choices=VirtualMachineStatusChoices, required=False)
+    site = SiteSerializer(nested=True, required=False, allow_null=True)
+    cluster = ClusterSerializer(nested=True, required=False, allow_null=True)
+    device = DeviceSerializer(nested=True, required=False, allow_null=True)
+    role = DeviceRoleSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    platform = PlatformSerializer(nested=True, required=False, allow_null=True)
+    primary_ip = IPAddressSerializer(nested=True, read_only=True)
+    primary_ip4 = IPAddressSerializer(nested=True, required=False, allow_null=True)
+    primary_ip6 = IPAddressSerializer(nested=True, required=False, allow_null=True)
+    config_template = ConfigTemplateSerializer(nested=True, required=False, allow_null=True, default=None)
+
+    # Counter fields
+    interface_count = serializers.IntegerField(read_only=True)
+    virtual_disk_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = VirtualMachine
+        fields = [
+            'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
+            'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
+            'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
+            'interface_count', 'virtual_disk_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+        validators = []
+
+
+class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer):
+    config_context = serializers.SerializerMethodField()
+
+    class Meta(VirtualMachineSerializer.Meta):
+        fields = [
+            'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
+            'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
+            'config_template', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'created',
+            'last_updated', 'interface_count', 'virtual_disk_count',
+        ]
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_config_context(self, obj):
+        return obj.get_config_context()
+
+
+#
+# VM interfaces
+#
+
+class VMInterfaceSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:vminterface-detail')
+    virtual_machine = VirtualMachineSerializer(nested=True)
+    parent = NestedVMInterfaceSerializer(required=False, allow_null=True)
+    bridge = NestedVMInterfaceSerializer(required=False, allow_null=True)
+    mode = ChoiceField(choices=InterfaceModeChoices, allow_blank=True, required=False)
+    untagged_vlan = VLANSerializer(nested=True, required=False, allow_null=True)
+    tagged_vlans = SerializedPKRelatedField(
+        queryset=VLAN.objects.all(),
+        serializer=NestedVLANSerializer,
+        required=False,
+        many=True
+    )
+    vrf = VRFSerializer(nested=True, required=False, allow_null=True)
+    l2vpn_termination = L2VPNTerminationSerializer(nested=True, read_only=True, allow_null=True)
+    count_ipaddresses = serializers.IntegerField(read_only=True)
+    count_fhrp_groups = serializers.IntegerField(read_only=True)
+    mac_address = serializers.CharField(
+        required=False,
+        default=None,
+        allow_null=True
+    )
+
+    class Meta:
+        model = VMInterface
+        fields = [
+            'id', 'url', 'display', 'virtual_machine', 'name', 'enabled', 'parent', 'bridge', 'mtu', 'mac_address',
+            'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'vrf', 'l2vpn_termination', 'tags', 'custom_fields',
+            'created', 'last_updated', 'count_ipaddresses', 'count_fhrp_groups',
+        ]
+        brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description')
+
+    def validate(self, data):
+
+        # Validate many-to-many VLAN assignments
+        virtual_machine = self.instance.virtual_machine if self.instance else data.get('virtual_machine')
+        for vlan in data.get('tagged_vlans', []):
+            if vlan.site not in [virtual_machine.site, None]:
+                raise serializers.ValidationError({
+                    'tagged_vlans': f"VLAN {vlan} must belong to the same site as the interface's parent virtual "
+                                    f"machine, or it must be global."
+                })
+
+        return super().validate(data)
+
+
+#
+# Virtual Disk
+#
+
+class VirtualDiskSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:virtualdisk-detail')
+    virtual_machine = VirtualMachineSerializer(nested=True)
+
+    class Meta:
+        model = VirtualDisk
+        fields = [
+            'id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size', 'tags', 'custom_fields',
+            'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'virtual_machine', 'name', 'description', 'size')

+ 3 - 296
netbox/vpn/api/serializers.py

@@ -1,297 +1,4 @@
-from django.contrib.contenttypes.models import ContentType
-from drf_spectacular.utils import extend_schema_field
-from rest_framework import serializers
-
-from ipam.api.serializers import IPAddressSerializer
-from ipam.api.nested_serializers import NestedRouteTargetSerializer
-from ipam.models import RouteTarget
-from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField, SerializedPKRelatedField
-from netbox.api.serializers import NetBoxModelSerializer
-from netbox.constants import NESTED_SERIALIZER_PREFIX
-from tenancy.api.serializers import TenantSerializer
-from utilities.api import get_serializer_for_model
-from vpn.choices import *
-from vpn.models import *
+from .serializers_.crypto import *
+from .serializers_.tunnels import *
+from .serializers_.l2vpn import *
 from .nested_serializers import *
-
-__all__ = (
-    'IKEPolicySerializer',
-    'IKEProposalSerializer',
-    'IPSecPolicySerializer',
-    'IPSecProfileSerializer',
-    'IPSecProposalSerializer',
-    'L2VPNSerializer',
-    'L2VPNTerminationSerializer',
-    'TunnelGroupSerializer',
-    'TunnelSerializer',
-    'TunnelTerminationSerializer',
-)
-
-
-class IKEProposalSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:ikeproposal-detail'
-    )
-    authentication_method = ChoiceField(
-        choices=AuthenticationMethodChoices
-    )
-    encryption_algorithm = ChoiceField(
-        choices=EncryptionAlgorithmChoices
-    )
-    authentication_algorithm = ChoiceField(
-        choices=AuthenticationAlgorithmChoices
-    )
-    group = ChoiceField(
-        choices=DHGroupChoices
-    )
-
-    class Meta:
-        model = IKEProposal
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm',
-            'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created',
-            'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class IKEPolicySerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:ikepolicy-detail'
-    )
-    version = ChoiceField(
-        choices=IKEVersionChoices
-    )
-    mode = ChoiceField(
-        choices=IKEModeChoices
-    )
-    proposals = SerializedPKRelatedField(
-        queryset=IKEProposal.objects.all(),
-        serializer=NestedIKEProposalSerializer,
-        required=False,
-        many=True
-    )
-
-    class Meta:
-        model = IKEPolicy
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments',
-            'tags', 'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class IPSecProposalSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:ipsecproposal-detail'
-    )
-    encryption_algorithm = ChoiceField(
-        choices=EncryptionAlgorithmChoices
-    )
-    authentication_algorithm = ChoiceField(
-        choices=AuthenticationAlgorithmChoices
-    )
-
-    class Meta:
-        model = IPSecProposal
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm',
-            'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class IPSecPolicySerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:ipsecpolicy-detail'
-    )
-    proposals = SerializedPKRelatedField(
-        queryset=IPSecProposal.objects.all(),
-        serializer=NestedIPSecProposalSerializer,
-        required=False,
-        many=True
-    )
-    pfs_group = ChoiceField(
-        choices=DHGroupChoices,
-        required=False
-    )
-
-    class Meta:
-        model = IPSecPolicy
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags',
-            'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class IPSecProfileSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:ipsecprofile-detail'
-    )
-    mode = ChoiceField(
-        choices=IPSecModeChoices
-    )
-    ike_policy = IKEPolicySerializer(
-        nested=True
-    )
-    ipsec_policy = IPSecPolicySerializer(
-        nested=True
-    )
-
-    class Meta:
-        model = IPSecProfile
-        fields = (
-            'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
-            'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-#
-# Tunnels
-#
-
-class TunnelGroupSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
-
-    # Related object counts
-    tunnel_count = RelatedObjectCountField('tunnels')
-
-    class Meta:
-        model = TunnelGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
-            'tunnel_count',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count')
-
-
-class TunnelSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:tunnel-detail'
-    )
-    status = ChoiceField(
-        choices=TunnelStatusChoices
-    )
-    group = TunnelGroupSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-    encapsulation = ChoiceField(
-        choices=TunnelEncapsulationChoices
-    )
-    ipsec_profile = IPSecProfileSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-    tenant = TenantSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-
-    # Related object counts
-    terminations_count = RelatedObjectCountField('terminations')
-
-    class Meta:
-        model = Tunnel
-        fields = (
-            'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
-            'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count',
-        )
-        brief_fields = ('id', 'url', 'display', 'name', 'description')
-
-
-class TunnelTerminationSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(
-        view_name='vpn-api:tunneltermination-detail'
-    )
-    tunnel = TunnelSerializer(
-        nested=True
-    )
-    role = ChoiceField(
-        choices=TunnelTerminationRoleChoices
-    )
-    termination_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    termination = serializers.SerializerMethodField(
-        read_only=True
-    )
-    outside_ip = IPAddressSerializer(
-        nested=True,
-        required=False,
-        allow_null=True
-    )
-
-    class Meta:
-        model = TunnelTermination
-        fields = (
-            'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
-            'tags', 'custom_fields', 'created', 'last_updated',
-        )
-        brief_fields = ('id', 'url', 'display')
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_termination(self, obj):
-        serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(obj.termination, context=context).data
-
-
-#
-# L2VPN
-#
-
-class L2VPNSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
-    type = ChoiceField(choices=L2VPNTypeChoices, required=False)
-    import_targets = SerializedPKRelatedField(
-        queryset=RouteTarget.objects.all(),
-        serializer=NestedRouteTargetSerializer,
-        required=False,
-        many=True
-    )
-    export_targets = SerializedPKRelatedField(
-        queryset=RouteTarget.objects.all(),
-        serializer=NestedRouteTargetSerializer,
-        required=False,
-        many=True
-    )
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-
-    class Meta:
-        model = L2VPN
-        fields = [
-            'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
-            'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
-        ]
-        brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description')
-
-
-class L2VPNTerminationSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
-    l2vpn = L2VPNSerializer(
-        nested=True
-    )
-    assigned_object_type = ContentTypeField(
-        queryset=ContentType.objects.all()
-    )
-    assigned_object = serializers.SerializerMethodField(read_only=True)
-
-    class Meta:
-        model = L2VPNTermination
-        fields = [
-            'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
-            'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
-        ]
-        brief_fields = ('id', 'url', 'display', 'l2vpn')
-
-    @extend_schema_field(serializers.JSONField(allow_null=True))
-    def get_assigned_object(self, instance):
-        serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
-        context = {'request': self.context['request']}
-        return serializer(instance.assigned_object, context=context).data

+ 0 - 0
netbox/vpn/api/serializers_/__init__.py


+ 135 - 0
netbox/vpn/api/serializers_/crypto.py

@@ -0,0 +1,135 @@
+from rest_framework import serializers
+
+from netbox.api.fields import ChoiceField, SerializedPKRelatedField
+from netbox.api.serializers import NetBoxModelSerializer
+from vpn.choices import *
+from vpn.models import IKEPolicy, IKEProposal, IPSecPolicy, IPSecProfile, IPSecProposal
+from ..nested_serializers import *
+
+__all__ = (
+    'IKEPolicySerializer',
+    'IKEProposalSerializer',
+    'IPSecPolicySerializer',
+    'IPSecProfileSerializer',
+    'IPSecProposalSerializer',
+)
+
+
+class IKEProposalSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:ikeproposal-detail'
+    )
+    authentication_method = ChoiceField(
+        choices=AuthenticationMethodChoices
+    )
+    encryption_algorithm = ChoiceField(
+        choices=EncryptionAlgorithmChoices
+    )
+    authentication_algorithm = ChoiceField(
+        choices=AuthenticationAlgorithmChoices
+    )
+    group = ChoiceField(
+        choices=DHGroupChoices
+    )
+
+    class Meta:
+        model = IKEProposal
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'authentication_method', 'encryption_algorithm',
+            'authentication_algorithm', 'group', 'sa_lifetime', 'comments', 'tags', 'custom_fields', 'created',
+            'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class IKEPolicySerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:ikepolicy-detail'
+    )
+    version = ChoiceField(
+        choices=IKEVersionChoices
+    )
+    mode = ChoiceField(
+        choices=IKEModeChoices
+    )
+    proposals = SerializedPKRelatedField(
+        queryset=IKEProposal.objects.all(),
+        serializer=NestedIKEProposalSerializer,
+        required=False,
+        many=True
+    )
+
+    class Meta:
+        model = IKEPolicy
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'version', 'mode', 'proposals', 'preshared_key', 'comments',
+            'tags', 'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class IPSecProposalSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:ipsecproposal-detail'
+    )
+    encryption_algorithm = ChoiceField(
+        choices=EncryptionAlgorithmChoices
+    )
+    authentication_algorithm = ChoiceField(
+        choices=AuthenticationAlgorithmChoices
+    )
+
+    class Meta:
+        model = IPSecProposal
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'encryption_algorithm', 'authentication_algorithm',
+            'sa_lifetime_seconds', 'sa_lifetime_data', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class IPSecPolicySerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:ipsecpolicy-detail'
+    )
+    proposals = SerializedPKRelatedField(
+        queryset=IPSecProposal.objects.all(),
+        serializer=NestedIPSecProposalSerializer,
+        required=False,
+        many=True
+    )
+    pfs_group = ChoiceField(
+        choices=DHGroupChoices,
+        required=False
+    )
+
+    class Meta:
+        model = IPSecPolicy
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'proposals', 'pfs_group', 'comments', 'tags',
+            'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class IPSecProfileSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:ipsecprofile-detail'
+    )
+    mode = ChoiceField(
+        choices=IPSecModeChoices
+    )
+    ike_policy = IKEPolicySerializer(
+        nested=True
+    )
+    ipsec_policy = IPSecPolicySerializer(
+        nested=True
+    )
+
+    class Meta:
+        model = IPSecProfile
+        fields = (
+            'id', 'url', 'display', 'name', 'description', 'mode', 'ike_policy', 'ipsec_policy', 'comments', 'tags',
+            'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')

+ 69 - 0
netbox/vpn/api/serializers_/l2vpn.py

@@ -0,0 +1,69 @@
+from django.contrib.contenttypes.models import ContentType
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from ipam.api.nested_serializers import NestedRouteTargetSerializer
+from ipam.models import RouteTarget
+from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField
+from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from tenancy.api.serializers_.tenants import TenantSerializer
+from utilities.api import get_serializer_for_model
+from vpn.choices import *
+from vpn.models import L2VPN, L2VPNTermination
+
+__all__ = (
+    'L2VPNSerializer',
+    'L2VPNTerminationSerializer',
+)
+
+
+class L2VPNSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpn-detail')
+    type = ChoiceField(choices=L2VPNTypeChoices, required=False)
+    import_targets = SerializedPKRelatedField(
+        queryset=RouteTarget.objects.all(),
+        serializer=NestedRouteTargetSerializer,
+        required=False,
+        many=True
+    )
+    export_targets = SerializedPKRelatedField(
+        queryset=RouteTarget.objects.all(),
+        serializer=NestedRouteTargetSerializer,
+        required=False,
+        many=True
+    )
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+
+    class Meta:
+        model = L2VPN
+        fields = [
+            'id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'import_targets', 'export_targets',
+            'description', 'comments', 'tenant', 'tags', 'custom_fields', 'created', 'last_updated'
+        ]
+        brief_fields = ('id', 'url', 'display', 'identifier', 'name', 'slug', 'type', 'description')
+
+
+class L2VPNTerminationSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:l2vpntermination-detail')
+    l2vpn = L2VPNSerializer(
+        nested=True
+    )
+    assigned_object_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    assigned_object = serializers.SerializerMethodField(read_only=True)
+
+    class Meta:
+        model = L2VPNTermination
+        fields = [
+            'id', 'url', 'display', 'l2vpn', 'assigned_object_type', 'assigned_object_id',
+            'assigned_object', 'tags', 'custom_fields', 'created', 'last_updated'
+        ]
+        brief_fields = ('id', 'url', 'display', 'l2vpn')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_assigned_object(self, instance):
+        serializer = get_serializer_for_model(instance.assigned_object, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(instance.assigned_object, context=context).data

+ 114 - 0
netbox/vpn/api/serializers_/tunnels.py

@@ -0,0 +1,114 @@
+from django.contrib.contenttypes.models import ContentType
+from drf_spectacular.utils import extend_schema_field
+from rest_framework import serializers
+
+from ipam.api.serializers_.ip import IPAddressSerializer
+from netbox.api.fields import ChoiceField, ContentTypeField, RelatedObjectCountField
+from netbox.api.serializers import NetBoxModelSerializer
+from netbox.constants import NESTED_SERIALIZER_PREFIX
+from tenancy.api.serializers_.tenants import TenantSerializer
+from utilities.api import get_serializer_for_model
+from vpn.choices import *
+from vpn.models import Tunnel, TunnelGroup, TunnelTermination
+
+from .crypto import IPSecProfileSerializer
+
+__all__ = (
+    'TunnelGroupSerializer',
+    'TunnelSerializer',
+    'TunnelTerminationSerializer',
+)
+
+
+#
+# Tunnels
+#
+
+class TunnelGroupSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='vpn-api:tunnelgroup-detail')
+
+    # Related object counts
+    tunnel_count = RelatedObjectCountField('tunnels')
+
+    class Meta:
+        model = TunnelGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'description', 'tags', 'custom_fields', 'created', 'last_updated',
+            'tunnel_count',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'tunnel_count')
+
+
+class TunnelSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:tunnel-detail'
+    )
+    status = ChoiceField(
+        choices=TunnelStatusChoices
+    )
+    group = TunnelGroupSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    encapsulation = ChoiceField(
+        choices=TunnelEncapsulationChoices
+    )
+    ipsec_profile = IPSecProfileSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+    tenant = TenantSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+
+    # Related object counts
+    terminations_count = RelatedObjectCountField('terminations')
+
+    class Meta:
+        model = Tunnel
+        fields = (
+            'id', 'url', 'display', 'name', 'status', 'group', 'encapsulation', 'ipsec_profile', 'tenant', 'tunnel_id',
+            'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'terminations_count',
+        )
+        brief_fields = ('id', 'url', 'display', 'name', 'description')
+
+
+class TunnelTerminationSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(
+        view_name='vpn-api:tunneltermination-detail'
+    )
+    tunnel = TunnelSerializer(
+        nested=True
+    )
+    role = ChoiceField(
+        choices=TunnelTerminationRoleChoices
+    )
+    termination_type = ContentTypeField(
+        queryset=ContentType.objects.all()
+    )
+    termination = serializers.SerializerMethodField(
+        read_only=True
+    )
+    outside_ip = IPAddressSerializer(
+        nested=True,
+        required=False,
+        allow_null=True
+    )
+
+    class Meta:
+        model = TunnelTermination
+        fields = (
+            'id', 'url', 'display', 'tunnel', 'role', 'termination_type', 'termination_id', 'termination', 'outside_ip',
+            'tags', 'custom_fields', 'created', 'last_updated',
+        )
+        brief_fields = ('id', 'url', 'display')
+
+    @extend_schema_field(serializers.JSONField(allow_null=True))
+    def get_termination(self, obj):
+        serializer = get_serializer_for_model(obj.termination, prefix=NESTED_SERIALIZER_PREFIX)
+        context = {'request': self.context['request']}
+        return serializer(obj.termination, context=context).data

+ 2 - 66
netbox/wireless/api/serializers.py

@@ -1,67 +1,3 @@
-from rest_framework import serializers
-
-from dcim.choices import LinkStatusChoices
-from dcim.api.serializers import InterfaceSerializer
-from ipam.api.serializers import VLANSerializer
-from netbox.api.fields import ChoiceField
-from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
-from tenancy.api.serializers import TenantSerializer
-from wireless.choices import *
-from wireless.models import *
+from .serializers_.wirelesslans import *
+from .serializers_.wirelesslinks import *
 from .nested_serializers import *
-
-__all__ = (
-    'WirelessLANGroupSerializer',
-    'WirelessLANSerializer',
-    'WirelessLinkSerializer',
-)
-
-
-class WirelessLANGroupSerializer(NestedGroupModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
-    parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
-    wirelesslan_count = serializers.IntegerField(read_only=True)
-
-    class Meta:
-        model = WirelessLANGroup
-        fields = [
-            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
-            'last_updated', 'wirelesslan_count', '_depth',
-        ]
-        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth')
-
-
-class WirelessLANSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
-    group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True)
-    status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True)
-    vlan = VLANSerializer(nested=True, required=False, allow_null=True)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
-    auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
-
-    class Meta:
-        model = WirelessLAN
-        fields = [
-            'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type',
-            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'ssid', 'description')
-
-
-class WirelessLinkSerializer(NetBoxModelSerializer):
-    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
-    status = ChoiceField(choices=LinkStatusChoices, required=False)
-    interface_a = InterfaceSerializer(nested=True)
-    interface_b = InterfaceSerializer(nested=True)
-    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
-    auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
-    auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
-
-    class Meta:
-        model = WirelessLink
-        fields = [
-            'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type',
-            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
-        ]
-        brief_fields = ('id', 'url', 'display', 'ssid', 'description')

+ 0 - 0
netbox/wireless/api/serializers_/__init__.py


+ 46 - 0
netbox/wireless/api/serializers_/wirelesslans.py

@@ -0,0 +1,46 @@
+from rest_framework import serializers
+
+from ipam.api.serializers_.vlans import VLANSerializer
+from netbox.api.fields import ChoiceField
+from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
+from tenancy.api.serializers_.tenants import TenantSerializer
+from wireless.choices import *
+from wireless.models import WirelessLAN, WirelessLANGroup
+from ..nested_serializers import *
+
+__all__ = (
+    'WirelessLANGroupSerializer',
+    'WirelessLANSerializer',
+)
+
+
+class WirelessLANGroupSerializer(NestedGroupModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslangroup-detail')
+    parent = NestedWirelessLANGroupSerializer(required=False, allow_null=True, default=None)
+    wirelesslan_count = serializers.IntegerField(read_only=True)
+
+    class Meta:
+        model = WirelessLANGroup
+        fields = [
+            'id', 'url', 'display', 'name', 'slug', 'parent', 'description', 'tags', 'custom_fields', 'created',
+            'last_updated', 'wirelesslan_count', '_depth',
+        ]
+        brief_fields = ('id', 'url', 'display', 'name', 'slug', 'description', 'wirelesslan_count', '_depth')
+
+
+class WirelessLANSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslan-detail')
+    group = WirelessLANGroupSerializer(nested=True, required=False, allow_null=True)
+    status = ChoiceField(choices=WirelessLANStatusChoices, required=False, allow_blank=True)
+    vlan = VLANSerializer(nested=True, required=False, allow_null=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
+    auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
+
+    class Meta:
+        model = WirelessLAN
+        fields = [
+            'id', 'url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant', 'auth_type',
+            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'ssid', 'description')

+ 31 - 0
netbox/wireless/api/serializers_/wirelesslinks.py

@@ -0,0 +1,31 @@
+from rest_framework import serializers
+
+from dcim.api.serializers_.device_components import InterfaceSerializer
+from dcim.choices import LinkStatusChoices
+from netbox.api.fields import ChoiceField
+from netbox.api.serializers import NetBoxModelSerializer
+from tenancy.api.serializers_.tenants import TenantSerializer
+from wireless.choices import *
+from wireless.models import WirelessLink
+
+__all__ = (
+    'WirelessLinkSerializer',
+)
+
+
+class WirelessLinkSerializer(NetBoxModelSerializer):
+    url = serializers.HyperlinkedIdentityField(view_name='wireless-api:wirelesslink-detail')
+    status = ChoiceField(choices=LinkStatusChoices, required=False)
+    interface_a = InterfaceSerializer(nested=True)
+    interface_b = InterfaceSerializer(nested=True)
+    tenant = TenantSerializer(nested=True, required=False, allow_null=True)
+    auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
+    auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
+
+    class Meta:
+        model = WirelessLink
+        fields = [
+            'id', 'url', 'display', 'interface_a', 'interface_b', 'ssid', 'status', 'tenant', 'auth_type',
+            'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
+        ]
+        brief_fields = ('id', 'url', 'display', 'ssid', 'description')